diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2023-09-04 03:01:13 +0300 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2023-09-05 23:39:23 +0300 |
commit | 4fd919c5493d7e5100ed0ed944048c6ef8b59f50 (patch) | |
tree | 7457876aff69ddea14598bf68ed38822020cd879 | |
parent | 03ca9c8010702337a06c8b0ca9ca1e90c301d47e (diff) | |
download | applause2-4fd919c5493d7e5100ed0ed944048c6ef8b59f50.tar.gz |
MIDI stuff has been moved into midi.lua
* common definitions are now in midi.h
-rw-r--r-- | applause.c | 4 | ||||
-rw-r--r-- | applause.lua | 244 | ||||
-rw-r--r-- | midi.h | 6 | ||||
-rw-r--r-- | midi.lua | 239 |
4 files changed, 248 insertions, 245 deletions
@@ -33,6 +33,8 @@ #include <jack/midiport.h> #include <jack/ringbuffer.h> +#include "midi.h" + #define LUA_MODULE "applause.lua" #define APPLAUSE_HISTORY ".applause_history" @@ -74,8 +76,6 @@ typedef struct applause_midi_port { static applause_midi_port midi_port; -typedef uint32_t applause_midi_sample; - static int svsem_init(size_t value) { diff --git a/applause.lua b/applause.lua index ff4bde5..675748e 100644 --- a/applause.lua +++ b/applause.lua @@ -90,11 +90,6 @@ enum applause_audio_state { enum applause_audio_state applause_push_sample(int output_port_id, double sample_double); -// FIXME: Perhaps a struct would be easier to handle? -typedef uint32_t applause_midi_sample; - -applause_midi_sample applause_pull_midi_sample(void); - // Useful in various situations void free(void *ptr); ]] @@ -296,23 +291,6 @@ function Stream:scale(v1, v2) 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 - function Stream:scan(fnc) return ScanStream:new(self, fnc) end @@ -1731,227 +1709,6 @@ function DelayXStream:len() end -- --- MIDI Support --- - -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[self] - if not sample then - sample = C.applause_pull_midi_sample() - sampleCache[self] = 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 - --- 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 - --- -- Primitives -- @@ -2384,4 +2141,5 @@ Client.__gc = Client.kill -- so they react to reload() -- dofile "dssi.lua" +dofile "midi.lua" dofile "evdev.lua" @@ -0,0 +1,6 @@ +/* This header is included from C and LuaJIT. */ + +// FIXME: Perhaps a struct would be easier to handle? +typedef uint32_t applause_midi_sample; + +applause_midi_sample applause_pull_midi_sample(void); 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 |