From 42d4109398fd5cf2416ef076af3c2ddea16a5070 Mon Sep 17 00:00:00 2001 From: s-ol Date: Thu, 13 Feb 2020 21:28:13 +0100 Subject: add launchcontrol midi --- lib/midi.moon | 125 ------------------------------------------------ lib/midi/core.moon | 90 ++++++++++++++++++++++++++++++++++ lib/midi/init.moon | 70 +++++++++++++++++++++++++++ lib/midi/launchctl.moon | 66 +++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 125 deletions(-) delete mode 100644 lib/midi.moon create mode 100644 lib/midi/core.moon create mode 100644 lib/midi/init.moon create mode 100644 lib/midi/launchctl.moon diff --git a/lib/midi.moon b/lib/midi.moon deleted file mode 100644 index 5aebc0b..0000000 --- a/lib/midi.moon +++ /dev/null @@ -1,125 +0,0 @@ -import Const, Op from require 'core' -import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi' -import band, rshift from require 'bit32' - -MIDI = { - [0x9]: 'note-on' - [0x8]: 'note-off' - - [0xa]: 'after-key' - [0xd]: 'after-channel' - - [0xb]: 'control-change' - [0xe]: 'pitch-bend' - [0xc]: 'program-change' -} - -class Dispatcher - new: (name) => - @input = RtMidiIn RtMidi.Api.UNIX_JACK - - id = nil - for port=1,@input\getportcount! - if name == @input\getportname port - id = port - break - - @input\openport id - - @listeners = {} - - tick: => - while true - delta, bytes = @input\getmessage! - break unless delta - - { status, a, b } = bytes - chan = band status, 0xf - status = MIDI[rshift status, 4] - @dispatch status, chan, a, b - - dispatch: (status, chan, a, b) => - L\trace "dispatching MIDI event #{status} CH#{chan} #{a} #{b}" - for mask, handler in pairs @listeners - match = true - match and= status == mask.status if mask.status - match and= chan == mask.chan if mask.chan - match and= a == mask.a if mask.a - if match - handler status, chan, a, b - - -- register a handler - -- mask is { :status, :chan, :a } (all keys optional) - attach: (mask, handler) => - @listeners[mask] = handler - - detach: (mask) => - @listeners[mask] = nil - -dispatch = Dispatcher 'system:midi_capture_2' - -class gate extends Op - @doc: "(midi/gate note [chan]) - gate from note-on and note-off messages" - - new: (...) => - super ... - - destroy: => - dispatch\detach @mask if @mask - - setup: (note, chan) => - dispatch\detach @mask if @mask - - note = note\getc 'num' - chan = chan and chan\getc 'num' - @value = false - - @mask = dispatch\attach { :chan, a: note }, (status) -> - if status == 'note-on' - @value = true - else if status == 'note-off' - @value = false - - update: (dt) => dispatch\tick! - -class cc extends Op - @doc: "(midi/cc cc [chan [range]]) - MIDI CC to number - -range can be one of: -- 'raw' [ 0 - 128[ -- 'uni' [ 0 - 1[ (default) -- 'bip' [-1 - 1[ -- 'rad' [ 0 - tau[ -- (num) [ 0 - num[" - - destroy: => - dispatch\detach @mask if @mask - - setup: (cc, chan, @range=Const.str'uni') => - dispatch\detach @mask if @mask - - cc = cc\getc 'num' - chan = chan and chan\getc 'num' - - @mask = dispatch\attach { status: 'control-change', :chan, a: cc }, (_, _, _, val) -> @apply val - - update: (dt) => dispatch\tick! - - apply: (val) => - @value = if @range.type == 'str' - switch @range\get! - when 'raw' then val - when 'uni' then val / 128 - when 'bip' then val / 64 - 1 - when 'rad' then val / 64 * math.pi - else - error "unknown range #{@range}" - elseif @range.type == 'num' - val / 128 * @range\get! - else - error "range has to be a string or number" - -{ - :gate - :cc -} diff --git a/lib/midi/core.moon b/lib/midi/core.moon new file mode 100644 index 0000000..ea49a2c --- /dev/null +++ b/lib/midi/core.moon @@ -0,0 +1,90 @@ +import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi' +import band, bor, lshift, rshift from require 'bit32' + +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} + +class Input + new: (name) => + @input = RtMidiIn RtMidi.Api.UNIX_JACK + + id = nil + for port=1,@input\getportcount! + if name == @input\getportname port + id = port + break + + @input\openport id + + @listeners = {} + + tick: => + while true + delta, bytes = @input\getmessage! + break unless delta + + { status, a, b } = bytes + chan = band status, 0xf + status = MIDI[rshift status, 4] + @dispatch status, chan, a, b + + dispatch: (status, chan, a, b) => + L\trace "dispatching MIDI event #{status} CH#{chan} #{a} #{b}" + for mask, handler in pairs @listeners + match = true + match and= status == mask.status if mask.status + match and= chan == mask.chan if mask.chan + match and= a == mask.a if mask.a + if match + handler status, chan, a, b + + -- register a handler + -- mask is { :status, :chan, :a } (all keys optional) + attach: (mask, handler) => + @listeners[mask] = handler + + detach: (mask) => + @listeners[mask] = nil + +class Output + new: (name) => + @output = RtMidiOut RtMidi.Api.UNIX_JACK + + id = nil + for port=1,@output\getportcount! + if name == @output\getportname port + id = port + break + + @output\openport id + + send: (status, chan, a, b) => + status = bor (lshift rMIDI[status], 4), chan + @output\sendmessage status, a, b + +class InOut + new: (inp, out) => + @inp = Input inp + @out = Output out + + tick: (...) => @inp\tick ... + attach: (...) => @inp\attach ... + detach: (...) => @inp\detach ... + send: (...) => @out\send ... + +{ + :Input + :Output + :InOut +} diff --git a/lib/midi/init.moon b/lib/midi/init.moon new file mode 100644 index 0000000..f522b6c --- /dev/null +++ b/lib/midi/init.moon @@ -0,0 +1,70 @@ +import Const, Op from require 'core' +import Input from require 'lib.midi.core' + +dispatch = Input 'system:midi_capture_6' + +class gate extends Op + @doc: "(midi/gate note [chan]) - gate from note-on and note-off messages" + + new: (...) => + super ... + + destroy: => + dispatch\detach @mask if @mask + + setup: (note, chan) => + dispatch\detach @mask if @mask + + note = note\getc 'num' + chan = chan and chan\getc 'num' + @value = false + + @mask = dispatch\attach { :chan, a: note }, (status) -> + if status == 'note-on' + @value = true + else if status == 'note-off' + @value = false + + update: (dt) => dispatch\tick! + +class cc extends Op + @doc: "(midi/cc cc [chan [range]]) - MIDI CC to number + +range can be one of: +- 'raw' [ 0 - 128[ +- 'uni' [ 0 - 1[ (default) +- 'bip' [-1 - 1[ +- 'rad' [ 0 - tau[ +- (num) [ 0 - num[" + + destroy: => + dispatch\detach @mask if @mask + + setup: (cc, chan, @range=Const.str'uni') => + dispatch\detach @mask if @mask + + cc = cc\getc 'num' + chan = chan and chan\getc 'num' + + @mask = dispatch\attach { status: 'control-change', :chan, a: cc }, (_, _, _, val) -> @apply val + + update: (dt) => dispatch\tick! + + apply: (val) => + @value = if @range.type == 'str' + switch @range\get! + when 'raw' then val + when 'uni' then val / 128 + when 'bip' then val / 64 - 1 + when 'rad' then val / 64 * math.pi + else + error "unknown range #{@range}" + elseif @range.type == 'num' + val / 128 * @range\get! + else + error "range has to be a string or number" + +{ + :gate + :cc +} diff --git a/lib/midi/launchctl.moon b/lib/midi/launchctl.moon new file mode 100644 index 0000000..c7d7b06 --- /dev/null +++ b/lib/midi/launchctl.moon @@ -0,0 +1,66 @@ +import Const, Op from require 'core' +import InOut from require 'lib.midi.core' +import bor, lshift from require 'bit32' + +launch = InOut 'system:midi_capture_6', 'system:midi_playback_6' + +color = (r, g) -> bor 12, r, (lshift g, 4) + +class gate_seq extends Op + @doc: "(launctl/gate-seq i start chan [steps]) - Gate-Sequencer + +returns true or false for the i-th step steps (buttons starting from start)." + + destroy: => + launch\detach @mask if @mask + + new: => + @steps = {} + @value = false + + setup: (@i, start, chan, steps=(Const.num 8)) => + launch\detach @mask if @mask + + @start = start\getc 'num' + @chan = chan\getc 'num' + steps = steps\getc 'num' + + while steps > #@steps + table.insert @steps, false + while steps < #@steps + table.remove @steps + + @mask = launch\attach { status: 'note-on', chan: @chan }, (_, _, note, _) -> @toggle note + + toggle: (note) => + i = note - @start + val = @steps[i+1] + if val != nil + @steps[i+1] = not val + curr_i = (@i\get 'num') % #@steps + launch\send 'note-on', @chan, note, light @steps[i+1], i == curr_i + + 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, 0 + when 'S ' then 0, 1 + when 'SA' then 0, 3 + + update: (dt) => + launch\tick! + + @i\update dt + curr_i = (@i\get 'num') % #@steps + prev_i = (curr_i - 1) % #@steps + + launch\send 'note-on', @chan, (@start + curr_i), light @steps[curr_i+1], true + launch\send 'note-on', @chan, (@start + prev_i), light @steps[prev_i+1], false + + @value = @steps[curr_i+1] + +{ + 'gate-seq': gate_seq +} -- cgit v1.2.3