diff options
-rw-r--r-- | applause.c | 231 | ||||
-rw-r--r-- | applause.lua | 154 |
2 files changed, 108 insertions, 277 deletions
@@ -66,33 +66,14 @@ static sig_atomic_t interrupted = 0; static sig_atomic_t playback = 0; -static jack_port_t *midi_port; +typedef struct applause_midi_port { + jack_port_t *jack_port; + jack_ringbuffer_t *buffer; +} applause_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][128]; -/** The MIDI note triggered last on a channel */ -static int midi_notes_last[16]; +static applause_midi_port midi_port; -/** - * State of all the MIDI controls as updated by - * CC commands. - * Access must be synchronized with `midi_mutex`. - * Perhaps this should use atomic operations instead - * (wasting a few kilobytes). - */ -static uint8_t midi_controls[16][128]; - -/** - * Mutex for synchronizing access to `midi_controls`. - * This MUST have the PTHREAD_PRIO_INHERIT protocol - * since it will be locked from a realtime thread. - */ -static pthread_mutex_t midi_mutex; +typedef uint32_t applause_midi_sample; static int svsem_init(size_t value) @@ -212,45 +193,29 @@ jack_process(jack_nframes_t nframes, void *arg) /* * MIDI processing. - * NOTE: This uses a priority inheriting mutex to - * remain realtime capable. + * FIXME: We could try to preserve the MIDI event timing by filling + * out the stream with 0 samples. */ - midi_in = jack_port_get_buffer(midi_port, nframes); + midi_in = jack_port_get_buffer(midi_port.jack_port, nframes); midi_events = jack_midi_get_event_count(midi_in); for (int i = 0; i < midi_events; i++) { jack_midi_event_t event; - int channel; + applause_midi_sample sample = 0; jack_midi_event_get(&event, midi_in, i); - channel = event.buffer[0] & 0x0F; - - switch (event.buffer[0] & 0xF0) { - case 0x80: /* NOTE OFF */ - 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; - case 0x90: /* NOTE ON */ - pthread_mutex_lock(&midi_mutex); - /* NOTE: Velocity of 0 has the same effect as NOTE OFF */ - midi_notes[channel] - [event.buffer[1]] = event.buffer[2]; - midi_notes_last[channel] = event.buffer[1]; - pthread_mutex_unlock(&midi_mutex); - break; + /* + * We don't know if event.buffer is large enough + * to be dereferenced as an applause_midi_sample pointer, so + * we manually mangle it into an integer based on + * buffer.size. + */ + for (int i = 0; i < event.size; i++) + sample |= event.buffer[i] << (i*8); - case 0xB0: /* Control Change */ - pthread_mutex_lock(&midi_mutex); - midi_controls[channel] - [event.buffer[1]] = event.buffer[2]; - pthread_mutex_unlock(&midi_mutex); - break; - } + jack_ringbuffer_write(midi_port.buffer, + (const char *)&sample, sizeof(sample)); } return 0; @@ -370,26 +335,25 @@ init_audio(int buffer_size) /* * Create MIDI input port */ - midi_port = jack_port_register(client, "midi_input", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - if (midi_port == NULL) { + midi_port.jack_port = jack_port_register(client, "midi_input", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + if (midi_port.jack_port == NULL) { fprintf(stderr, "no more JACK ports available\n"); return 1; } - memset(&midi_controls, 0, sizeof(midi_controls)); - memset(&midi_notes, 0, sizeof(midi_notes)); - memset(&midi_notes_last, 0, sizeof(midi_notes_last)); - - pthread_mutexattr_t prioinherit; - if (pthread_mutexattr_setprotocol(&prioinherit, PTHREAD_PRIO_INHERIT)) { - fprintf(stderr, "Error initializing MIDI mutex with priority inheritance!\n"); + /* + * Make sure that the MIDI buffer is large enough to hold the maximum + * number of MIDI samples delivered in jack_process(). + */ + midi_port.buffer = jack_ringbuffer_create(sizeof(applause_midi_sample)* + jack_get_buffer_size(client)); + if (!midi_port.buffer) { + fprintf(stderr, "cannot create ringbuffer\n"); return 1; } - pthread_mutex_init(&midi_mutex, &prioinherit); - /* Tell the JACK server that we are ready to roll. Our * process() callback will start running now. */ @@ -426,120 +390,6 @@ init_audio(int buffer_size) return 0; } -/** - * Get the MIDI velocity of the `note` last - * triggered on `channel` due to a NOTE ON message. - * This function is meant to be called using LuaJIT's - * FFI interface. - */ -int -applause_midi_velocity_getvalue(int note, int channel) -{ - int value; - - /* - * It's enough to assert() here since this - * function should only be called by the - * MIDIVelocityStream generator. - */ - assert(0 <= note && note <= 127); - assert(1 <= channel && channel <= 16); - - /* The NOTE arrays are 0-based */ - channel--; - - 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); - - return value; -} - -/** - * Get the MIDI note and velocity of the note last - * triggered on `channel`. - * This function is meant to be called using LuaJIT's - * FFI interface. - * - * @return A MIDI note number (least significant byte) and - * velocity (second least significant byte). - */ -int -applause_midi_note_getvalue(int channel) -{ - int value; - - /* - * It's enough to assert() here since this - * function should only be called by the - * MIDINoteStream generator. - */ - assert(1 <= channel && channel <= 16); - - /* The NOTE arrays are 0-based */ - channel--; - - 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); - - return value; -} - -/** - * Get the last value of the MIDI control `control` - * on `channel`. - * This function is meant to be called using LuaJIT's - * FFI interface. - */ -int -applause_midi_cc_getvalue(int control, int channel) -{ - int value; - - /* - * It's enough to assert() here since this - * function should only be called by the - * MIDICCStream generator. - */ - assert(0 <= control && control <= 127); - assert(1 <= channel && channel <= 16); - - /* The NOTE arrays are 0-based */ - channel--; - - 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_controls[channel][control]; - pthread_mutex_unlock(&midi_mutex); - - return value; -} - enum applause_audio_state { APPLAUSE_AUDIO_OK = 0, APPLAUSE_AUDIO_INTERRUPTED, @@ -604,6 +454,23 @@ applause_push_sample(int output_port_id, double sample_double) return APPLAUSE_AUDIO_OK; } +/** + * Pull one MIDI sample from the ring buffer. + * + * This can be called from MIDIStream's tick function. + * + * @return A MIDI event encoded into a MIDI sample, or 0. + */ +applause_midi_sample +applause_pull_midi_sample(void) +{ + applause_midi_sample sample = 0; + + jack_ringbuffer_read(midi_port.buffer, (char *)&sample, sizeof(sample)); + + return sample; +} + static int l_Stream_fork(lua_State *L) { diff --git a/applause.lua b/applause.lua index 91507fa..1b1701a 100644 --- a/applause.lua +++ b/applause.lua @@ -83,9 +83,9 @@ enum applause_audio_state { enum applause_audio_state applause_push_sample(int output_port_id, double sample_double); -int applause_midi_velocity_getvalue(int note, int channel); -int applause_midi_note_getvalue(int channel); -int applause_midi_cc_getvalue(int control, int channel); +typedef uint32_t applause_midi_sample; + +applause_midi_sample applause_pull_midi_sample(void); ]] -- Sample rate @@ -1642,113 +1642,58 @@ end -- -- MIDI Support +-- NOTE: The MIDIStream is defined at the very end, since +-- we need to use primitives not yet defined -- --- Velocity of NOTE ON for a specific note on a channel -MIDIVelocityStream = DeriveClass(Stream) +-- Last value of a specific control channel +function Stream:CC(control, channel) + channel = channel or 0 -function MIDIVelocityStream:ctor(note, channel) - -- `note` may be a note name like "A4" - self.note = type(note) == "string" and ntom(note) or note - assert(0 <= self.note and self.note <= 127, - "MIDI note out of range (0 <= x <= 127)") + 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 - self.channel = channel or 1 - assert(1 <= self.channel and self.channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") + return self:map(function(sample) + value = band(sample, 0xFFFF) == filter and + rshift(sample, 16) or value + return value + end) end --- This is for calling from external code (e.g. from --- streams supporting MIDI natively) -function MIDIVelocityStream.getValue(note, channel) +-- 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 - -- NOTE: The native function assert() for invalid - -- notes or channels to avoid segfaults + channel = channel or 0 + assert(0 <= note and note <= 127, "MIDI note out of range (0 <= x <= 127)") - assert(1 <= channel and channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") - - return C.applause_midi_velocity_getvalue(note, channel) -end - -function MIDIVelocityStream:gtick() - local note = self.note - local channel = self.channel + assert(0 <= channel and channel <= 15, + "MIDI channel out of range (0 <= x <= 15)") - return function() - return C.applause_midi_velocity_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 MIDINoteStream:ctor(channel) - self.channel = channel or 1 - assert(1 <= self.channel and self.channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") -end - --- This is for calling from external code (e.g. from --- streams supporting MIDI natively) -function MIDINoteStream.getValue(channel) - -- NOTE: The native function assert() for invalid - -- notes or channels to avoid segfaults - assert(1 <= channel and channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") - - return C.applause_midi_note_getvalue(channel) -end - -function MIDINoteStream:gtick() - local channel = self.channel - - return function() - return C.applause_midi_note_getvalue(channel) - end -end - -MIDICCStream = DeriveClass(Stream) - -function MIDICCStream:ctor(control, channel) - self.control = control - self.channel = channel or 1 - - assert(0 <= self.control and self.control <= 127, - "MIDI control number out of range (0 <= x <= 127)") - assert(1 <= self.channel and self.channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") -end - --- This is for calling from external code (e.g. from --- streams supporting MIDI natively) -function MIDICCStream.getValue(control, channel) - -- NOTE: The native function assert() for invalid - -- notes or channels to avoid segfaults - assert(0 <= control and control <= 127, - "MIDI control number out of range (0 <= x <= 127)") - assert(1 <= channel and channel <= 16, - "MIDI channel out of range (1 <= x <= 16)") - - return C.applause_midi_cc_getvalue(control, channel) -end - -function MIDICCStream:gtick() - local control = self.control - local channel = self.channel + 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 function() - return C.applause_midi_cc_getvalue(control, channel) - end + 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 @@ -2334,3 +2279,22 @@ Client.__gc = Client.kill -- so they react to reload() -- dofile "dssi.lua" + +-- +-- See above, MIDIStream depends on tostream() and other +-- primitives. +-- +do + local class = DeriveClass(Stream) + + function class:gtick() + return C.applause_pull_midi_sample + end + + -- FIXME: Since a sample must only be pulled once + -- per tick, so MIDIStream can be reused, it must + -- always be cached. + -- Perhaps it's easier to peek into the ring buffer + -- advance the read pointer per tick. + MIDIStream = class:cache() +end |