diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2020-04-14 09:21:00 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2020-04-14 09:21:00 +0000 |
| commit | 5faab5d81cc5ed36824a52a273e83eb20240cb2e (patch) | |
| tree | dba10fadf4cdad0f5524df642fab83f6310a8574 /alv-lib | |
| parent | move spec out of spec/core (diff) | |
| download | alive-5faab5d81cc5ed36824a52a273e83eb20240cb2e.tar.gz alive-5faab5d81cc5ed36824a52a273e83eb20240cb2e.zip | |
move lib to alv-lib module
Diffstat (limited to 'alv-lib')
| -rw-r--r-- | alv-lib/logic.moon | 153 | ||||
| -rw-r--r-- | alv-lib/math.moon | 192 | ||||
| -rw-r--r-- | alv-lib/midi.moon | 99 | ||||
| -rw-r--r-- | alv-lib/midi/core.moon | 122 | ||||
| -rw-r--r-- | alv-lib/midi/launchctl.moon | 194 | ||||
| -rw-r--r-- | alv-lib/osc.moon | 87 | ||||
| -rw-r--r-- | alv-lib/pilot.moon | 90 | ||||
| -rw-r--r-- | alv-lib/random.moon | 85 | ||||
| -rw-r--r-- | alv-lib/sc.moon | 83 | ||||
| -rw-r--r-- | alv-lib/string.moon | 18 | ||||
| -rw-r--r-- | alv-lib/time.moon | 285 | ||||
| -rw-r--r-- | alv-lib/util.moon | 112 |
12 files changed, 1520 insertions, 0 deletions
diff --git a/alv-lib/logic.moon b/alv-lib/logic.moon new file mode 100644 index 0000000..62de1c1 --- /dev/null +++ b/alv-lib/logic.moon @@ -0,0 +1,153 @@ +import Op, ValueStream, Input, Error, val from require 'alv.base' + +all_same = (first, list) -> + for v in *list + if v != first + return false + + first + +tobool = (val) -> + switch val + when false, nil, 0 + false + else + true + +class ReduceOp extends Op + pattern = val! + val! * 0 + setup: (inputs) => + @out or= ValueStream 'bool' + { first, rest } = pattern\match inputs + super + first: Input.hot first + rest: [Input.hot v for v in *rest] + + tick: => + { :first, :rest } = @unwrap_all! + accum = tobool first + for val in *rest + accum = @.fn accum, tobool val + + @out\set accum + +eq = ValueStream.meta + meta: + name: 'eq' + summary: "Check for equality." + examples: { '(== a b [c]…)', '(eq a b [c]…)' } + description: "`true` if the types and values of all arguments are equal." + + value: class extends Op + pattern = val! + val! * 0 + setup: (inputs) => + @out or= ValueStream 'bool', false + { first, rest } = pattern\match inputs + same = all_same first\type!, [i\type! for i in *rest] + + super if same + { + first: Input.hot first + rest: [Input.hot v for v in *rest] + } + else + {} + + tick: => + if not @inputs.first + @out\set false + return + + { :first, :rest } = @unwrap_all! + + equal = true + for other in *rest + if first != other + equal = false + break + + @out\set equal + +not_eq = ValueStream.meta + meta: + name: 'not-eq' + summary: "Check for inequality." + examples: { '(!= a b [c]…)', '(not-eq a b [c]…)' } + description: "`true` if types or values of any two arguments are different." + + value: class extends Op + setup: (inputs) => + @out or= ValueStream 'bool', false + assert #inputs > 1, Error 'argument', "need at least two values" + super [Input.hot v for v in *inputs] + + tick: => + if not @inputs[1] + @out\set true + return + + diff = true + for a=1, #@inputs-1 + for b=a+1, #@inputs + if @inputs[a].stream == @inputs[b].stream + diff = false + break + + break unless diff + + @out\set diff + +and_ = ValueStream.meta + meta: + name: 'and' + summary: "Logical AND." + examples: { '(and a b [c…])' } + value: class extends ReduceOp + fn: (a, b) -> a and b + +or_ = ValueStream.meta + meta: + name: 'or' + summary: "Logical OR." + examples: { '(or a b [c…])' } + value: class extends ReduceOp + fn: (a, b) -> a or b + +not_ = ValueStream.meta + meta: + name: 'not' + summary: "Logical NOT." + examples: { '(not a)' } + + value: class extends Op + new: => super 'bool' + + setup: (inputs) => + { value } = match 'any', inputs + super value: Input.hot value + + tick: => @out\set not tobool @inputs.value! + +bool = ValueStream.meta + meta: + name: 'bool' + summary: "Cast value to bool." + examples: { '(bool a)' } + description: "`false` if a is `false`, `nil` or `0`, `true` otherwise." + + value: class extends Op + setup: (inputs) => + @out or= ValueStream 'bool' + { value } = val!\match inputs + super value: Input.hot value + + tick: => @out\set tobool @inputs\value! + +{ + :eq, '==': eq + 'not-eq': not_eq, '!=': not_eq + and: and_ + or: or_ + not: not_ + :bool +} diff --git a/alv-lib/math.moon b/alv-lib/math.moon new file mode 100644 index 0000000..6f5f3fb --- /dev/null +++ b/alv-lib/math.moon @@ -0,0 +1,192 @@ +import Op, ValueStream, Error, Input, val from require 'alv.base' +unpack or= table.unpack + +class ReduceOp extends Op + pattern = val.num + val.num*0 + setup: (inputs) => + @out or= ValueStream 'num' + { first, rest } = pattern\match inputs + super + first: Input.hot first + rest: [Input.hot v for v in *rest] + + tick: => + { :first, :rest } = @unwrap_all! + accum = first + for val in *rest + accum = @.fn accum, val + @out\set accum + +func_op = (func, pattern) -> + class extends Op + + setup: (inputs) => + @out or= ValueStream 'num' + params = pattern\match inputs + super [Input.hot p for p in *params] + + tick: => @out\set func unpack @unwrap_all! + +func_def = (name, args, func, summary, pattern) -> + ValueStream.meta + meta: + :name + :summary + examples: { "(#{name} #{args})" } + value: func_op func, pattern or val.num\rep 1, 1 + +evenodd_op = (remainder) -> + class extends Op + pattern = val.num + -val.num + setup: (inputs) => + @out or= ValueStream 'bool' + { val, div } = pattern\match inputs + super + val: Input.hot val + div: Input.hot div or ValueStream.num 2 + + tick: => + { :val, :div } = @unwrap_all! + @out\set (val % div) == remainder + +add = ValueStream.meta + meta: + name: 'add' + summary: "Add values." + examples: { '(+ a b [c…])', '(add a b [c…])' } + description: "Sum all arguments." + value: class extends ReduceOp + fn: (a, b) -> a + b + +sub = ValueStream.meta + meta: + name: 'sub' + summary: "Subtract values." + examples: { '(- a b [c…])', '(sub a b [c…])' } + description: "Subtract all other arguments from `a`." + value: class extends ReduceOp + fn: (a, b) -> a - b + +mul = ValueStream.meta + meta: + name: 'mul' + summary: "Multiply values." + examples: { '(* a b [c…])', '(mul a b [c…])' } + value: class extends ReduceOp + fn: (a, b) -> a * b + +div = ValueStream.meta + meta: + name: 'div' + summary: "Divide values." + examples: { '(/ a b [c…])', '(div a b [c…])' } + description: "Divide `a` by all other arguments." + value: class extends ReduceOp + fn: (a, b) -> a / b + +pow = ValueStream.meta + meta: + name: 'pow' + summary: "Raise to a power." + examples: { '(^ base exp)', '(pow base exp' } + description: "Raise `base` to the power `exp`." + value: class extends ReduceOp + fn: (a, b) -> a ^ b + +mod = ValueStream.meta + meta: + name: 'mod' + summary: 'Modulo operator.' + examples: { '(% num div)', '(mod num div)' } + description: "Calculate remainder of division by `div`." + value: func_op 2, (a, b) -> a % b + +even = ValueStream.meta + meta: + name: 'even' + summary: 'Check whether val is even.' + examples: { '(even val [div])' } + description: "`true` if dividing `val` by `div` has remainder zero. +`div` defaults to 2." + value: evenodd_op 0 + +odd = ValueStream.meta + meta: + name: 'odd' + summary: 'Check whether val is odd.' + examples: { '(odd val [div])' } + description: "`true` if dividing `val` by `div` has remainder one. +`div` defaults to 2." + value: evenodd_op 1 + +mix = ValueStream.meta + meta: + name: 'mix' + summary: 'Linearly interpolate.' + examples: { '(mix a b i)' } + description: "Interpolate between `a` and `b` using `i` in range 0-1." + value: func_op 3, (a, b, i) -> i*b + (1-i)*a + +min = ValueStream.meta + meta: + name: 'min' + summary: "Find the minimum." + examples: { '(min a b [c…])' } + description: "Return the lowest of arguments." + value: func_op '*', math.min + +max = ValueStream.meta + meta: + name: 'max' + summary: "Find the maximum." + examples: { '(max a b [c…])' } + description: "Return the highest of arguments." + value: func_op '*', math.min + +cos = func_def 'cos', 'alpha', math.cos, "Cosine function (radians)." +sin = func_def 'sin', 'alpha', math.sin, "Sine function (radians)." +tan = func_def 'tan', 'alpha', math.tan, "Tangent function (radians)." +acos = func_def 'acos', 'cos', math.acos, "Inverse cosine function (radians)." +asin = func_def 'asin', 'sin', math.asin, "Inverse sine function (radians)." +atan = func_def 'atan', 'tan', math.atan, "Inverse tangent function (radians)." +atan2 = func_def 'atan2', 'y x', math.atan2, "Inverse tangent function (two argument version).", val.num\rep(2, 2) +cosh = func_def 'cosh', 'alpha', math.cosh, "Hyperbolic cosine function (radians)." +sinh = func_def 'sinh', 'alpha', math.sinh, "Hyperbolic sine function (radians)." +tanh = func_def 'tanh', 'alpha', math.tanh, "Hyperbolic tangent function (radians)." + +floor = func_def 'floor', 'val', math.floor, "Round towards negative infinity." +ceil = func_def 'ceil', 'val', math.ceil, "Round towards positive infinity." +abs = func_def 'abs', 'val', math.abs, "Get the absolute value." + +exp = func_def 'exp', 'exp', math.floor, "*e* number raised to a power." +log = func_def 'log', 'val [base]', math.log, "Logarithm with given base.", val.num*2 +log10 = func_def 'log10', 'val', math.log10, "Logarithm with base 10." +sqrt = func_def 'sqrt', 'val', math.sqrt, "Square root function." + +{ + :add, '+': add + :sub, '-': sub + :mul, '*': mul + :div, '/': div + :pow, '^': pow + :mod, '%': mod + + :even, :odd + + :mix + :min, :max + + pi: with ValueStream.wrap math.pi + .meta = summary: 'The pi constant.' + tau: with ValueStream.wrap math.pi*2 + .meta = summary: 'The tau constant.' + huge: with ValueStream.wrap math.huge + .meta = summary: 'Positive infinity constant.' + + :sin, :cos, :tan + :asin, :acos, :atan, :atan2 + :sinh, :cosh, :tanh + + :floor, :ceil, :abs + :exp, :log, :log10, :sqrt +} diff --git a/alv-lib/midi.moon b/alv-lib/midi.moon new file mode 100644 index 0000000..88d9547 --- /dev/null +++ b/alv-lib/midi.moon @@ -0,0 +1,99 @@ +import ValueStream, EventStream, Op, Input, val, evt from require 'alv.base' +import input, output, inout, apply_range from require 'alv-lib.midi.core' + +gate = ValueStream.meta + meta: + name: 'gate' + summary: "gate from note-on and note-off messages." + examples: { '(midi/gate [port] note [chan])' } + + value: class extends Op + pattern = -evt['midi/port'] + val.num -val.num + setup: (inputs, scope) => + @out or= ValueStream 'bool' + { port, note, chan } = pattern\match inputs + super + port: Input.hot port or scope\get '*midi*' + note: Input.hot note + chan: Input.hot chan or ValueStream.num -1 + + tick: => + { :port, :note, :chan } = @inputs + + if note\dirty! or chan\dirty! + @out\set false + + if port\dirty! + for msg in *port! + if msg.a == note! and (chan! == -1 or msg.chan == chan!) + if msg.status == 'note-on' + @out\set true + elseif msg.status == 'note-off' + @out\set false + +trig = ValueStream.meta + meta: + name: 'trig' + summary: "`bang`s from note-on messages." + examples: { '(midi/trig [port] note [chan])' } + + value: class extends Op + pattern = -evt['midi/port'] + val.num -val.num + setup: (inputs, scope) => + @out or= EventStream 'bang' + { port, note, chan } = pattern\match inputs + super + port: Input.hot port or scope\get '*midi*' + note: Input.cold note + chan: Input.cold chan or ValueStream.num -1 + + tick: => + { :port, :note, :chan } = @inputs + + for msg in *port! + if msg.a == note! and (chan! == -1 or msg.chan == chan!) + if msg.status == 'note-on' + @out\add true + +cc = ValueStream.meta + meta: + name: 'cc' + summary: "`num` from cc-change messages." + examples: { '(midi/cc [port] cc [chan [range]])' } + description: " +`range` can be one of: +- 'raw' [ 0 - 128[ +- 'uni' [ 0 - 1[ (default) +- 'bip' [-1 - 1[ +- 'rad' [ 0 - tau[ +- 'deg' [ 0 - 360[ +- (num) [ 0 - num[" + + value: class extends Op + pattern = -evt['midi/port'] + val.num + -val.num + -val.num + setup: (inputs, scope) => + { port, cc, chan, range } = pattern\match inputs + super + port: Input.hot port or scope\get '*midi*' + cc: Input.cold cc + chan: Input.cold chan or ValueStream.num -1 + range: Input.cold range or ValueStream.str 'uni' + + @out or= ValueStream 'num', apply_range @inputs.range, 0 + + tick: => + { :port, :cc, :chan, :range } = @inputs + for msg in *port! + if msg.status == 'control-change' and + (chan! == -1 or msg.chan == chan!) and + msg.a == cc! + @out\set apply_range range, msg.b + +{ + :input + :output + :inout + :gate + :trig + :cc +} diff --git a/alv-lib/midi/core.moon b/alv-lib/midi/core.moon new file mode 100644 index 0000000..ddb1e8a --- /dev/null +++ b/alv-lib/midi/core.moon @@ -0,0 +1,122 @@ +import ValueStream, IOStream, Op, Input, Error, val from require 'alv.base' +import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi' + +bit = do + ok, bit = pcall require, 'bit32' + if ok then bit else require 'bit' +import band, bor, lshift, rshift from bit + +MIDI = { + [0x9]: 'note-on' + [0x8]: 'note-off' + + [0xa]: 'after-key' + [0xd]: 'after-channel' + + [0xb]: 'control-change' + [0xe]: 'pitch-bend' + [0xc]: 'program-change' +} + +rMIDI = {v,k for k,v in pairs MIDI} + +find_port = (Klass, name) -> + with Klass RtMidi.Api.UNIX_JACK + id = nil + for port=1, \getportcount! + if name == \getportname port + id = port + break + + \openport id + +class MidiPort extends IOStream + new: => super 'midi/port' + + setup: (inp, out) => + @inp = inp and find_port RtMidiIn, inp + @out = out and find_port RtMidiOut, out + + tick: => + return unless @inp + while true + delta, bytes = @inp\getmessage! + break unless delta + + { status, a, b } = bytes + chan = band status, 0xf + status = MIDI[rshift status, 4] + @add { :status, :chan, :a, :b } + + send: (status, chan, a, b) => + assert @out, Error 'type', "#{@} is not an output or bidirectional port" + if 'string' == type 'status' + status = bor (lshift rMIDI[status], 4), chan + @out\sendmessage status, a, b + +class PortOp extends Op + new: (...) => + super ... + @out or= MidiPort! + + tick: (inp, out) => + { :inp, :out } = @unwrap_all! + @out\setup inp, out + +input = ValueStream.meta + meta: + name: 'input' + summary: "Create a MIDI input port." + examples: { '(midi/input name)' } + + value: class extends PortOp + setup: (inputs) => + name = val.str\match inputs + super inp: Input.hot name + +output = ValueStream.meta + meta: + name: 'output' + summary: "Create a MIDI output port." + examples: { '(midi/output name)' } + + value: class extends PortOp + setup: (inputs) => + name = val.str\match inputs + super out: Input.hot name + +inout = ValueStream.meta + meta: + name: 'inout' + summary: "Create a bidirectional MIDI port." + examples: { '(midi/inout name)' } + + value: class extends PortOp + setup: (inputs) => + { inp, out } = (val.str + val.str)\match inputs + super + inp: Input.hot inp + out: Input.hot out + +apply_range = (range, val) -> + if range\type! == 'str' + switch range! + when 'raw' then val + when 'uni' then val / 128 + when 'bip' then val / 64 - 1 + when 'rad' then val / 64 * math.pi + when 'deg' then val / 128 * 360 + else + error Error 'argument', "unknown range '#{range!}'" + elseif range.type == 'num' + val / 128 * range! + else + error Error 'argument', "range has to be a string or number" + +{ + :input + :output + :inout + :apply_range + :bit +} diff --git a/alv-lib/midi/launchctl.moon b/alv-lib/midi/launchctl.moon new file mode 100644 index 0000000..90e8956 --- /dev/null +++ b/alv-lib/midi/launchctl.moon @@ -0,0 +1,194 @@ +import ValueStream, EventStream, Op, Input, val, evt from require 'alv.base' +import apply_range, bit from require 'alv-lib.midi.core' +import bor, lshift from bit + +color = (r, g) -> bit.bor 12, r, (bit.lshift g, 4) + +cc_seq = ValueStream.meta + meta: + name: 'cc-seq' + summary: "MIDI CC-Sequencer." + examples: { '(launchctl/cc-seq [port] i start chan [steps [range]])' } + description: " +returns the value for the i-th step steps (buttons starting from start). +steps defaults to 8. + +range can be one of: +- 'raw' [ 0 - 128[ +- 'uni' [ 0 - 1[ (default) +- 'bip' [-1 - 1[ +- 'rad' [ 0 - tau[ +- 'deg' [ 0 - 360[ +- (num) [ 0 - num[" + + value: class extends Op + num = val.num + pattern = -evt['midi/port'] + num + num + num + -num + -(val.str + num) + setup: (inputs, scope) => + { port, i, start, chan, steps, range } = pattern\match inputs + + super + port: Input.hot port or scope\get '*ctrl*' + i: Input.hot i + start: Input.hot start + chan: Input.hot chan + steps: Input.hot steps or ValueStream.num 8 + range: Input.hot range or ValueStream.str 'uni' + + @state or= {} + @out or= ValueStream 'num', apply_range @inputs.range, 0 + + tick: => + { :port, :i, :start, :chan, :steps, :range } = @inputs + + if steps\dirty! + while steps! > #@state + table.insert @state, 0 + while steps! < #@state + table.remove @state + + curr_i = i! % #@state + if port\dirty! + changed = false + for msg in *port! + if msg.status == 'control-change' and msg.chan == chan! + rel_i = msg.a - start! + if rel_i >= 0 and rel_i < #@state + @state[rel_i+1] = msg.b + changed = rel_i == curr_i + @out\set apply_range range, @state[curr_i+1] if changed + else + @out\set apply_range range, @state[curr_i+1] + +gate_seq = ValueStream.meta + meta: + name: 'gate-seq' + summary: "MIDI Gate-Sequencer." + examples: { '(launchctl/gate-seq [port] i start chan [steps])' } + description: " +Send `true` or `false` for the `i`-th note-button (MIDI-notes starting from +`start`). `steps` defaults to 8." + + value: class extends Op + pattern = -evt['midi/port'] + val.num + val.num + val.num + -val.num + setup: (inputs, scope) => + @out or= ValueStream 'bool' + @state or= {} + { port, i, start, chan, steps } = pattern\match inputs + + super + port: Input.hot port or scope\get '*ctrl*' + i: Input.hot i + start: Input.hot start + chan: Input.hot chan + steps: Input.hot steps or ValueStream.num 8 + + light = (set, active) -> + set = if set then 'S' else ' ' + active = if active then 'A' else ' ' + color switch set .. active + when ' ' then 0, 0 + when ' A' then 1, 1 + when 'S ' then 1, 0 + when 'SA' then 3, 1 + + display: (i, active) => + start, chan = @inputs.start!, @inputs.chan! + @inputs.port.stream\send 'note-on', chan, (start + i), light @state[i+1], active + + tick: => + { :port, :i, :start, :chan, :steps } = @inputs + + if steps\dirty! + while steps! > #@state + table.insert @state, false + while steps! < #@state + table.remove @state + + curr_i = i! % #@state + + if port\dirty! + for msg in *port! + if msg.status == 'note-on' and msg.chan == chan! + rel_i = msg.a - start! + if rel_i >= 0 and rel_i < #@state + @state[rel_i+1] = not @state[rel_i+1] + @display rel_i, rel_i == curr_i + + if i\dirty! + prev_i = (curr_i - 1) % #@state + + @display curr_i, true + @display prev_i, false + + @out\set @state[curr_i+1] + +trig_seq = ValueStream.meta + meta: + name: 'trig-seq' + summary: "MIDI Trigger-Sequencer." + examples: { '(launchctl/trig-seq [port] i start chan [steps])' } + description: " +Send bangs for the `i`-th note-button (MIDI-notes starting from `start`). +`steps` defaults to 8." + + value: class extends Op + pattern = -evt['midi/port'] + val.num + val.num + val.num + -val.num + setup: (inputs, scope) => + @out or= EventStream 'bang' + @state or= {} + { port, i, start, chan, steps } = pattern\match inputs + + super + port: Input.hot port or scope\get '*ctrl*' + i: Input.hot i + start: Input.hot start + chan: Input.hot chan + steps: Input.hot steps or ValueStream.num 8 + + light = (set, active) -> + set = if set then 'S' else ' ' + active = if active then 'A' else ' ' + color switch set .. active + when ' ' then 0, 0 + when ' A' then 1, 1 + when 'S ' then 1, 0 + when 'SA' then 3, 1 + + display: (i, active) => + start, chan = @inputs.start!, @inputs.chan! + @inputs.port.stream\send 'note-on', chan, (start + i), light @state[i+1], active + + tick: => + { :port, :i, :start, :chan, :steps } = @inputs + + if steps\dirty! + while steps! > #@state + table.insert @state, false + while steps! < #@state + table.remove @state + + curr_i = i! % #@state + + if port\dirty! + for msg in *port! + if msg.status == 'note-on' and msg.chan == chan! + rel_i = msg.a - start! + if rel_i >= 0 and rel_i < #@state + @state[rel_i+1] = not @state[rel_i+1] + @display rel_i, rel_i == curr_i + + if i\dirty! + prev_i = (curr_i - 1) % #@state + + @display curr_i, true + @display prev_i, false + + if @state[curr_i+1] + @out\add true + +{ + 'cc-seq': cc_seq + 'gate-seq': gate_seq + 'trig-seq': trig_seq +} diff --git a/alv-lib/osc.moon b/alv-lib/osc.moon new file mode 100644 index 0000000..b7e25e7 --- /dev/null +++ b/alv-lib/osc.moon @@ -0,0 +1,87 @@ +import Op, ValueStream, Input, val, evt from require 'alv.base' +import pack from require 'osc' +import dns, udp from require 'socket' + +unpack or= table.unpack + +connect = ValueStream.meta + meta: + name: 'connect' + summary: "Create a UDP remote." + examples: { '(osc/connect host port)' } + + value: class extends Op + pattern = val.str + val.num + setup: (inputs) => + @out or= ValueStream 'udp/socket' + { host, port } = pattern\match inputs + super + host: Input.hot host + port: Input.hot port + + tick: => + { :host, :port } = @unwrap_all! + ip = dns.toip host + + @out\set with sock = udp! + \setpeername ip, port + +send = ValueStream.meta + meta: + name: 'send' + summary: "Send events via OSC." + examples: { '(osc/send [socket] path evt)' } + description: "Sends an OSC message with `evt` as an argument. + +- `socket` should be a `udp/socket` value. This argument can be omitted and the + value be passed as a dynamic definition in `*sock*` instead. +- `path` is the OSC path to send the message to. It should be a string-value. +- `evt` is the argument to send. It should be an event stream." + value: class extends Op + pattern = -val['udp/socket'] + val.str + evt! + setup: (inputs, scope) => + { socket, path, value } = pattern\match inputs + super + socket: Input.cold socket or scope\get '*sock*' + path: Input.cold path + value: Input.hot value + + tick: => + { :socket, :path, :value } = @unwrap_all! + for val in *value + msg = pack path, if 'table' == type val then unpack val else val + socket\send msg + +sync = ValueStream.meta + meta: + name: 'sync' + summary: "Synchronize a value via OSC." + examples: { '(osc/sync [socket] path val)' } + description: "sends a message whenever any parameter is dirty." + description: "Sends an OSC message with `val` as an argument whenever any +of the arguments change. + +- `socket` should be a `udp/socket` value. This argument can be omitted and the + value be passed as a dynamic definition in `*sock*` instead. +- `path` is the OSC path to send the message to. It should be a string-value. +- `val` is the value to Synchronize. It should be a value stream." + + value: class extends Op + pattern = -val['udp/socket'] + val.str + val! + setup: (inputs, scope) => + { socket, path, value } = pattern\match inputs + super + socket: Input.hot socket or scope\get '*sock*' + path: Input.hot path + value: Input.hot value + + tick: => + { :socket, :path, :value } = @unwrap_all! + msg = pack path, value + socket\send msg + +{ + :connect + :send + :sync +} diff --git a/alv-lib/pilot.moon b/alv-lib/pilot.moon new file mode 100644 index 0000000..0d6eefb --- /dev/null +++ b/alv-lib/pilot.moon @@ -0,0 +1,90 @@ +import Op, ValueStream, Input, val, evt from require 'alv.base' +import udp from require 'socket' + +local conn + +hex = "0123456789abcdef" +encode = (arg) -> + switch type arg + when 'number' then + i = 1 + math.floor arg + hex\sub i, i + when 'string' then arg + else error "invalid type: #{type arg}" + +send = (...) -> + str = '' + for i = 1, select '#', ... + tbl = select i, ... + str ..= table.concat [encode v for v in *tbl] + conn or= udp! + conn\sendto str, '127.0.0.1', 49161 + +arg = val.num / val.str + +play = ValueStream.meta + meta: + name: 'play' + summary: "Play a note when a bang arrives." + examples: { '(pilot/play trig ch oct note [vel [len]])' } + + value: class extends Op + pattern = evt.bang + arg^5 + setup: (inputs) => + { trig, args } = pattern\match inputs + super + trig: Input.hot trig + args: [Input.cold a for a in *args] + + tick: => + { :trig, :args } = @inputs + for _ in *trig! + send [a! for a in *@inputs.args] + +play_ = ValueStream.meta + meta: + name: 'play!' + summary: "Play a note when a note arrives." + examples: { '(pilot/play! ch oct note [vel [len]])' } + + value: class extends Op + pattern = arg + arg + (evt.num / evt.str) + arg^2 + setup: (inputs) => + { chan, octv, note, args } = pattern\match inputs + super + chan: Input.cold chan + octv: Input.cold octv + note: Input.hot note + args: [Input.cold a for a in *args] + + tick: => + { :chan, :octv, :note, :args } = @inputs + args = for a in *args do a! + for note in *note! + send { chan!, octv!, note }, args + +effect = ValueStream.meta + meta: + name: 'effect' + summary: "Set effect parameters." + examples: { '(pilot/effect which a b)' } + description: "`effect` should be one of 'DIS', 'CHO', 'REV' or 'FEE'" + + value: class extends Op + pattern = val.str + arg + arg + setup: (inputs) => + { which, a, b } = pattern\match inputs + super { + Input.hot which + Input.hot a + Input.hot b + } + + tick: => + send @unwrap_all! + +{ + :play + 'play!': play_ + :effect +} diff --git a/alv-lib/random.moon b/alv-lib/random.moon new file mode 100644 index 0000000..8c4d096 --- /dev/null +++ b/alv-lib/random.moon @@ -0,0 +1,85 @@ +import ValueStream, Error, Op, Input, val, evt from require 'alv.base' + +apply_range = (range, val) -> + if range\type! == 'str' + switch range! + when 'uni' then val + when 'bip' then val*2 - 1 + when 'rad' then val*2 * math.pi + when 'deg' then val * 360 + else + error Error 'argument', "unknown range '#{range!}'" + elseif range\type! == 'num' + val * range! + else + error Error 'argument', "range has to be a string or number" + +range_doc = " +range can be one of: +- 'uni' [ 0 - 1[ (default) +- 'bip' [-1 - 1[ +- 'rad' [ 0 - tau[ +- 'deg' [ 0 - 360[ +- (num) [ 0 - num[" + +pattern = -evt.bang + -(val.num / val.str) + +num = ValueStream.meta + meta: + name: 'num' + summary: 'Generate a random number.' + examples: { '(random/num [trigger] [range]))' } + description: "Generate a random value in `range` when created and on `trig`. +#{range_doc}" + + value: class extends Op + new: (...) => + super ... + @out or= ValueStream 'num' + @state or @gen! + + gen: => @state = math.random! + + setup: (inputs) => + { trig, range } = pattern\match inputs + super + trig: trig and Input.hot trig + range: Input.hot range or ValueStream.str 'uni' + + tick: => + @gen! if @inputs.trig and @inputs.trig\dirty! + @out\set apply_range @inputs.range, @state + +vec = (n) -> + ValueStream.meta + meta: + name: "vec#{n}" + summary: 'Generate a random vector.' + examples: { '(random/vec#{n} [trigger] [range]))' } + description: "Generate a random vec#{n} in `range` when created and on `trig`. +#{range_doc}" + + value: class extends Op + new: (...) => + super ... + @out or= ValueStream "vec#{n}" + @state or @gen! + + gen: => @state = for i=1,n do math.random! + + setup: (inputs) => + { trig, range } = pattern\match inputs + super + trig: trig and Input.hot trig + range: Input.hot range or ValueStream.str 'uni' + + tick: => + @gen! if @inputs.trig and @inputs.trig\dirty! + @out\set [apply_range @inputs.range, v for v in *@state] + +{ + :num + vec2: vec 2 + vec3: vec 3 + vec4: vec 4 +} diff --git a/alv-lib/sc.moon b/alv-lib/sc.moon new file mode 100644 index 0000000..2857392 --- /dev/null +++ b/alv-lib/sc.moon @@ -0,0 +1,83 @@ +import Op, ValueStream, Input, val, evt from require 'alv.base' +import pack from require 'osc' +import dns, udp from require 'socket' + +unpack or= table.unpack + +play = ValueStream.meta + meta: + name: 'play' + summary: 'Play a SuperCollider SynthDef on bangs.' + examples: { '(play [socket] synth trig [param val…])' } + description: " +Plays the synth `synth` on the `udp/socket` `socket` whenever `trig` is live. + +- `socket` should be a `udp/socket` value. This argument can be omitted and the + value be passed as a dynamic definition in `*sock*` instead. +- `synth` is the SC synthdef name. It should be a string-value. +- `trig` is the trigger signal. It should be a stream of bang-events. +- `param` is the name of a synthdef parameter. It should be a string-value." + value: class extends Op + pattern = -val['udp/socket'] + val.str + evt.bang + (val.str + val.num)\rep 0 + setup: (inputs, scope) => + { socket, synth, trig, ctrls } = pattern\match inputs + + flat_ctrls = {} + for { key, value } in *ctrls + table.insert flat_ctrls, key + table.insert flat_ctrls, value + + super + trig: Input.hot trig + socket: Input.cold socket or scope\get '*sock*' + synth: Input.cold synth + ctrls: [Input.cold v for v in *flat_ctrls] + + tick: => + if @inputs.trig\dirty! and @inputs.trig! + { :socket, :synth, :ctrls } = @unwrap_all! + msg = pack '/s_new', synth, -1, 0, 1, unpack ctrls + socket\send msg + +play_ = ValueStream.meta + meta: + name: 'play!' + summary: 'Play a SuperCollider SynthDef on events.' + examples: { '(play [socket] synth [param evt/val…])' } + description: " +Plays the synth `synth` on the `udp/socket` `socket` whenever any `evt` is live. + +- `socket` should be a `udp/socket` value. This argument can be omitted and the + value be passed as a dynamic definition in `*sock*` instead. +- `synth` is the SC synthdef name. It should be a string-value. +- `param` is the name of a synthdef parameter. It should be a string-value. +- `val` and `evt` are the parameter values to send. They should be number + streams. Incoming events will cause a note to be played, while value changes + will not." + value: class extends Op + pattern = -val['udp/socket'] + val.str + (val.str + (val.num / evt.num))\rep 0 + setup: (inputs, scope) => + { socket, synth, trig, ctrls } = pattern\match inputs + + flat = {} + for { key, value } in *ctrls + table.insert flat, Input.cold key + table.insert flat, if value\metatype! == 'event' + Input.hot value + else + Input.cold value + + super + socket: Input.cold socket or scope\get '*sock*' + synth: Input.cold synth + ctrls: flat + + tick: => + { :socket, :synth, :ctrls } = @unwrap_all! + msg = pack '/s_new', synth, -1, 0, 1, unpack ctrls + socket\send msg + +{ + :play + 'play!': play_ +} diff --git a/alv-lib/string.moon b/alv-lib/string.moon new file mode 100644 index 0000000..faa7ab3 --- /dev/null +++ b/alv-lib/string.moon @@ -0,0 +1,18 @@ +import Op, ValueStream, Input from require 'alv.base' + +str = ValueStream.meta + meta: + name: 'str' + summary: "Concatenate/stringify values." + examples: { '(.. v1 [v2…])', '(str v1 [v2…])' } + value: class extends Op + setup: (inputs) => + @out or= ValueStream 'string' + super [Input.hot v for v in *inputs] + + tick: => + @out\set table.concat [tostring v! for v in *@inputs] + +{ + :str, '..': str +} diff --git a/alv-lib/time.moon b/alv-lib/time.moon new file mode 100644 index 0000000..2589d96 --- /dev/null +++ b/alv-lib/time.moon @@ -0,0 +1,285 @@ +import + ValueStream, EventStream, IOStream, + Error, Op, Input, val, evt +from require 'alv.base' +import monotime from require 'system' + +class Clock extends IOStream + new: (@frametime) => + super 'clock' + + return unless monotime + @last = monotime! + @dt = 0 + @is_dirty = false + + tick: => + time = monotime! + @dt = time - @last + if @dt >= @frametime + @add { dt: @dt, :time } + @last = time + +class ScaledClock extends EventStream + set: (val) => @value + unwrap: => @value + +clock = ValueStream.meta + meta: + name: 'clock' + summary: "Create a clock source." + examples: { '(clock)', '(clock fps)' } + description: "Creates a new clock event stream. + +The clock event stream is an IO that triggers other operators at a fixed +frame rate. + +- `fps` has to be an eval-time constant and defaults to `60` if omitted." + value: class extends Op + new: (...) => + super ... + @out or= Clock! + + setup: (inputs) => + fps = (-val.num)\match inputs + super fps: Input.hot fps or ValueStream.num 60 + @out.frametime = 1 / @inputs.fps! + + tick: => + @out.frametime = 1 / @inputs.fps! + +scale_time = ValueStream.meta + meta: + name: 'scale-time' + summary: "Scale clock time." + examples: { '(scale-time [clock] scale)' } + description: "Creates a new clock event stream scaled by `scale`. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `scale` should be a num-value." + value: class extends Op + new: (...) => + super ... + @out or= EventStream 'clock' + + pattern = -evt.clock + val.num + -val.str + setup: (inputs, scope) => + { clock, scale } = pattern\match inputs + super + clock: Input.hot clock or scope\get '*clock*' + scale: Input.cold scale + + tick: => + scale = @inputs.scale! + for evt in *@inputs.clock! + @out\add {k, v*scale for k,v in pairs evt} + +lfo = ValueStream.meta + meta: + name: 'lfo' + summary: "Low-frequency oscillator." + examples: { '(lfo [clock] freq [wave])' } + description: "Oscillates betwen `0` and `1` at the frequency `freq`. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `freq` should be a num-value. +- `wave` selects the wave shape from one of the following: + - `'sin'` (the default) + - `'saw'` + - `'tri'`" + value: class extends Op + new: (...) => + super ... + @state or= 0 + @out or= ValueStream 'num' + + default_wave = ValueStream.str 'sin' + pattern = -evt.clock + val.num + -val.str + setup: (inputs, scope) => + { clock, freq, wave } = pattern\match inputs + super + clock: Input.hot clock or scope\get '*clock*' + freq: Input.cold freq + wave: Input.hot wave or default_wave + + tau = math.pi * 2 + tick: => + for tick in *@inputs.clock! + @state += tick.dt * @inputs.freq! + + @out\set switch @inputs.wave! + when 'sin' then .5 + .5 * math.cos @state * tau + when 'saw' then @state % 1 + when 'tri' then math.abs (2*@state % 2) - 1 + else error Error 'argument', "unknown wave type '#{wave}'" + +ramp = ValueStream.meta + meta: + name: 'ramp' + summary: "Sawtooth LFO." + examples: { '(ramp [clock] period [max])' } + description: "Ramps from `0` to `max` once every `period` seconds. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `period` should be a num-value. +- `max` should be a num-value and defaults to `period` if omitted." + value: class extends Op + new: (...) => + super ... + @state or= 0 + @out or= ValueStream 'num' + + pattern = -evt.clock + val.num + -val.num + setup: (inputs, scope) => + { clock, period, max } = pattern\match inputs + super + clock: Input.hot clock or scope\get '*clock*' + period: Input.cold period + max: max and Input.cold max + + tick: => + for tick in *@inputs.clock! + period = @inputs.period! + max = (@inputs.max or @inputs.period)! + @phase += tick.dt / period + + while @phase >= 1 + @phase -= 1 + + @out\set @phase * max + +tick = ValueStream.meta + meta: + name: 'tick' + summary: "Count ticks." + examples: { '(tick [clock] period)' } + description: "Counts upwards by one every `period` seconds. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `period` should be a num-value. +- returns a `num` value that increases by 1 every `period`." + value: class extends Op + new: (...) => + super ... + @state or= { phase: 0, count: 0 } + @out or= ValueStream 'num', @state.count + + pattern = -evt.clock + val.num + setup: (inputs, scope) => + { clock, period } = pattern\match inputs + super + clock: Input.hot clock or scope\get '*clock*' + period: Input.cold period + + tick: => + for tick in *@inputs.clock! + @state.phase += tick.dt / @inputs.period! + + while @state.phase >= 1 + @state.phase -= 1 + @state.count += 1 + @out\set @state.count + +every = ValueStream.meta + meta: + name: 'every' + summary: "Emit events regularly." + examples: { '(every [clock] period [evt])' } + description: "Emits `evt` as an event once every `period` seconds. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `period` should be a num-value. +- `evt` can be a value of any type. It defaults to `bang`. +- the return type will be an event stream with the same type as `evt`." + value: class extends Op + new: (...) => + super ... + @state or= 0 + + pattern = -evt.clock + val.num + -val! + setup: (inputs, scope) => + { clock, period, evt } = pattern\match inputs + super + clock: Input.hot clock or scope\get '*clock*' + period: Input.cold period + evt: Input.cold evt or ValueStream 'bang', true + @out = EventStream @inputs.evt\type! + + tick: => + for tick in *@inputs.clock! + @state += tick.dt / @inputs.period! + + while @state >= 1 + @state -= 1 + @out\add @inputs.evt! + +sequence = ValueStream.meta + meta: + name: 'sequence' + summary: "Emit a sequence of values as events over time." + examples: { '(sequence [clock] delay0 evt1 delay1 evt2 delay2…)' } + description: " +Emits `evt1`, `evt2`, … as events with delays `delay0`, `delay1`, … in between. + +- `clock` should be a `time/clock` event stream. This argument can be omitted + and the stream be passed as a dynamic definition in `*clock*` instead. +- `delay0`, `delay1`, … must be num-values. +- `evt1`, `evt2`, … must be values of the same type. +- the return type will be an event stream with the same type as the `evt`s." + value: class extends Op + new: (...) => + super ... + @state or= { i: 1, t: 0 } + + pair = (val! + val.num)\named('value', 'delay') + pattern = -evt.clock + val.num + pair*0 + + inputify = (step) -> + { + delay: Input.cold step.delay + value: if step.value then Input.hot step.value + } + + setup: (inputs, scope) => + { clock, first, steps } = pattern\match inputs + @out = EventStream steps[1].value\type! + table.insert steps, 1, { delay: first } + super + clock: Input.hot clock or scope\get '*clock*' + steps: [inputify step for step in *steps] + + tick: => + for tick in *@inputs.clock! + @state.t += tick.dt + + change, current = false, nil + while true + current = @inputs.steps[@state.i] + if @state.t >= current.delay! + @state.t -= current.delay! + @state.i = 1 + (@state.i % #@inputs.steps) + change = true + else + break + + if current.value and (change or current.value\dirty!) + @out\add current.value! + +{ + :clock + 'scale-time': scale_time + :lfo + :ramp + :tick + :every + :sequence + '*clock*': with Clock 1/60 + .meta = + name: '*clock*' + summary: 'Default clock source (60fps).' +} diff --git a/alv-lib/util.moon b/alv-lib/util.moon new file mode 100644 index 0000000..dc133e4 --- /dev/null +++ b/alv-lib/util.moon @@ -0,0 +1,112 @@ +import Op, ValueStream, EventStream, Input, val, evt from require 'alv.base' + +all_same = (list) -> + for v in *list[2,] + if v != list[1] + return false + + list[1] + +switch_ = ValueStream.meta + meta: + name: 'switch' + summary: "Switch between multiple inputs." + examples: { '(switch i v0 [v1 v2…])' } + description: " +- when `i` is `true`, the first value is reproduced. +- when `i` is `false`, the second value is reproduced. +- when `i` is a `num`, it is [math/floor][]ed and the matching argument + (indexed starting from 0) is reproduced." + + value: class extends Op + val_or_evt = (val! / evt!)! + pattern = (val.num / val.bool) + val_or_evt*0 + setup: (inputs) => + { i, values } = pattern\match inputs + + @state = values[1].value.__class == ValueStream + + @out = if @state + ValueStream values[1]\type! + else + EventStream values[1]\type! + + super + i: Input.hot i + values: [Input.hot v for v in *values] + + tick: => + { :i, :values } = @inputs + active = switch i! + when true + values[1] + when false + values[2] + else + i = 1 + (math.floor i!) % #values + values[i] + if @state + @out\set active and active! + else + if active and active\dirty! + for event in *active! + @out\add event + +edge = ValueStream.meta + meta: + name: 'edge' + summary: "Convert rising edges to bangs." + examples: { '(edge bool)' } + + value: class extends Op + setup: (inputs) => + @out or= EventStream 'bang' + value = val.bool\match inputs + super value: Input.hot value + + tick: => + now = @inputs.value! + if now and not @state.last + @out\set true + @state.last = now + +change = ValueStream.meta + meta: + name: 'change' + summary: "Convert value changes to events." + examples: { '(change val)' } + + value: class extends Op + setup: (inputs) => + value = val!\match inputs + @out or= EventStream value\type! + super value: Input.hot value + + tick: => + now = @inputs.value! + if now != @state + @out\add @inputs.value! + @state = now + +hold = ValueStream.meta + meta: + name: 'hold' + summary: "Convert events to value changes." + examples: { '(hold evt)' } + + value: class extends Op + setup: (inputs) => + event = evt!\match inputs + @out or= ValueStream event\type! + super event: Input.hot event + + tick: => + for val in *@inputs.event! + @out\set val + +{ + 'switch': switch_ + :edge + :change + :hold +} |
