aboutsummaryrefslogtreecommitdiffstats
path: root/alv-lib/midi
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/midi
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/midi')
-rw-r--r--alv-lib/midi/core.moon122
-rw-r--r--alv-lib/midi/launchctl.moon194
2 files changed, 316 insertions, 0 deletions
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
+}