diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2015-12-31 10:17:25 +0100 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2015-12-31 14:09:09 +0100 |
commit | fc9c4746a21226723875cebfbf310065038141bf (patch) | |
tree | 6e255faf3700ff5e7761e039d7e66113d8e2061a | |
parent | f5e8f70a7aaab571e1136e2eb789e3c1ebdc26f6 (diff) | |
download | applause2-fc9c4746a21226723875cebfbf310065038141bf.tar.gz |
implemented basic support for MIDI NOTE ON/OFF events
* MIDIVelocityStream will report the velocities on a particular
note. This can be used to build polyphonic instruments.
* MIDINoteStream will report all possible notes on a particular
channel. The velocity of the note last triggered is also encoded
into the stream values. This can be used to build simple
monophonic instruments.
* Map bit methods to stream methods, e.g. Stream:band()
Is especially useful for streams that encode multiple values
into single integers for performance reasons.
* mtof() and ftom() methods for converting from and to MIDI note
numbers. mtof() is very efficient since its values are all
precalculated in a lookup table.
* fixed tostring() method for streams smaller than 1024 samples
-rw-r--r-- | applause.c | 154 | ||||
-rw-r--r-- | applause.lua | 94 |
2 files changed, 218 insertions, 30 deletions
@@ -44,6 +44,17 @@ static sig_atomic_t buffer_xrun = 0; static sig_atomic_t interrupted = 0; static jack_port_t *midi_port; + +/** + * State of all the MIDI notes as updated by + * NOTE ON/OFF commands. + * The values are the NOTE ON velocities. + * Access must be syncronized with `midi_mutex`. + */ +static uint8_t midi_notes[16][127]; +/** The MIDI note triggered last on a channel */ +static int midi_notes_last[16]; + /** * State of all the MIDI controls as updated by * CC commands. @@ -52,6 +63,7 @@ static jack_port_t *midi_port; * (wasting a few kilobytes). */ static uint8_t midi_controls[16][127]; + /** * Mutex for synchronizing access to `midi_controls`. * This MUST have the PTHREAD_PRIO_INHERIT protocol @@ -177,17 +189,36 @@ jack_process(jack_nframes_t nframes, void *arg) for (int i = 0; i < midi_events; i++) { jack_midi_event_t event; + int channel; jack_midi_event_get(&event, midi_in, i); + channel = event.buffer[0] & 0x0F; + + switch (event.buffer[0] & 0xF0) { + case 0x80: + pthread_mutex_lock(&midi_mutex); + /* NOTE: The NOTE OFF velocity is currently ignored */ + midi_notes[channel] + [event.buffer[1]] = 0; + midi_notes_last[channel] = event.buffer[1]; + pthread_mutex_unlock(&midi_mutex); + break; - if ((event.buffer[0] & 0xF0) != 0xB0) - /* not a control command */ - continue; + case 0x90: + pthread_mutex_lock(&midi_mutex); + midi_notes[channel] + [event.buffer[1]] = event.buffer[2]; + midi_notes_last[channel] = event.buffer[1]; + pthread_mutex_unlock(&midi_mutex); + break; - pthread_mutex_lock(&midi_mutex); - midi_controls[event.buffer[0] & 0x0F] - [event.buffer[1]] = event.buffer[2]; - pthread_mutex_unlock(&midi_mutex); + case 0xB0: + pthread_mutex_lock(&midi_mutex); + midi_controls[channel] + [event.buffer[1]] = event.buffer[2]; + pthread_mutex_unlock(&midi_mutex); + break; + } } return 0; @@ -342,6 +373,69 @@ init_audio(int buffer_size) } static int +l_MIDIVelocityStream_getValue(lua_State *L) +{ + int top = lua_gettop(L); + lua_Integer channel, note, value; + + luaL_argcheck(L, top == 2, top, "Note and channel number expected"); + luaL_checktype(L, 1, LUA_TNUMBER); + luaL_checktype(L, 2, LUA_TNUMBER); + + note = lua_tointeger(L, 1); + luaL_argcheck(L, 0 <= note && note <= 127, note, + "Invalid note number range (0 <= x <= 127)"); + channel = lua_tointeger(L, 2); + luaL_argcheck(L, 0 <= channel && channel <= 15, channel, + "Invalid channel range (0 <= x <= 15)"); + + pthread_mutex_lock(&midi_mutex); + /* + * This thread might be lifted to realtime priority + * since this is a priority inheritance mutex. + * We will block a realtime thread while we're in the + * critical section. + * Therefore it is crucial that the following code + * is realtime-safe. + */ + value = midi_notes[channel][note]; + pthread_mutex_unlock(&midi_mutex); + + lua_pushinteger(L, value); + return 1; +} + +static int +l_MIDINoteStream_getValue(lua_State *L) +{ + int top = lua_gettop(L); + lua_Integer channel, value; + + luaL_argcheck(L, top == 1, top, "Channel number expected"); + luaL_checktype(L, 1, LUA_TNUMBER); + + channel = lua_tointeger(L, 1); + luaL_argcheck(L, 0 <= channel && channel <= 15, channel, + "Invalid channel range (0 <= x <= 15)"); + + pthread_mutex_lock(&midi_mutex); + /* + * This thread might be lifted to realtime priority + * since this is a priority inheritance mutex. + * We will block a realtime thread while we're in the + * critical section. + * Therefore it is crucial that the following code + * is realtime-safe. + */ + value = midi_notes_last[channel] | + (midi_notes[channel][midi_notes_last[channel]] << 8); + pthread_mutex_unlock(&midi_mutex); + + lua_pushinteger(L, value); + return 1; +} + +static int l_MIDICCStream_getValue(lua_State *L) { int top = lua_gettop(L); @@ -459,22 +553,26 @@ l_Stream_play(lua_State *L) return 0; } +typedef struct NativeMethod { + const char *class_name; + const char *method_name; + lua_CFunction func; +} NativeMethod; + +static const NativeMethod native_methods[] = { + {"Stream", "play", l_Stream_play}, + {"MIDIVelocityStream", "getValue", l_MIDIVelocityStream_getValue}, + {"MIDINoteStream", "getValue", l_MIDINoteStream_getValue}, + {"MIDICCStream", "getValue", l_MIDICCStream_getValue}, + {NULL, NULL, NULL} +}; + int main(int argc, char **argv) { int buffer_size = DEFAULT_BUFFER_SIZE; struct sigaction sigint_action; - static const luaL_Reg stream_methods[] = { - {"play", l_Stream_play}, - {NULL, NULL} - }; - - static const luaL_Reg midiccstream_methods[] = { - {"getValue", l_MIDICCStream_getValue}, - {NULL, NULL} - }; - lua_State *L; /* @@ -512,18 +610,18 @@ main(int argc, char **argv) init_audio(buffer_size); /* - * Register C functions in the `Stream` class + * Register native C functions. + * This may be unefficient when registering multiple + * methods in the same class. */ - lua_getglobal(L, "Stream"); - luaL_register(L, NULL, stream_methods); - lua_pop(L, 1); - - /* - * Register C functions in the `MIDICCStream` class - */ - lua_getglobal(L, "MIDICCStream"); - luaL_register(L, NULL, midiccstream_methods); - lua_pop(L, 1); + for (const NativeMethod *method = native_methods; + method->class_name; + method++) { + lua_getglobal(L, method->class_name); + lua_pushcfunction(L, method->func); + lua_setfield(L, -2, method->method_name); + lua_pop(L, 1); /* pop class table */ + } /* * Set global `samplerate` diff --git a/applause.lua b/applause.lua index e5a9b42..a76fb41 100644 --- a/applause.lua +++ b/applause.lua @@ -1,5 +1,9 @@ local sndfile = require "sndfile" local ffi = require "ffi" +local bit = require "bit" + +-- Make table.new() available +require "table.new" -- -- Define C functions for benchmarking (POSIX libc) @@ -123,6 +127,21 @@ for _, fnc in pairs{"abs", "acos", "asin", "atan", end end +function Stream:bnot() + return self:map(bit.bnot) +end + +-- Register all binary operators of the "bit" module +for _, name in pairs{"bor", "band", "bxor", + "lshift", "rshift", "arshift", + "rol", "ror"} do + local fnc = bit[name] + + Stream[name] = function(self, v) + return self:map(function(x) return fnc(x, v) end) + end +end + -- Scalar operations -- In contrast to stream operations (based on ZipStream), -- these work only with scalars and do not @@ -384,7 +403,7 @@ function Stream:__tostring() t = self:sub(1, 1024):totable() table.insert(t, "...") else - t = self() + t = self:totable() end for i = 1, #t do t[i] = tostring(t[i]) end @@ -884,6 +903,50 @@ function NoiseStream:tick() end end +-- Velocity of NOTE ON for a specific note on a channel +MIDIVelocityStream = DeriveClass(Stream, function(self, note, channel) + self.note = note + self.channel = channel or 0 +end) + +-- implemented in applause.c, private! +function MIDIVelocityStream.getValue(note, channel) + error("C function not registered!") +end + +function MIDIVelocityStream:tick() + local note = self.note + local channel = self.channel + local getValue = self.getValue + + return function() + return getValue(note, channel) + end +end + +-- Stream of integer words representing the last MIDI note +-- triggered on a channel with its corresponding velocity +-- (of the NOTE ON message). +-- The MIDI note is the lower byte and the velocity the +-- upper byte of the word. +MIDINoteStream = DeriveClass(Stream, function(self, channel) + self.channel = channel or 0 +end) + +-- implemented in applause.c, private! +function MIDINoteStream.getValue(channel) + error("C function not registered!") +end + +function MIDINoteStream:tick() + local channel = self.channel + local getValue = self.getValue + + return function() + return getValue(channel) + end +end + MIDICCStream = DeriveClass(Stream, function(self, control, channel) self.control = control self.channel = channel or 0 @@ -894,7 +957,6 @@ function MIDICCStream.getValue(control, channel) error("C function not registered!") end --- FIXME: Perhaps implement tick() directly in C? function MIDICCStream:tick() local control = self.control local channel = self.channel @@ -905,6 +967,34 @@ function MIDICCStream:tick() end end +-- MIDI primitives + +-- There are only 120 different 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(120, 0) +for note = 0, 119 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[bit.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 math.floor(12*math.log(freq/440, 2) + 0.5)+69 +end + +function Stream:ftom() return self:map(ftom) end + -- primitives function tostream(v) |