aboutsummaryrefslogtreecommitdiffstats
path: root/alv-lib
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-04-14 09:21:00 +0000
committers-ol <s-ol@users.noreply.github.com>2020-04-14 09:21:00 +0000
commit5faab5d81cc5ed36824a52a273e83eb20240cb2e (patch)
treedba10fadf4cdad0f5524df642fab83f6310a8574 /alv-lib
parentmove spec out of spec/core (diff)
downloadalive-5faab5d81cc5ed36824a52a273e83eb20240cb2e.tar.gz
alive-5faab5d81cc5ed36824a52a273e83eb20240cb2e.zip
move lib to alv-lib module
Diffstat (limited to 'alv-lib')
-rw-r--r--alv-lib/logic.moon153
-rw-r--r--alv-lib/math.moon192
-rw-r--r--alv-lib/midi.moon99
-rw-r--r--alv-lib/midi/core.moon122
-rw-r--r--alv-lib/midi/launchctl.moon194
-rw-r--r--alv-lib/osc.moon87
-rw-r--r--alv-lib/pilot.moon90
-rw-r--r--alv-lib/random.moon85
-rw-r--r--alv-lib/sc.moon83
-rw-r--r--alv-lib/string.moon18
-rw-r--r--alv-lib/time.moon285
-rw-r--r--alv-lib/util.moon112
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
+}