aboutsummaryrefslogtreecommitdiffhomepage
path: root/midi.lua
blob: ac6283d6150810fb0a7a49aa425dad3baeae76ba (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
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