aboutsummaryrefslogtreecommitdiffhomepage
path: root/midi.lua
diff options
context:
space:
mode:
Diffstat (limited to 'midi.lua')
-rw-r--r--midi.lua239
1 files changed, 239 insertions, 0 deletions
diff --git a/midi.lua b/midi.lua
new file mode 100644
index 0000000..ac6283d
--- /dev/null
+++ b/midi.lua
@@ -0,0 +1,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