4.1 KiB
Per‑Channel MIDI Looping
Overview
Each looper channel can be either audio or MIDI. Audio channels record and loop audio samples (existing behaviour). MIDI channels record and loop MIDI event sequences, using separate JACK MIDI input/output ports. The state machine (IDLE → RECORD → LOOPING → PAUSED) operates identically for both types.
Commands
| Command | Source | Action |
|---|---|---|
CMD_ADD_MIDI_CHANNEL |
MIDI note 66 | Adds a new MIDI looping channel |
add_midi |
FIFO pipe | Same |
CMD_REMOVE_CHANNEL |
MIDI note 61 | Removes the last‑added channel (audio or MIDI) |
CMD_CYCLE |
any note binding | Toggles channel state (IDLE→RECORD→LOOPING→PAUSED) |
Ports
When a MIDI channel is created, two JACK MIDI ports are registered:
looper:channel<N>_midi_in(input)looper:channel<N>_midi_out(output)
The <N> is a global counter, independent of the index inside the internal channel array.
Recording
During STATE_RECORD:
- All incoming MIDI events on the
_midi_inport are stored in the channel’s event buffer, along with their frame offset relative to the start of the recording. - The incoming events are also forwarded to the
_midi_outport, providing a direct pass‑through during recording.
Buffer limit: A channel can hold up to MAX_MIDI_EVENTS (1024) events.
Looping
During STATE_LOOPING:
- All recorded events are output at the start of every cycle (frame 0). This is a simplification; no per‑event timestamp scheduling is implemented. The loop length is determined by the total number of recorded events.
Pass‑Through
During STATE_IDLE (and STATE_PAUSED for MIDI) incoming MIDI events are copied from _midi_in to _midi_out unchanged.
FIFO Pipe Commands
The FIFO pipe at /tmp/looper_cmd accepts the following new line‑based commands:
| Command | Effect |
|---|---|
add_midi |
Adds a MIDI channel |
stop |
Resets all channels to idle |
bind <ch> |
Binds the next control note to channel <ch> |
unbind |
Resets binding to channel 0 |
Example Workflow
- Start the looper.
- Connect a MIDI keyboard to
looper:channel1_midi_in. - Send MIDI note 66 on
looper:controlto create a MIDI channel. - Send a CYCLE command (e.g., MIDI note 62 under control key) to start recording.
- Play notes on the keyboard – the events are captured.
- Send CYCLE again to enter LOOPING mode – the captured sequence repeats.
- Send CYCLE again to pause, or send STOP (note 65 under control key) to reset.
Implementation Details
- Channel structure (
struct channel_tinchannel.h):typefield (CHANNEL_AUDIOorCHANNEL_MIDI)loopunion containingaudio_buffer[MAX_BUFFER]ormidi_events[MAX_MIDI_EVENTS]
- MIDI event type (
midi_event_t):timestamp(frame offset relative to loop start)status,note,velocity
- Processing (
process_callbackinlooper.c):- The callback checks
typebefore routing to the appropriate handler block. - MIDI handler reads from
midi_inport, writes tomidi_outport.
- The callback checks
- Port cleanup: On channel removal, both MIDI ports are unregistered via
jack_port_unregister()after a one‑RT‑cycle grace period.
Testing
Integration tests in tests/integration.c cover:
test_midi_channel_add– verifies that sendingadd_midivia FIFO createslooper:channel<N>_midi_inports.test_fifo_stop_bind_unbind– verifies thatstop,bind, andunbindFIFO commands are processed correctly.- Other existing tests continue to verify audio‑only functionality.
Run the test suite with:
make test