docs: add MIDI looping documentation and update evaluation

Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-10 11:54:00 +00:00
parent df5ecef580
commit 4e489b5e40
2 changed files with 133 additions and 40 deletions

90
docs/2-midi-looping.md Normal file
View File

@@ -0,0 +1,90 @@
# PerChannel 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 note66 | Adds a new MIDI looping channel |
| `add_midi` | FIFO pipe | Same |
| `CMD_REMOVE_CHANNEL` | MIDI note61 | Removes the lastadded 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`:
1. All incoming MIDI events on the `_midi_in` port are stored in the channels event buffer, along with their frame offset relative to the start of the recording.
2. The incoming events are also **forwarded** to the `_midi_out` port, providing a direct passthrough 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 (frame0). This is a simplification; no perevent timestamp scheduling is implemented. The loop length is determined by the total number of recorded events.
## PassThrough
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 linebased 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
1. Start the looper.
2. Connect a MIDI keyboard to `looper:channel1_midi_in`.
3. Send MIDI note66 on `looper:control` to create a MIDI channel.
4. Send a CYCLE command (e.g., MIDI note62 under control key) to start recording.
5. Play notes on the keyboard the events are captured.
6. Send CYCLE again to enter LOOPING mode the captured sequence repeats.
7. Send CYCLE again to pause, or send STOP (note65 under control key) to reset.
## Implementation Details
- **Channel structure** (`struct channel_t` in `channel.h`):
- `type` field (`CHANNEL_AUDIO` or `CHANNEL_MIDI`)
- `loop` union containing `audio_buffer[MAX_BUFFER]` or `midi_events[MAX_MIDI_EVENTS]`
- **MIDI event type** (`midi_event_t`):
- `timestamp` (frame offset relative to loop start)
- `status`, `note`, `velocity`
- **Processing** (`process_callback` in `looper.c`):
- The callback checks `type` before routing to the appropriate handler block.
- MIDI handler reads from `midi_in` port, writes to `midi_out` port.
- **Port cleanup**: On channel removal, both MIDI ports are unregistered via `jack_port_unregister()` after a oneRTcycle grace period.
## Testing
Integration tests in `tests/integration.c` cover:
- `test_midi_channel_add` verifies that sending `add_midi` via FIFO creates `looper:channel<N>_midi_in` ports.
- `test_fifo_stop_bind_unbind` verifies that `stop`, `bind`, and `unbind` FIFO commands are processed correctly.
- Other existing tests continue to verify audioonly functionality.
Run the test suite with:
```bash
make test
```