aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2015-12-31 10:17:25 +0100
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2015-12-31 14:09:09 +0100
commitfc9c4746a21226723875cebfbf310065038141bf (patch)
tree6e255faf3700ff5e7761e039d7e66113d8e2061a
parentf5e8f70a7aaab571e1136e2eb789e3c1ebdc26f6 (diff)
downloadapplause2-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.c154
-rw-r--r--applause.lua94
2 files changed, 218 insertions, 30 deletions
diff --git a/applause.c b/applause.c
index 7a54f08..7160b16 100644
--- a/applause.c
+++ b/applause.c
@@ -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)