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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
local bit = require "bit"
local ffi = require "ffi"
local C = ffi.C
cdef_include "midi.h"
MIDIStream = DeriveClass(Stream)
function MIDIStream:gtick()
return function()
-- This is always cached since there is only one MIDI event queue
-- and it must not be pulled more than once per tick.
local sample = sampleCache[MIDIStream]
if not sample then
sample = C.applause_pull_midi_sample()
sampleCache[MIDIStream] = sample
end
return sample
end
end
-- Last value of a specific control channel
function Stream:CC(control, channel)
channel = channel or 0
assert(0 <= control and control <= 127,
"MIDI control number out of range (0 <= x <= 127)")
assert(0 <= channel and channel <= 15,
"MIDI channel out of range (0 <= x <= 15)")
local filter = bit.bor(0xB0, channel, bit.lshift(control, 8))
local value = 0
local band, rshift = bit.band, bit.rshift
return self:map(function(sample)
value = band(sample, 0xFFFF) == filter and
tonumber(rshift(sample, 16)) or value
return value
end)
end
-- same as Stream:scale() but for values between [0, 127]
-- (ie. MIDI CC values)
-- FIXME: If Stream:CC() would output between [-1, 1], there would be no need
-- for Stream:ccscale().
function Stream:ccscale(v1, v2)
local lower = v2 and v1 or 0
local upper = v2 or v1
if type(lower) == "number" and type(upper) == "number" then
return self:map(function(x)
return (x/127)*(upper - lower) + lower
end)
else
return self*((upper - lower)/127) + lower
end
end
-- Velocity of NOTE ON for a specific note on a channel
function Stream:mvelocity(note, channel)
-- `note` may be a note name like "A4"
note = type(note) == "string" and ntom(note) or note
channel = channel or 0
assert(0 <= note and note <= 127,
"MIDI note out of range (0 <= x <= 127)")
assert(0 <= channel and channel <= 15,
"MIDI channel out of range (0 <= x <= 15)")
local on_filter = bit.bor(0x90, channel, bit.lshift(note, 8))
local off_filter = bit.bor(0x80, channel, bit.lshift(note, 8))
local value = 0
local band, rshift = bit.band, bit.rshift
return self:map(function(sample)
value = band(sample, 0xFFFF) == on_filter and
rshift(sample, 16) or
band(sample, 0xFFFF) == off_filter and
0 or value
return value
end)
end
--
-- MIDI primitives
--
do
local band = bit.band
local floor, log = math.floor, math.log
local note_names = {
"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"
}
-- MIDI note number to name
-- NOTE: mton() can handle the words as generated by MIDINoteStream
function mton(note)
note = band(note, 0xFF)
local octave = floor(note / 12)-1
return note_names[(note % 12)+1]..octave
end
function Stream:mton() return self:map(mton) end
local ntom_offsets = {}
for i, name in ipairs(note_names) do
ntom_offsets[name] = i-1
-- Saving the offsets for the lower-cased note names
-- avoids a string.upper() call in ntom()
ntom_offsets[name:lower()] = i-1
end
-- Note name to MIDI note number
function ntom(name)
local octave = name:byte(-1) - 48 + 1
return octave*12 + ntom_offsets[name:sub(1, -2)]
end
function Stream:ntom() return self:map(ntom) end
-- There are only 128 possible MIDI notes,
-- so their frequencies can and should be cached.
-- We do this once instead of on-demand, so the lookup
-- table consists of consecutive numbers.
local mtof_cache = table.new(128, 0)
for note = 0, 127 do
-- MIDI NOTE 69 corresponds to 440 Hz
mtof_cache[note] = 440*math.pow(2, (note - 69)/12)
end
-- Convert from MIDI note to frequency
-- NOTE: mtof() can handle the words as generated by MIDINoteStream
function mtof(note)
return mtof_cache[band(note, 0xFF)]
end
function Stream:mtof() return self:map(mtof) end
-- Convert from frequency to closest MIDI note
function ftom(freq)
-- NOTE: math.log/2 is a LuaJIT extension
return floor(12*log(freq/440, 2) + 0.5)+69
end
function Stream:ftom() return self:map(ftom) end
end
-- Convert from MIDI name to frequency
function ntof(name) return mtof(ntom(name)) end
function Stream:ntof() return self:map(ntof) end
-- Convert from frequency to closest MIDI note name
function fton(freq) return mton(ftom(freq)) end
function Stream:fton() return self:map(fton) end
-- Tick an instrument only when an inputstream (note_stream),
-- gets ~= 0. When it changes back to 0 again, an "off"-stream
-- is triggered. This allows the construction of instruments with
-- Attack-Sustain and Decay phases based on real-time control signals.
-- The note values can be passed into the constructor by using functions
-- for the "on" and "off" streams.
-- Usually, the note stream will be a MIDIStream:mvelocity(), so the two
-- instrument streams can be based on the MIDI velocity (but don't have
-- to be if the velocity is not important).
InstrumentStream = DeriveClass(MuxableStream)
InstrumentStream.sig_last_stream = 1
function InstrumentStream:muxableCtor(note_stream, on_stream, off_stream)
note_stream = tostream(note_stream)
local note_stream_cached
if type(on_stream) == "function" then
note_stream_cached = note_stream:cache()
self.on_stream = on_stream(note_stream_cached)
else
self.on_stream = tostream(on_stream)
end
if type(off_stream) == "function" then
note_stream_cached = note_stream_cached or note_stream:cache()
self.off_stream = off_stream(note_stream_cached)
else
-- The "off" stream is optional
self.off_stream = off_stream and tostream(off_stream)
end
-- `note_stream` is cached only when required
self.note_stream = note_stream_cached or note_stream
end
function InstrumentStream:gtick()
local note_tick = self.note_stream:gtick()
local on_stream = self.on_stream
local on_stream_inf = on_stream:len() == math.huge
local off_stream = self.off_stream
local on_tick
local function off_tick() return 0 end
return function()
local note = note_tick()
if not note then return end
if on_tick == nil then -- no note
if note == 0 then return off_tick() or 0 end
-- FIXME: This is not strictly real-time safe
on_tick = on_stream:gtick()
return on_tick() or 0
else -- note on
if note ~= 0 then
local sample = on_tick()
if sample then return sample end
-- on_stream must be finite, retrigger
on_tick = on_stream:gtick()
return on_tick() or 0
elseif not on_stream_inf then
-- don't cut off finite on_streams
local sample = on_tick()
if sample then return sample end
end
-- FIXME: This is not strictly real-time safe
on_tick = nil
if off_stream then off_tick = off_stream:gtick() end
return off_tick() or 0
end
end
end
function InstrumentStream:len()
return self.note_stream:len()
end
function Stream:instrument(on_stream, off_stream)
return InstrumentStream:new(self, on_stream, off_stream)
end
|