From 4fd919c5493d7e5100ed0ed944048c6ef8b59f50 Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Mon, 4 Sep 2023 03:01:13 +0300 Subject: MIDI stuff has been moved into midi.lua * common definitions are now in midi.h --- midi.lua | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 midi.lua (limited to 'midi.lua') 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 -- cgit v1.2.3