aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-04 03:01:13 +0300
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-05 23:39:23 +0300
commit4fd919c5493d7e5100ed0ed944048c6ef8b59f50 (patch)
tree7457876aff69ddea14598bf68ed38822020cd879
parent03ca9c8010702337a06c8b0ca9ca1e90c301d47e (diff)
downloadapplause2-4fd919c5493d7e5100ed0ed944048c6ef8b59f50.tar.gz
MIDI stuff has been moved into midi.lua
* common definitions are now in midi.h
-rw-r--r--applause.c4
-rw-r--r--applause.lua244
-rw-r--r--midi.h6
-rw-r--r--midi.lua239
4 files changed, 248 insertions, 245 deletions
diff --git a/applause.c b/applause.c
index d859323..f0a901e 100644
--- a/applause.c
+++ b/applause.c
@@ -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"
diff --git a/midi.h b/midi.h
new file mode 100644
index 0000000..092c18a
--- /dev/null
+++ b/midi.h
@@ -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