aboutsummaryrefslogtreecommitdiffstats
path: root/alv-lib/_midi.moon
blob: c456a01eb8631e0a54345c4ae44170a667222f24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import Constant, T, Struct, Op, Input, T, Error, const from require 'alv.base'
import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi'

bit = if _VERSION == 'Lua 5.4'
  {
    band: (a, b) -> a & b
    bor: (a, b) -> a | b
    lshift: (a, b) -> a << b
    rshift: (a, b) -> a >> b
  }
else
  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 InPort
  new: (@name) =>
    @port = find_port RtMidiIn, @name
    @msgs = {}

  poll: =>
    @msgs = while true
      delta, bytes = @port\getmessage!
      break unless delta
      { status, a, b } = bytes
      chan = band status, 0xf
      status = MIDI[rshift status, 4]
      { :status, :chan, :a, :b }

  __tostring: => "[#{@name}]"
  __tojson: => string.format '%q', tostring @

class OutPort
  new: (@name) =>
    @port = find_port RtMidiOut, @name

  send: (status, chan, a, b) =>
    if 'string' == type 'status'
      status = bor (lshift rMIDI[status], 4), chan
    @port\sendmessage status, a, b

  __tostring: => "[#{@name}]"
  __tojson: => string.format '%q', tostring @

class PortOp extends Op
  setup: (inputs) =>
    super inputs
    { :inp, :out } = @unwrap_all!

    if inp and out
      type = Struct in: T['midi/in'], out: T['midi/out']
      @out = type\mk_const { 'in': InPort(inp), out: OutPort(out) }
    elseif inp
      @out = T['midi/in']\mk_const InPort inp
    elseif out
      @out = T['midi/out']\mk_const OutPort out
    else
      error "no port opened"

input = Constant.meta
  meta:
    name: 'input'
    summary: "Create a MIDI input port."
    examples: { '(midi/input name)' }

  value: class extends PortOp
    setup: (inputs) =>
      name = const.str\match inputs
      super inp: Input.hot name

    poll: =>
      @.out!\poll!
      false

output = Constant.meta
  meta:
    name: 'output'
    summary: "Create a MIDI output port."
    examples: { '(midi/output name)' }

  value: class extends PortOp
    setup: (inputs) =>
      name = const.str\match inputs
      super out: Input.hot name

port = Constant.meta
  meta:
    name: 'port'
    summary: "Create a bidirectional MIDI port."
    examples: { '(midi/port name)' }

  value: class extends PortOp
    setup: (inputs) =>
      { inp, out } = (const.str + const.str)\match inputs
      super
        inp: Input.hot inp
        out: Input.hot out

    poll: =>
      @.out!.in\poll!
      false

apply_range = (range, val) ->
  if range\type! == T.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! == T.num
    val / 128 * range!
  else
    error Error 'argument', "range has to be a string or number"

{
  :input
  :output
  :port
  :apply_range
  :bit
}