From f7f18f9fa7cc099a118c2d7a98d720b14e099524 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 21:31:52 +0000 Subject: [PATCH 01/17] style: fix formatting and include order in source files --- src/looper.c | 3 +-- src/main.c | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/looper.c b/src/looper.c index dae0dd1..610f08a 100644 --- a/src/looper.c +++ b/src/looper.c @@ -83,8 +83,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { const float *f_in = (const float *)in; for (i = 0; i < nframes; i++) { if (channels[c].record_pos < LOOP_BUF_SIZE) - channels[c].loop_buffer[channels[c].record_pos++] = - f_in[i]; + channels[c].loop_buffer[channels[c].record_pos++] = f_in[i]; f_out[i] = f_in[i]; } } else { diff --git a/src/main.c b/src/main.c index 9a82edd..61220f7 100644 --- a/src/main.c +++ b/src/main.c @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include int main(int argc, char *argv[]) { (void)argc; @@ -43,7 +43,10 @@ int main(int argc, char *argv[]) { while (1) { looper_process_commands(client); - { struct timespec ts = { .tv_sec = 0, .tv_nsec = 50000000 }; nanosleep(&ts, NULL); } /* check commands every 50 ms */ + { + struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000}; + nanosleep(&ts, NULL); + } /* check commands every 50 ms */ } jack_client_close(client); -- 2.49.1 From 392dabbc0fab9c41c5243bb556db93032afe63ef Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 21:31:54 +0000 Subject: [PATCH 02/17] feat: add command queue and FIFO pipe for unified input handling Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/command.h | 19 +++++++++++++ src/looper.c | 45 +++++++++++++++++++++++++++++++ src/main.c | 7 +++++ src/midi.c | 56 +++++++++++++------------------------- src/pipe.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/pipe.h | 9 +++++++ src/queue.c | 30 +++++++++++++++++++++ src/queue.h | 31 +++++++++++++++++++++ 8 files changed, 234 insertions(+), 38 deletions(-) create mode 100644 src/command.h create mode 100644 src/pipe.c create mode 100644 src/pipe.h create mode 100644 src/queue.c create mode 100644 src/queue.h diff --git a/src/command.h b/src/command.h new file mode 100644 index 0000000..717e13b --- /dev/null +++ b/src/command.h @@ -0,0 +1,19 @@ +#ifndef COMMAND_H +#define COMMAND_H + +typedef enum { + CMD_CYCLE, // toggle record/stop for a channel + CMD_STOP, // force to idle + // CMD_LOOP_TOGGLE not needed, CYCLE covers it + CMD_BIND_CHANNEL, // bind a channel index (data = channel) + CMD_UNBIND, // reset bind to channel 0 + // ADD and REMOVE are still driven via atomics for now +} cmd_type_t; + +typedef struct { + cmd_type_t type; + int channel; // which channel; -1 means "current/bound" + int data; // extra parameter (e.g. bind channel number) +} command_t; + +#endif diff --git a/src/looper.c b/src/looper.c index 610f08a..cbc9dd2 100644 --- a/src/looper.c +++ b/src/looper.c @@ -9,6 +9,8 @@ #include #include #include +#include "command.h" +#include "queue.h" /* Global state (shared across files) */ struct channel_t channels[MAX_CHANNELS]; @@ -20,10 +22,46 @@ jack_port_t *midi_control_port = NULL; jack_port_t *midi_clock_port = NULL; atomic_int control_key_active = 0; atomic_int bind_channel = 0; +spsc_queue_t cmd_queue; /* Deferred removal index (1 second grace) */ static int pending_unregister_idx = -1; +static void apply_command(command_t cmd) { + switch (cmd.type) { + case CMD_CYCLE: + if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) { + int cur = atomic_load(&channels[cmd.channel].state); + int next; + switch (cur) { + case STATE_IDLE: next = STATE_RECORD; break; + case STATE_RECORD: next = STATE_LOOPING; break; + case STATE_LOOPING: next = STATE_PAUSED; break; + case STATE_PAUSED: next = STATE_LOOPING; break; + default: next = STATE_IDLE; break; + } + atomic_store(&channels[cmd.channel].state, next); + } + break; + case CMD_STOP: + if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) + atomic_store(&channels[cmd.channel].state, STATE_IDLE); + else { + for (int i = 0; i < MAX_CHANNELS; i++) + atomic_store(&channels[i].state, STATE_IDLE); + } + break; + case CMD_BIND_CHANNEL: + atomic_store(&bind_channel, cmd.data); + break; + case CMD_UNBIND: + atomic_store(&bind_channel, 0); + break; + default: + break; + } +} + /* ---------------------------------------------------------------- * process callback * ---------------------------------------------------------------- */ @@ -37,6 +75,12 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } + /* drain RT‑safe commands */ + command_t cmd; + while (queue_pop(&cmd_queue, &cmd)) { + apply_command(cmd); + } + /* process each active channel */ for (int c = 0; c < MAX_CHANNELS; c++) { if (!atomic_load(&channels[c].active)) @@ -171,6 +215,7 @@ void jack_shutdown_cb(void *arg) { * looper initialisation * ---------------------------------------------------------------- */ int looper_init(jack_client_t *client) { + queue_init(&cmd_queue); /* channel 0 */ channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE); diff --git a/src/main.c b/src/main.c index 61220f7..321455a 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,6 @@ // cppcheck-suppress missingIncludeSystem #include "looper.h" +#include "pipe.h" #include #include #include @@ -33,6 +34,12 @@ int main(int argc, char *argv[]) { return 1; } + if (pipe_start_reader() != 0) { + fprintf(stderr, "pipe reader initialisation failed\n"); + jack_client_close(client); + return 1; + } + if (jack_activate(client)) { fprintf(stderr, "Cannot activate client\n"); jack_client_close(client); diff --git a/src/midi.c b/src/midi.c index bac71c5..eee1bb3 100644 --- a/src/midi.c +++ b/src/midi.c @@ -1,6 +1,8 @@ // cppcheck-suppress missingIncludeSystem #include "midi.h" #include "channel.h" +#include "command.h" +#include "queue.h" #include #include #include @@ -9,6 +11,7 @@ extern atomic_int control_key_active; extern atomic_int cmd_add; extern atomic_int cmd_remove; extern atomic_int bind_channel; +extern spsc_queue_t cmd_queue; void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { (void)nframes; @@ -34,7 +37,8 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { if (ck) { atomic_store(&control_key_active, 0); if (note < 16) { - atomic_store(&bind_channel, note); + command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = note }; + queue_push(&cmd_queue, cmd); } else { switch (note) { case 60: @@ -43,30 +47,19 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { case 61: atomic_store(&cmd_remove, 1); break; - case 62: /* trigger looper – channel via bind_channel */ + case 62: { int bch = atomic_load(&bind_channel); if (bch >= 0 && bch < MAX_CHANNELS) { - int cur = atomic_load(&channels[bch].state); - switch (cur) { - case STATE_IDLE: - atomic_store(&channels[bch].state, STATE_RECORD); - break; - case STATE_RECORD: - atomic_store(&channels[bch].state, STATE_LOOPING); - break; - case STATE_LOOPING: - atomic_store(&channels[bch].state, STATE_PAUSED); - break; - case STATE_PAUSED: - atomic_store(&channels[bch].state, STATE_LOOPING); - break; - } + command_t cmd = { .type = CMD_CYCLE, .channel = bch, .data = 0 }; + queue_push(&cmd_queue, cmd); } } break; - case 63: /* unbind – reset bind to channel 0 */ - atomic_store(&bind_channel, 0); - break; + case 63: + { + command_t cmd = { .type = CMD_UNBIND, .channel = -1, .data = 0 }; + queue_push(&cmd_queue, cmd); + } break; default: break; } @@ -74,24 +67,11 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { } else { /* direct mapping */ switch (note) { - case 1: /* toggle channel 0 */ - { - int cur0 = atomic_load(&channels[0].state); - switch (cur0) { - case STATE_IDLE: - atomic_store(&channels[0].state, STATE_RECORD); - break; - case STATE_RECORD: - atomic_store(&channels[0].state, STATE_LOOPING); - break; - case STATE_LOOPING: - atomic_store(&channels[0].state, STATE_PAUSED); - break; - case STATE_PAUSED: - atomic_store(&channels[0].state, STATE_LOOPING); - break; - } - } break; + case 1: + { + command_t cmd = { .type = CMD_CYCLE, .channel = 0, .data = 0 }; + queue_push(&cmd_queue, cmd); + } break; case 60: atomic_store(&cmd_add, 1); break; diff --git a/src/pipe.c b/src/pipe.c new file mode 100644 index 0000000..8062659 --- /dev/null +++ b/src/pipe.c @@ -0,0 +1,75 @@ +#include "pipe.h" +#include "queue.h" +#include "command.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define FIFO_PATH "/tmp/looper_cmd" +#define LINE_MAX 256 + +/* forward‑declare the global queue (defined in looper.c) */ +extern spsc_queue_t cmd_queue; + +/* external atomic flags for add/remove (defined in looper.c) */ +extern atomic_int cmd_add; +extern atomic_int cmd_remove; + +static void *pipe_thread_func(void *arg) { + (void)arg; + FILE *fifo = fopen(FIFO_PATH, "r"); + if (!fifo) { + perror("fopen fifo"); + return NULL; + } + char line[LINE_MAX]; + while (fgets(line, sizeof(line), fifo)) { + /* strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len-1] == '\n') + line[len-1] = '\0'; + + if (strcmp(line, "add") == 0) { + atomic_store(&cmd_add, 1); + } else if (strcmp(line, "remove") == 0) { + atomic_store(&cmd_remove, 1); + } else if (strncmp(line, "record ", 7) == 0) { + int ch = atoi(line + 7); + command_t cmd = { .type = CMD_CYCLE, .channel = ch, .data = 0 }; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "stop") == 0) { + command_t cmd = { .type = CMD_STOP, .channel = -1, .data = 0 }; + queue_push(&cmd_queue, cmd); + } else if (strncmp(line, "bind ", 5) == 0) { + int ch = atoi(line + 5); + command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = ch }; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "unbind") == 0) { + command_t cmd = { .type = CMD_UNBIND, .channel = -1, .data = 0 }; + queue_push(&cmd_queue, cmd); + } + /* ignore unknown lines */ + } + fclose(fifo); + return NULL; +} + +int pipe_start_reader(void) { + /* create FIFO if it doesn't exist */ + if (mkfifo(FIFO_PATH, 0666) != 0 && errno != EEXIST) { + perror("mkfifo"); + return -1; + } + pthread_t tid; + if (pthread_create(&tid, NULL, pipe_thread_func, NULL) != 0) { + perror("pthread_create"); + return -1; + } + pthread_detach(tid); /* we don't need to join */ + return 0; +} diff --git a/src/pipe.h b/src/pipe.h new file mode 100644 index 0000000..f1a8307 --- /dev/null +++ b/src/pipe.h @@ -0,0 +1,9 @@ +#ifndef PIPE_H +#define PIPE_H + +/* Start the FIFO reader thread. + * Creates /tmp/looper_cmd (or aborts on error). + * Returns 0 on success, -1 on failure. */ +int pipe_start_reader(void); + +#endif diff --git a/src/queue.c b/src/queue.c new file mode 100644 index 0000000..bca85c6 --- /dev/null +++ b/src/queue.c @@ -0,0 +1,30 @@ +#include "queue.h" +#include +#include + +void queue_init(spsc_queue_t *q) { + /* nothing to allocate, just ensure head/tail start at 0 */ + q->head = 0; + q->tail = 0; +} + +bool queue_push(spsc_queue_t *q, command_t cmd) { + int h = atomic_load_explicit(&q->head, memory_order_relaxed); + int t = atomic_load_explicit(&q->tail, memory_order_acquire); + int next = (h + 1) % QUEUE_CAPACITY; + if (next == t) + return false; /* queue full */ + q->buffer[h] = cmd; + atomic_store_explicit(&q->head, next, memory_order_release); + return true; +} + +bool queue_pop(spsc_queue_t *q, command_t *cmd) { + int t = atomic_load_explicit(&q->tail, memory_order_relaxed); + int h = atomic_load_explicit(&q->head, memory_order_acquire); + if (t == h) + return false; /* queue empty */ + *cmd = q->buffer[t]; + atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY, memory_order_release); + return true; +} diff --git a/src/queue.h b/src/queue.h new file mode 100644 index 0000000..e0da752 --- /dev/null +++ b/src/queue.h @@ -0,0 +1,31 @@ +#ifndef QUEUE_H +#define QUEUE_H + +#include "command.h" +#include + +/* Fixed‑size lock‑free SPSC queue (single producer, single consumer). + * The queue is safe for one thread writing (producer) and one thread + * reading (consumer). No locks, no dynamic memory allocation. + * Must be initialised before first use. All operations are RT‑safe. */ + +#define QUEUE_CAPACITY 256 + +typedef struct { + command_t buffer[QUEUE_CAPACITY]; + /* head: index where next element will be written (producer only) + * tail: index of next element to read (consumer only) */ + int head; + int tail; +} spsc_queue_t; + +/* Initialise queue (must be called once before any push/pop). */ +void queue_init(spsc_queue_t *q); + +/* Push a command. Returns true on success, false if queue full. */ +bool queue_push(spsc_queue_t *q, command_t cmd); + +/* Pop a command. Returns true if a command was retrieved, false if empty. */ +bool queue_pop(spsc_queue_t *q, command_t *cmd); + +#endif -- 2.49.1 From a8a9c6164b170968a2911fb0998586a1bfc6b9c9 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 21:35:38 +0000 Subject: [PATCH 03/17] docs: update evaluation.md with detailed code review and recommendations Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 72 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/evaluation.md b/evaluation.md index 7943b9e..e470498 100644 --- a/evaluation.md +++ b/evaluation.md @@ -4,21 +4,61 @@ | Category | Rating | Remarks | |--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Mocked / Left Undone | ✅ OK | Multi‑channel and dynamic channel add/remove are now implemented. Control key (note 64) is handled as a modifier for command selection. Backward compatibility for note 1, 60, 61 retained. | -| Potential Segfaults | ✅ Fixed | Added null checks for both `audio_in` and `audio_out` in the process callback, and `channel_add` no longer marks the channel active if port registration fails. | -| Memory Safety | ✅ OK | No dynamic memory allocation; only a fixed‑size global buffer. No leaks, no use‑after‑free. | -| Thread Safety / Race | ⚠️ Warning | `atomic_load`/`store` on `current_state` is correct, but the audio processing uses the *original* state loaded *before* MIDI events are handled in the same callback. State changes that occur in the current cycle are ignored until the next cycle – can cause missed transitions (e.g., start recording one cycle late). | -| Performance | ✅ OK | Linear buffer access, no system calls or allocations in the real‑time callback. Atomic operations are cheap. Fixed buffer size (0.96 MB) is safe. | -| Architectural Soundness | ✅ OK | Dynamic multi‑channel architecture with per‑channel state and ports. Real‑time safe command queue via atomic flags. Abstraction via `channel_t` struct. Extensible for future binding. | +| Mocked / Left Undone | ⚠️ Partial | Control‑key modifier and bind commands are now dispatched correctly. However, note that `CMD_STOP` is defined but never triggered from MIDI or FIFO (FIFO supports `"stop"`). The MIDI code still uses raw atomic stores for `cmd_add`/`cmd_remove` instead of pushing command‑queue actions – this is a minor inconsistency but works. The test file contains many more tests than the code can actually satisfy (e.g., `test_control_key_modifier`, `test_bind_channel`, `test_bind_unbind`, `test_remove_channel`) – these tests will fail because the looper’s current mapping does not match what the tests expect (the tests use note numbers that do not map to the actual commands). | +| Potential Segfaults | ✅ Good | All `jack_port_get_buffer` results are checked for NULL before dereference. No array overruns (fixed‑size loops). The SPSC queue uses modulo arithmetic within bounded capacity. | +| Memory Safety | ✅ OK | No dynamic allocation in the RT path. All buffers (loop buffers, command queue) are statically sized. No use‑after‑free – the only deferred operation (port unregister) is done in the main loop after marking inactive. | +| Thread Safety / Race | ⚠️ Warning | The SPSC queue uses correct atomic memory ordering (`acquire`/`release`). However, the `process_callback` first calls `midi_handle_events` (which pushes to the queue), then drains the queue **in the same cycle**. This means state changes pushed by MIDI are applied *within the same audio cycle* – that is fine. **But the test code injects MIDI notes via a separate client, and the looper’s MIDI handler runs MIDI events *before* draining the queue – so a MIDI note pushed in the same cycle will be processed immediately. That is correct and expected.** No race condition there. However, there is a **potential issue with `channels[c].prev_state` being read and written from the RT thread without atomic operations** – `prev_state` is a plain `int`, not `atomic_int`. This is accessed in the process callback and nowhere else, so it is safe (single consumer). The `channel_add` and `channel_remove` functions are called from the non‑RT main loop while the RT callback may be reading `active`, `state`, `audio_in`, `audio_out` – these are all atomic, so safe. | +| Performance | ✅ Good | No syscalls, no allocations, no locks in RT path. Atomic operations are cheap. Buffer accesses are linear. Queue operations are O(1). | +| Architectural Soundness | ✅ Good | Clean separation: MIDI handler pushes commands, RT callback applies them, main loop handles add/remove via atomic flags. The command queue is a reasonable lightweight approach. However, the mixture of atomic flags for add/remove and the command queue for state transitions is a bit inconsistent – a uniform command‑queue approach for everything would be cleaner. The FIFO pipe works well. | -## Test Evaluation +## Detailed Remarks -| Aspect | Remarks | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `test_audio_pass_through` | Verifies basic audio connectivity; passes when JACK server running. Does not test any looper‑specific behavior beyond pass‑through. | -| `test_looper_looping` | Exercises the state machine (IDLE→RECORD→LOOPING) using MIDI note 1. Detects repeated audio bursts. Works with current implementation but uses note 1 instead of the required control key (64). The 0.1‑second beep and 4‑second wait may be sensitive to CPU load. | -| `test_multiple_channels` | Expects dynamic channel creation via note 60 (add channel). Current looper does not handle this command, causing immediate failure. This test is effectively a placeholder for future implementation. | -| Coverage gaps | No tests for: control key note 64, remove channel, binding, per‑channel loops, state transitions other than note 1, robust handling of JACK server disconnection. | -| Thread safety | The test assumes sequential execution and uses long sleeps for synchronization. The real‑time thread is managed by JACK; the test process runs asynchronously, which can lead to timing‑sensitive failures on heavily loaded systems. | -| Resource handling | Tests properly kill child process and close JACK clients. No memory leaks. | -| Overall verdict | The test suite provides a minimal smoke‑check but does **not** validate the full specification. It must be updated to use the correct control key (64), cover dynamic channel commands (add/remove/bind), and handle non‑existent features before it can be considered a trustworthy integration test. | +### 1. Mocked / Left Undone +- `CMD_STOP` is defined and handled in `apply_command`, and the FIFO recognises `"stop"`, but the MIDI handler never sends `CMD_STOP`. This is not an error, just an unused path. +- The MIDI handler still uses `atomic_store(&cmd_add, 1)` and `atomic_store(&cmd_remove, 1)` for add/remove. This works but breaks uniformity – could have used `CMD_ADD_CHANNEL` / `CMD_REMOVE_CHANNEL` command types (which are not even defined in `cmd_type_t` yet). The current approach is functional. +- The test file (`tests/integration.c`) is **out‑of‑sync** with the actual MIDI mapping: + - `test_looper_looping` sends note `1` – but the looper now expects note `1` to cycle channel 0. That works. + - `test_multiple_channels` sends note `60` – works (triggers `cmd_add`). + - `test_control_key_modifier` sends control key (64) then note `62`. The looper expects control key + note `62` to toggle the bound channel – but note `62` is **also** triggered by the control‑key branch. That matches and should work. + - `test_bind_channel` sends control key + note `0` to bind, then control+62 to toggle. The looper binds channel 0 with note `0` under control‑key (note <16). That works. + - `test_bind_unbind` sends control+63 for unbind – the looper handles that (`case 63: CMD_UNBIND`). Works. + - `test_remove_channel` sends note `61` – works. + - **However, there is no test that uses the FIFO pipe** – it remains untested in the suite. + - **More importantly, the test code does not verify that the looper’s output port connections are correct** when using the control‑key modifier tests. The tests assume the looper has only one audio input/output pair, but after adding channels, there are more ports – connections may fail silently. This could cause the tests to hang or fail. +- No tests for `"stop"` via FIFO or MIDI. + +### 2. Potential Segfaults +- All audio/MIDI port buffer accesses are guarded (`if (!out) continue` etc.). No dangling pointers. +- The command queue is fixed‑size; push returns false when full – caller does not check return value in all places (e.g., in `midi_handle_events` the return value is ignored). If the queue fills, notes are dropped silently – not a segfault, but a functional limitation. +- No use of `malloc` in RT path. + +### 3. Memory Safety +- No memory leaks. The only allocations happen at startup (JACK ports, thread creation). No `free` of static buffers. +- The FIFO reader uses a stack‑allocated `char line[256]` – safe. +- The SPSC queue buffer is a static global – no dynamic allocation. + +### 4. Thread Safety / Race Conditions +- The SPSC queue is correctly implemented with atomic ordering. Producer (MIDI handler, FIFO thread) and consumer (RT callback) are single‑writer, single‑reader. +- The `channels` struct fields `state`, `active` are atomic – correct. `prev_state` is plain `int` but accessed only from the RT callback (single thread) – safe. +- The `control_key_active` flag is atomic and used correctly. +- The main loop (`looper_process_commands`) runs in the non‑RT main thread and reads/writes `channels[idx].audio_in`, `channels[idx].audio_out` after verifying `active == 0`. This is safe because the RT callback skips inactive channels. +- **Potential time‑of‑check/time‑of‑use**: When `looper_process_commands` calls `channel_remove`, it sets `active = 0` and marks `pending_unregister_idx`. In the next iteration, it calls `jack_port_unregister`. Meanwhile, the RT callback could have just loaded `active == 1` and then the port pointers become invalid? No – because the RT callback checks `atomic_load(&channels[c].active)` and if it sees `1`, it uses the port pointers. If the main thread sets `active = 0` and then later unregisters, the RT thread might have already passed the check and is about to use the port pointer – that would be a use‑after‑unregister. **This is a real race.** The main loop waits one cycle (50 ms) before unregistering, but the RT thread can still be in the middle of a process cycle when `active` is set to 0. The window is narrow but possible. A safer approach would be to **not unregister ports while the RT thread could be using them** – for example, use a double‑buffer or delay unregistration by at least one JACK period using a `jack_ringbuffer` or an atomic counter. Currently, it is not 100% safe. **Consider this a moderate race condition.** + +### 5. Performance +- The RT callback is lean: one queue drain, then per‑channel audio processing with simple state‑machine branches. No syscalls, no allocations. +- The only potential performance bottleneck is the per‑sample `fabsf()` in the test client – not in the looper itself. Looper’s performance is fine. + +### 6. Architectural Soundness +- The separation into MIDI handler (producer), RT callback (consumer), and main loop (housekeeping) is sound. The command queue is a good abstraction. +- Inconsistency: add/remove uses atomic flags; other commands use the queue. This is a minor design smell but works for now. Future unification would be beneficial. +- The FIFO reader thread is correctly detached and won't block shutdown (but if the looper exits, the thread remains until the pipe is closed – acceptable). +- The test file is overly ambitious and seems to have been written before the code – it tests features that are not implemented (like the control‑key modifier with note numbers that were never assigned to those commands in the original specification). This may reflect a misunderstanding between the test author and the code author. + +## Overall Verdict + +The code is **functional and safe for basic use** (single‑channel looping, add/remove channels, FIFO control). It has a **minor race condition** when removing channels (use‑after‑unregister risk) and a **moderate inconsistency** between atomic flags and command queue. The **test suite is unreliable** because it expects a mapping that does not match the code’s actual note assignments in some scenarios. No segfaults, no memory leaks, good performance. + +**Recommendations:** +- Fix the race in channel removal by using a ringbuffer or ensuring the RT thread has completed at least one cycle after marking `active = 0` before unregistering. +- Unify all commands (including add/remove) into the command queue for consistency. +- Update the test suite to match the actual note mapping and to test the FIFO pipe. -- 2.49.1 From 437ac319135f25b423f7183e1a9bb22b0e1cae29 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 22:03:11 +0000 Subject: [PATCH 04/17] feat: unify add/remove commands into queue and fix race on channel removal Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/command.h | 4 +-- src/looper.c | 74 ++++++++++++++++++++++++++++++--------------------- src/midi.c | 27 ++++++++++++------- src/pipe.c | 13 +++++---- 4 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/command.h b/src/command.h index 717e13b..d14dd9c 100644 --- a/src/command.h +++ b/src/command.h @@ -4,10 +4,10 @@ typedef enum { CMD_CYCLE, // toggle record/stop for a channel CMD_STOP, // force to idle - // CMD_LOOP_TOGGLE not needed, CYCLE covers it CMD_BIND_CHANNEL, // bind a channel index (data = channel) CMD_UNBIND, // reset bind to channel 0 - // ADD and REMOVE are still driven via atomics for now + CMD_ADD_CHANNEL, // add a new dynamic channel + CMD_REMOVE_CHANNEL, // remove last dynamic channel } cmd_type_t; typedef struct { diff --git a/src/looper.c b/src/looper.c index cbc9dd2..d4c5254 100644 --- a/src/looper.c +++ b/src/looper.c @@ -16,16 +16,17 @@ struct channel_t channels[MAX_CHANNELS]; atomic_int channel_count = 0; int next_channel_id = 1; -atomic_int cmd_add = 0; -atomic_int cmd_remove = 0; +spsc_queue_t cmd_queue_main; +atomic_int global_rt_cycles = 0; jack_port_t *midi_control_port = NULL; jack_port_t *midi_clock_port = NULL; atomic_int control_key_active = 0; atomic_int bind_channel = 0; spsc_queue_t cmd_queue; -/* Deferred removal index (1 second grace) */ +/* Deferred removal index and cycle counter */ static int pending_unregister_idx = -1; +static int pending_unregister_cycle = 0; static void apply_command(command_t cmd) { switch (cmd.type) { @@ -199,6 +200,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } + atomic_fetch_add_explicit(&global_rt_cycles, 1, memory_order_release); return 0; } @@ -216,6 +218,7 @@ void jack_shutdown_cb(void *arg) { * ---------------------------------------------------------------- */ int looper_init(jack_client_t *client) { queue_init(&cmd_queue); + queue_init(&cmd_queue_main); /* channel 0 */ channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE); @@ -250,37 +253,46 @@ int looper_init(jack_client_t *client) { * main‑loop command processing * ---------------------------------------------------------------- */ void looper_process_commands(jack_client_t *client) { - /* Unregister any ports that were marked for deferred removal. - By now the real‑time thread has had at least one full cycle - to see the `active = 0` store. */ - if (pending_unregister_idx != -1) { - int idx = pending_unregister_idx; - if (channels[idx].audio_in) - jack_port_unregister(client, channels[idx].audio_in); - if (channels[idx].audio_out) - jack_port_unregister(client, channels[idx].audio_out); - pending_unregister_idx = -1; - } - - if (atomic_exchange(&cmd_add, 0)) { - int idx; - for (idx = 0; idx < MAX_CHANNELS; idx++) - if (!channels[idx].active) - break; - if (idx < MAX_CHANNELS) { - channel_add(client, idx); + /* Drain main‑loop command queue (add/remove) */ + command_t cmd; + while (queue_pop(&cmd_queue_main, &cmd)) { + switch (cmd.type) { + case CMD_ADD_CHANNEL: { + int idx; + for (idx = 0; idx < MAX_CHANNELS; idx++) + if (!channels[idx].active) + break; + if (idx < MAX_CHANNELS) + channel_add(client, idx); + break; + } + case CMD_REMOVE_CHANNEL: { + int remove_idx = -1; + for (int idx = 1; idx < MAX_CHANNELS; idx++) + if (channels[idx].active) + remove_idx = idx; + if (remove_idx != -1) { + channel_remove(client, remove_idx); + pending_unregister_idx = remove_idx; + pending_unregister_cycle = atomic_load(&global_rt_cycles); + } + break; + } + default: + break; } } - if (atomic_exchange(&cmd_remove, 0)) { - int remove_idx = -1; - for (int idx = 1; idx < MAX_CHANNELS; idx++) - if (channels[idx].active) - remove_idx = idx; - if (remove_idx != -1) { - /* Mark inactive now; ports will be unregistered next round */ - channel_remove(client, remove_idx); - pending_unregister_idx = remove_idx; + /* Deferred port unregistration – wait until RT thread has seen active=0 */ + if (pending_unregister_idx != -1) { + int current_cycle = atomic_load(&global_rt_cycles); + if (current_cycle - pending_unregister_cycle >= 1) { + int idx = pending_unregister_idx; + if (channels[idx].audio_in) + jack_port_unregister(client, channels[idx].audio_in); + if (channels[idx].audio_out) + jack_port_unregister(client, channels[idx].audio_out); + pending_unregister_idx = -1; } } } diff --git a/src/midi.c b/src/midi.c index eee1bb3..f1f3957 100644 --- a/src/midi.c +++ b/src/midi.c @@ -8,10 +8,9 @@ #include extern atomic_int control_key_active; -extern atomic_int cmd_add; -extern atomic_int cmd_remove; extern atomic_int bind_channel; extern spsc_queue_t cmd_queue; +extern spsc_queue_t cmd_queue_main; void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { (void)nframes; @@ -42,11 +41,15 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { } else { switch (note) { case 60: - atomic_store(&cmd_add, 1); - break; + { + command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); + } break; case 61: - atomic_store(&cmd_remove, 1); - break; + { + command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); + } break; case 62: { int bch = atomic_load(&bind_channel); @@ -73,11 +76,15 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { queue_push(&cmd_queue, cmd); } break; case 60: - atomic_store(&cmd_add, 1); - break; + { + command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); + } break; case 61: - atomic_store(&cmd_remove, 1); - break; + { + command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); + } break; default: break; } diff --git a/src/pipe.c b/src/pipe.c index 8062659..b1b49e6 100644 --- a/src/pipe.c +++ b/src/pipe.c @@ -13,12 +13,9 @@ #define FIFO_PATH "/tmp/looper_cmd" #define LINE_MAX 256 -/* forward‑declare the global queue (defined in looper.c) */ +/* forward‑declare the global queues (defined in looper.c) */ extern spsc_queue_t cmd_queue; - -/* external atomic flags for add/remove (defined in looper.c) */ -extern atomic_int cmd_add; -extern atomic_int cmd_remove; +extern spsc_queue_t cmd_queue_main; static void *pipe_thread_func(void *arg) { (void)arg; @@ -35,9 +32,11 @@ static void *pipe_thread_func(void *arg) { line[len-1] = '\0'; if (strcmp(line, "add") == 0) { - atomic_store(&cmd_add, 1); + command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); } else if (strcmp(line, "remove") == 0) { - atomic_store(&cmd_remove, 1); + command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main, cmd); } else if (strncmp(line, "record ", 7) == 0) { int ch = atoi(line + 7); command_t cmd = { .type = CMD_CYCLE, .channel = ch, .data = 0 }; -- 2.49.1 From b1e330e83974c74aab3d5b9ff919965d50e3f94d Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 22:20:35 +0000 Subject: [PATCH 05/17] refactor: remove stale cmd_add/cmd_remove declarations from channel.h Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/channel.h b/src/channel.h index a0c2f89..64a0ece 100644 --- a/src/channel.h +++ b/src/channel.h @@ -31,8 +31,6 @@ struct channel_t { extern struct channel_t channels[MAX_CHANNELS]; extern atomic_int channel_count; extern int next_channel_id; -extern atomic_int cmd_add; -extern atomic_int cmd_remove; void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); -- 2.49.1 From bd5fd59b7b37dca5c65f2dbee3e6c8ce536c0175 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 22:51:13 +0000 Subject: [PATCH 06/17] fix: add missing source files to build Co-authored-by: aider (deepseek/deepseek-reasoner) --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index bc98f22..1488cce 100644 --- a/makefile +++ b/makefile @@ -2,7 +2,7 @@ CC ?= gcc CFLAGS ?= -Wall -Wextra -g -Isrc LDFLAGS ?= -ljack -lm -SRC = src/main.c src/looper.c src/channel.c src/midi.c +SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c OBJ = $(SRC:.c=.o) looper: $(OBJ) -- 2.49.1 From de0389e1447931bb40ac09eb233e432f4356cd69 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 23:12:53 +0000 Subject: [PATCH 07/17] feat: remove MIDI-driven add/remove channel commands to fix SPSC race Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/midi.c | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/midi.c b/src/midi.c index f1f3957..a9ac6dc 100644 --- a/src/midi.c +++ b/src/midi.c @@ -40,16 +40,10 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { queue_push(&cmd_queue, cmd); } else { switch (note) { - case 60: - { - command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); - } break; - case 61: - { - command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); - } break; + case 60: /* add channel not rt‑safe, moved to FIFO pipe */ + case 61: /* remove channel not rt‑safe, moved to FIFO pipe */ + /* no operation here */ + break; case 62: { int bch = atomic_load(&bind_channel); @@ -75,16 +69,10 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { command_t cmd = { .type = CMD_CYCLE, .channel = 0, .data = 0 }; queue_push(&cmd_queue, cmd); } break; - case 60: - { - command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); - } break; - case 61: - { - command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); - } break; + case 60: /* add channel not rt‑safe, moved to FIFO pipe */ + case 61: /* remove channel not rt‑safe, moved to FIFO pipe */ + /* no operation here */ + break; default: break; } -- 2.49.1 From 7edd95d06e5ab8261bb84d58eb580cc22a666160 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 23:32:21 +0000 Subject: [PATCH 08/17] fix: split main command queue into per-source SPSC queues Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/looper.c | 37 +++++++++++++++++++++++++++++++++---- src/midi.c | 30 +++++++++++++++++++++--------- src/pipe.c | 6 +++--- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/looper.c b/src/looper.c index d4c5254..4aabbe5 100644 --- a/src/looper.c +++ b/src/looper.c @@ -16,7 +16,8 @@ struct channel_t channels[MAX_CHANNELS]; atomic_int channel_count = 0; int next_channel_id = 1; -spsc_queue_t cmd_queue_main; +spsc_queue_t cmd_queue_main_midi; +spsc_queue_t cmd_queue_main_fifo; atomic_int global_rt_cycles = 0; jack_port_t *midi_control_port = NULL; jack_port_t *midi_clock_port = NULL; @@ -218,7 +219,8 @@ void jack_shutdown_cb(void *arg) { * ---------------------------------------------------------------- */ int looper_init(jack_client_t *client) { queue_init(&cmd_queue); - queue_init(&cmd_queue_main); + queue_init(&cmd_queue_main_midi); + queue_init(&cmd_queue_main_fifo); /* channel 0 */ channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE); @@ -253,9 +255,36 @@ int looper_init(jack_client_t *client) { * main‑loop command processing * ---------------------------------------------------------------- */ void looper_process_commands(jack_client_t *client) { - /* Drain main‑loop command queue (add/remove) */ + /* Drain main‑loop command queues (add/remove) */ command_t cmd; - while (queue_pop(&cmd_queue_main, &cmd)) { + while (queue_pop(&cmd_queue_main_midi, &cmd)) { + switch (cmd.type) { + case CMD_ADD_CHANNEL: { + int idx; + for (idx = 0; idx < MAX_CHANNELS; idx++) + if (!channels[idx].active) + break; + if (idx < MAX_CHANNELS) + channel_add(client, idx); + break; + } + case CMD_REMOVE_CHANNEL: { + int remove_idx = -1; + for (int idx = 1; idx < MAX_CHANNELS; idx++) + if (channels[idx].active) + remove_idx = idx; + if (remove_idx != -1) { + channel_remove(client, remove_idx); + pending_unregister_idx = remove_idx; + pending_unregister_cycle = atomic_load(&global_rt_cycles); + } + break; + } + default: + break; + } + } + while (queue_pop(&cmd_queue_main_fifo, &cmd)) { switch (cmd.type) { case CMD_ADD_CHANNEL: { int idx; diff --git a/src/midi.c b/src/midi.c index a9ac6dc..634e4d3 100644 --- a/src/midi.c +++ b/src/midi.c @@ -10,7 +10,7 @@ extern atomic_int control_key_active; extern atomic_int bind_channel; extern spsc_queue_t cmd_queue; -extern spsc_queue_t cmd_queue_main; +extern spsc_queue_t cmd_queue_main_midi; void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { (void)nframes; @@ -40,10 +40,16 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { queue_push(&cmd_queue, cmd); } else { switch (note) { - case 60: /* add channel not rt‑safe, moved to FIFO pipe */ - case 61: /* remove channel not rt‑safe, moved to FIFO pipe */ - /* no operation here */ - break; + case 60: + { + command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 61: + { + command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main_midi, cmd); + } break; case 62: { int bch = atomic_load(&bind_channel); @@ -69,10 +75,16 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { command_t cmd = { .type = CMD_CYCLE, .channel = 0, .data = 0 }; queue_push(&cmd_queue, cmd); } break; - case 60: /* add channel not rt‑safe, moved to FIFO pipe */ - case 61: /* remove channel not rt‑safe, moved to FIFO pipe */ - /* no operation here */ - break; + case 60: + { + command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 61: + { + command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + queue_push(&cmd_queue_main_midi, cmd); + } break; default: break; } diff --git a/src/pipe.c b/src/pipe.c index b1b49e6..c9a4e8b 100644 --- a/src/pipe.c +++ b/src/pipe.c @@ -15,7 +15,7 @@ /* forward‑declare the global queues (defined in looper.c) */ extern spsc_queue_t cmd_queue; -extern spsc_queue_t cmd_queue_main; +extern spsc_queue_t cmd_queue_main_fifo; static void *pipe_thread_func(void *arg) { (void)arg; @@ -33,10 +33,10 @@ static void *pipe_thread_func(void *arg) { if (strcmp(line, "add") == 0) { command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); + queue_push(&cmd_queue_main_fifo, cmd); } else if (strcmp(line, "remove") == 0) { command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main, cmd); + queue_push(&cmd_queue_main_fifo, cmd); } else if (strncmp(line, "record ", 7) == 0) { int ch = atoi(line + 7); command_t cmd = { .type = CMD_CYCLE, .channel = ch, .data = 0 }; -- 2.49.1 From 7b6138415457aaf11f8cbed007114b532a92e870 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 23:55:07 +0000 Subject: [PATCH 09/17] docs: update evaluation.md with current code analysis Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 87 ++++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/evaluation.md b/evaluation.md index e470498..10de4e6 100644 --- a/evaluation.md +++ b/evaluation.md @@ -2,63 +2,66 @@ ## Summary Table -| Category | Rating | Remarks | -|--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Mocked / Left Undone | ⚠️ Partial | Control‑key modifier and bind commands are now dispatched correctly. However, note that `CMD_STOP` is defined but never triggered from MIDI or FIFO (FIFO supports `"stop"`). The MIDI code still uses raw atomic stores for `cmd_add`/`cmd_remove` instead of pushing command‑queue actions – this is a minor inconsistency but works. The test file contains many more tests than the code can actually satisfy (e.g., `test_control_key_modifier`, `test_bind_channel`, `test_bind_unbind`, `test_remove_channel`) – these tests will fail because the looper’s current mapping does not match what the tests expect (the tests use note numbers that do not map to the actual commands). | -| Potential Segfaults | ✅ Good | All `jack_port_get_buffer` results are checked for NULL before dereference. No array overruns (fixed‑size loops). The SPSC queue uses modulo arithmetic within bounded capacity. | -| Memory Safety | ✅ OK | No dynamic allocation in the RT path. All buffers (loop buffers, command queue) are statically sized. No use‑after‑free – the only deferred operation (port unregister) is done in the main loop after marking inactive. | -| Thread Safety / Race | ⚠️ Warning | The SPSC queue uses correct atomic memory ordering (`acquire`/`release`). However, the `process_callback` first calls `midi_handle_events` (which pushes to the queue), then drains the queue **in the same cycle**. This means state changes pushed by MIDI are applied *within the same audio cycle* – that is fine. **But the test code injects MIDI notes via a separate client, and the looper’s MIDI handler runs MIDI events *before* draining the queue – so a MIDI note pushed in the same cycle will be processed immediately. That is correct and expected.** No race condition there. However, there is a **potential issue with `channels[c].prev_state` being read and written from the RT thread without atomic operations** – `prev_state` is a plain `int`, not `atomic_int`. This is accessed in the process callback and nowhere else, so it is safe (single consumer). The `channel_add` and `channel_remove` functions are called from the non‑RT main loop while the RT callback may be reading `active`, `state`, `audio_in`, `audio_out` – these are all atomic, so safe. | -| Performance | ✅ Good | No syscalls, no allocations, no locks in RT path. Atomic operations are cheap. Buffer accesses are linear. Queue operations are O(1). | -| Architectural Soundness | ✅ Good | Clean separation: MIDI handler pushes commands, RT callback applies them, main loop handles add/remove via atomic flags. The command queue is a reasonable lightweight approach. However, the mixture of atomic flags for add/remove and the command queue for state transitions is a bit inconsistent – a uniform command‑queue approach for everything would be cleaner. The FIFO pipe works well. | +| Category | Rating | Remarks | +|--------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Mocked / Left Undone | ✅ Minor | `CMD_STOP` is only reachable via the FIFO pipe (`"stop"`). The MIDI handler never sends it. This is an unused path, not a bug. The FIFO pipe is still untested in the integration suite – all tests use MIDI. No test for `"stop"` via FIFO. Everything else is implemented and matches the test suite. | +| Potential Segfaults | ✅ Good | All `jack_port_get_buffer()` calls are guarded against NULL. Array bounds respected (fixed‑size loops). SPSC queues use modulo arithmetic, no overrun possible. No dynamic memory in RT path. | +| Memory Safety | ✅ OK | No leaks, no use‑after‑free. All buffers static. Deferred port unregistration waits for at least one RT cycle after `active=0` – safe. The FIFO reader thread uses stack memory for line reading. | +| Thread Safety / Race | ✅ Good | SPSC queues have correct `acquire`/`release` ordering. Multi‑producer issue on main‑loop commands has been fixed by giving each producer its own queue (`cmd_queue_main_midi` for MIDI, `cmd_queue_main_fifo` for FIFO). The RT callback only writes to `cmd_queue_main_midi`; the FIFO thread only writes to `cmd_queue_main_fifo`. Both are consumed solely by the main loop, restoring SPSC safety. The deferred unregistration race is now fixed via `global_rt_cycles` counter – the main loop ensures the RT thread has completed a cycle after `active=0` before calling `jack_port_unregister()`. `prev_state` is a plain `int` but accessed only from the RT callback (single thread). All other shared state (`state`, `active`, `control_key_active`, `bind_channel`) uses atomics. | +| Performance | ✅ Good | No syscalls, locks, or dynamic allocations in the RT callback. Two queue drains (one for RT commands, one for main‑loop commands) add negligible overhead. O(1) queue operations. Linear audio processing. | +| Architectural Soundness | ✅ Good | Clean separation: each input source has its own SPSC queue for non‑RT commands; RT callback drains its own queue; main loop drains both auxiliary queues. The command queue approach is now fully uniform (no atomic flags remaining for add/remove). The FIFO pipe works in parallel. The code is easily extensible to new input sources. | ## Detailed Remarks ### 1. Mocked / Left Undone -- `CMD_STOP` is defined and handled in `apply_command`, and the FIFO recognises `"stop"`, but the MIDI handler never sends `CMD_STOP`. This is not an error, just an unused path. -- The MIDI handler still uses `atomic_store(&cmd_add, 1)` and `atomic_store(&cmd_remove, 1)` for add/remove. This works but breaks uniformity – could have used `CMD_ADD_CHANNEL` / `CMD_REMOVE_CHANNEL` command types (which are not even defined in `cmd_type_t` yet). The current approach is functional. -- The test file (`tests/integration.c`) is **out‑of‑sync** with the actual MIDI mapping: - - `test_looper_looping` sends note `1` – but the looper now expects note `1` to cycle channel 0. That works. - - `test_multiple_channels` sends note `60` – works (triggers `cmd_add`). - - `test_control_key_modifier` sends control key (64) then note `62`. The looper expects control key + note `62` to toggle the bound channel – but note `62` is **also** triggered by the control‑key branch. That matches and should work. - - `test_bind_channel` sends control key + note `0` to bind, then control+62 to toggle. The looper binds channel 0 with note `0` under control‑key (note <16). That works. - - `test_bind_unbind` sends control+63 for unbind – the looper handles that (`case 63: CMD_UNBIND`). Works. - - `test_remove_channel` sends note `61` – works. - - **However, there is no test that uses the FIFO pipe** – it remains untested in the suite. - - **More importantly, the test code does not verify that the looper’s output port connections are correct** when using the control‑key modifier tests. The tests assume the looper has only one audio input/output pair, but after adding channels, there are more ports – connections may fail silently. This could cause the tests to hang or fail. -- No tests for `"stop"` via FIFO or MIDI. +- `CMD_STOP` is only reachable via the FIFO pipe (`"stop"`). The MIDI handler never sends it. This is not a bug, just an unused feature path. +- The FIFO pipe is completely untested in the main integration suite. All existing tests use MIDI notes. Adding a FIFO‑based test would increase coverage. +- No other functionality is missing. ### 2. Potential Segfaults -- All audio/MIDI port buffer accesses are guarded (`if (!out) continue` etc.). No dangling pointers. -- The command queue is fixed‑size; push returns false when full – caller does not check return value in all places (e.g., in `midi_handle_events` the return value is ignored). If the queue fills, notes are dropped silently – not a segfault, but a functional limitation. -- No use of `malloc` in RT path. +- Every `jack_port_get_buffer()` call is followed by a null check (`if (!out) continue;`). +- Array accesses are bounded by `MAX_CHANNELS` and `QUEUE_CAPACITY`. +- No use of `malloc` or variable‑length arrays in the real‑time callback. +- The only unguarded `jack_port_get_buffer()` is in `midi_handle_events` where the caller already checks `midi_ctrl_buf` against NULL. Safe. ### 3. Memory Safety -- No memory leaks. The only allocations happen at startup (JACK ports, thread creation). No `free` of static buffers. -- The FIFO reader uses a stack‑allocated `char line[256]` – safe. -- The SPSC queue buffer is a static global – no dynamic allocation. +- All `loop_buffer` arrays and command queue buffers are static globals. No heap allocation in RT context. +- Port unregistration is deferred until after the RT thread has surely passed the `active=0` check (via `global_rt_cycles`). No use‑after‑unregister possible. +- The FIFO reader thread uses a stack‑allocated line buffer – safe. +- No memory leaks are present. ### 4. Thread Safety / Race Conditions -- The SPSC queue is correctly implemented with atomic ordering. Producer (MIDI handler, FIFO thread) and consumer (RT callback) are single‑writer, single‑reader. -- The `channels` struct fields `state`, `active` are atomic – correct. `prev_state` is plain `int` but accessed only from the RT callback (single thread) – safe. -- The `control_key_active` flag is atomic and used correctly. -- The main loop (`looper_process_commands`) runs in the non‑RT main thread and reads/writes `channels[idx].audio_in`, `channels[idx].audio_out` after verifying `active == 0`. This is safe because the RT callback skips inactive channels. -- **Potential time‑of‑check/time‑of‑use**: When `looper_process_commands` calls `channel_remove`, it sets `active = 0` and marks `pending_unregister_idx`. In the next iteration, it calls `jack_port_unregister`. Meanwhile, the RT callback could have just loaded `active == 1` and then the port pointers become invalid? No – because the RT callback checks `atomic_load(&channels[c].active)` and if it sees `1`, it uses the port pointers. If the main thread sets `active = 0` and then later unregisters, the RT thread might have already passed the check and is about to use the port pointer – that would be a use‑after‑unregister. **This is a real race.** The main loop waits one cycle (50 ms) before unregistering, but the RT thread can still be in the middle of a process cycle when `active` is set to 0. The window is narrow but possible. A safer approach would be to **not unregister ports while the RT thread could be using them** – for example, use a double‑buffer or delay unregistration by at least one JACK period using a `jack_ringbuffer` or an atomic counter. Currently, it is not 100% safe. **Consider this a moderate race condition.** +- **RT‑safe commands queue (`cmd_queue`)** – single writer (MIDI handler, called from RT callback) and single reader (the same callback, immediately after writing). Correct. +- **Add/remove command queues** – two separate SPSC queues: + - `cmd_queue_main_midi`: written only by the RT callback (via `midi_handle_events`). + - `cmd_queue_main_fifo`: written only by the FIFO reader thread (non‑RT). + Both are read only by the main loop (single consumer). No concurrent writes to the same queue. +- `global_rt_cycles` is incremented with `memory_order_release` at the end of every process callback. The main loop reads it with implicit acquire. This ensures visibility of the store to `active` and prevents unregistering ports while the RT thread may still be using them. +- `channel_add()` and `channel_remove()` are called only from the main loop, never from the RT callback. The RT callback reads `active`, `state`, `audio_in`, `audio_out` (all atomic). Safe. +- `prev_state` is a plain `int` but written and read only from the RT callback – no data race. ### 5. Performance -- The RT callback is lean: one queue drain, then per‑channel audio processing with simple state‑machine branches. No syscalls, no allocations. -- The only potential performance bottleneck is the per‑sample `fabsf()` in the test client – not in the looper itself. Looper’s performance is fine. +- The RT callback performs: + 1. MIDI event processing (may push to `cmd_queue` and `cmd_queue_main_midi`). + 2. Drain `cmd_queue` (O(1) per command, usually 0–2 commands). + 3. Per‑channel audio processing (linear buffer copy or playback). + 4. MIDI clock event handling (rare). + 5. Increment `global_rt_cycles` (atomic store). +- No syscalls, no locks, no `printf` in the RT path. +- The main loop runs at 50 ms intervals; draining two queues is negligible. ### 6. Architectural Soundness -- The separation into MIDI handler (producer), RT callback (consumer), and main loop (housekeeping) is sound. The command queue is a good abstraction. -- Inconsistency: add/remove uses atomic flags; other commands use the queue. This is a minor design smell but works for now. Future unification would be beneficial. -- The FIFO reader thread is correctly detached and won't block shutdown (but if the looper exits, the thread remains until the pipe is closed – acceptable). -- The test file is overly ambitious and seems to have been written before the code – it tests features that are not implemented (like the control‑key modifier with note numbers that were never assigned to those commands in the original specification). This may reflect a misunderstanding between the test author and the code author. +- The design cleanly separates RT‑safe command handling (immediate in the process callback) from non‑RT operations (deferred to the main loop). +- Each input source (MIDI, FIFO) has its own dedicated SPSC queue for commands that must be processed outside the RT thread. This avoids the multi‑producer race that existed before. +- All commands are represented by a uniform `command_t` structure with a typed enum. No ad‑hoc atomic flags remain. +- The FIFO pipe reader runs in a detached thread – simple and non‑blocking. +- The code is easy to extend: adding a new input source (e.g., network socket) would involve creating a new SPSC queue and another drain loop. ## Overall Verdict -The code is **functional and safe for basic use** (single‑channel looping, add/remove channels, FIFO control). It has a **minor race condition** when removing channels (use‑after‑unregister risk) and a **moderate inconsistency** between atomic flags and command queue. The **test suite is unreliable** because it expects a mapping that does not match the code’s actual note assignments in some scenarios. No segfaults, no memory leaks, good performance. +The code is **safe, race‑free, and architecturally sound**. It meets all real‑time constraints and correctly implements the looper’s state machine with unified command handling from MIDI and a FIFO pipe. -**Recommendations:** -- Fix the race in channel removal by using a ringbuffer or ensuring the RT thread has completed at least one cycle after marking `active = 0` before unregistering. -- Unify all commands (including add/remove) into the command queue for consistency. -- Update the test suite to match the actual note mapping and to test the FIFO pipe. +**Minor remaining items:** +- The FIFO pipe is untested in the integration suite. +- `CMD_STOP` is not triggered from MIDI (only from FIFO). +- The existing `evaluation.md` is outdated and should be replaced with this evaluation. -- 2.49.1 From c592c24634b80216e7c0a72191363f95c8074882 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 23:56:09 +0000 Subject: [PATCH 10/17] feat: add MIDI stop command and FIFO pipe integration test Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/midi.c | 5 +++ tests/integration.c | 87 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/midi.c b/src/midi.c index 634e4d3..4395550 100644 --- a/src/midi.c +++ b/src/midi.c @@ -63,6 +63,11 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { command_t cmd = { .type = CMD_UNBIND, .channel = -1, .data = 0 }; queue_push(&cmd_queue, cmd); } break; + case 65: + { + command_t cmd = { .type = CMD_STOP, .channel = -1, .data = 0 }; + queue_push(&cmd_queue, cmd); + } break; default: break; } diff --git a/tests/integration.c b/tests/integration.c index 1d4eea2..6c83ff3 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -836,6 +836,87 @@ static int test_remove_channel(void) { } +/* test FIFO pipe */ +static int test_fifo_pipe(void) { + printf("Test: FIFO pipe add/remove\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_fifo", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + + /* write "add\n" to the FIFO */ + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "add\n", 4); + close(fd); + + safe_usleep(1500000); /* give main loop time to process */ + + const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); + int found = 0; + if (ports) { + for (int i = 0; ports[i]; i++) { + if (strstr(ports[i], "looper:channel1_input")) { + found = 1; + break; + } + } + jack_free(ports); + } + + /* Write "remove\n" to the FIFO */ + fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "remove\n", 7); + close(fd); + + safe_usleep(1500000); + + ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); + int still_found = 0; + if (ports) { + for (int i = 0; ports[i]; i++) { + if (strstr(ports[i], "looper:channel1_input")) { + still_found = 1; + break; + } + } + jack_free(ports); + } + + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + + if (!found) { + fprintf(stderr, " FAIL: channel not added via FIFO\n"); + return 1; + } + if (still_found) { + fprintf(stderr, " FAIL: channel not removed via FIFO\n"); + return 1; + } + printf(" PASS (FIFO add/remove works)\n"); + return 0; +} + int main(void) { /* 1. binary must exist */ if (system("test -x ./looper") != 0) { @@ -886,6 +967,12 @@ int main(void) { failures++; } + /* 10. Test FIFO pipe */ + if (test_fifo_pipe() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + if (failures > 0) { fprintf(stderr, "%d test(s) FAILED\n", failures); return 1; -- 2.49.1 From be3188bbe23458f3feb4d17a9ae47f05e9edf654 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 00:16:03 +0000 Subject: [PATCH 11/17] fix: keep FIFO fd open across both writes to prevent hang Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/integration.c b/tests/integration.c index 6c83ff3..1091027 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -860,8 +860,7 @@ static int test_fifo_pipe(void) { return 1; } write(fd, "add\n", 4); - close(fd); - + /* Keep fd open; do NOT close yet */ safe_usleep(1500000); /* give main loop time to process */ const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); @@ -876,14 +875,7 @@ static int test_fifo_pipe(void) { jack_free(ports); } - /* Write "remove\n" to the FIFO */ - fd = open("/tmp/looper_cmd", O_WRONLY); - if (fd < 0) { - perror("open fifo"); - jack_client_close(client); - kill(pid, SIGTERM); waitpid(pid, NULL, 0); - return 1; - } + /* Write "remove\n" to the FIFO, same fd */ write(fd, "remove\n", 7); close(fd); -- 2.49.1 From 011d29cb09b9c54193c56a4b2e36680eb38fb0df Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 00:21:57 +0000 Subject: [PATCH 12/17] docs: update evaluation.md with final code review Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 100 +++++++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/evaluation.md b/evaluation.md index 10de4e6..d4a9497 100644 --- a/evaluation.md +++ b/evaluation.md @@ -2,66 +2,76 @@ ## Summary Table -| Category | Rating | Remarks | -|--------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Mocked / Left Undone | ✅ Minor | `CMD_STOP` is only reachable via the FIFO pipe (`"stop"`). The MIDI handler never sends it. This is an unused path, not a bug. The FIFO pipe is still untested in the integration suite – all tests use MIDI. No test for `"stop"` via FIFO. Everything else is implemented and matches the test suite. | -| Potential Segfaults | ✅ Good | All `jack_port_get_buffer()` calls are guarded against NULL. Array bounds respected (fixed‑size loops). SPSC queues use modulo arithmetic, no overrun possible. No dynamic memory in RT path. | -| Memory Safety | ✅ OK | No leaks, no use‑after‑free. All buffers static. Deferred port unregistration waits for at least one RT cycle after `active=0` – safe. The FIFO reader thread uses stack memory for line reading. | -| Thread Safety / Race | ✅ Good | SPSC queues have correct `acquire`/`release` ordering. Multi‑producer issue on main‑loop commands has been fixed by giving each producer its own queue (`cmd_queue_main_midi` for MIDI, `cmd_queue_main_fifo` for FIFO). The RT callback only writes to `cmd_queue_main_midi`; the FIFO thread only writes to `cmd_queue_main_fifo`. Both are consumed solely by the main loop, restoring SPSC safety. The deferred unregistration race is now fixed via `global_rt_cycles` counter – the main loop ensures the RT thread has completed a cycle after `active=0` before calling `jack_port_unregister()`. `prev_state` is a plain `int` but accessed only from the RT callback (single thread). All other shared state (`state`, `active`, `control_key_active`, `bind_channel`) uses atomics. | -| Performance | ✅ Good | No syscalls, locks, or dynamic allocations in the RT callback. Two queue drains (one for RT commands, one for main‑loop commands) add negligible overhead. O(1) queue operations. Linear audio processing. | -| Architectural Soundness | ✅ Good | Clean separation: each input source has its own SPSC queue for non‑RT commands; RT callback drains its own queue; main loop drains both auxiliary queues. The command queue approach is now fully uniform (no atomic flags remaining for add/remove). The FIFO pipe works in parallel. The code is easily extensible to new input sources. | +| Category | Rating | Remarks | +|--------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Mocked / Left Undone | ✅ Everything implemented | `CMD_STOP` is now sent from MIDI (note 65) and from FIFO (`"stop"`). FIFO pipe add/remove test is in the integration suite. All command types are wired to both sources. No missing paths. | +| Potential Segfaults | ✅ Good | Every `jack_port_get_buffer()` call is null‑checked. Array bounds respected (`MAX_CHANNELS`, `QUEUE_CAPACITY`). No `malloc`/`free` in RT path. The only unguarded `jack_port_get_buffer()` is in `midi_handle_events` where the caller already verified the buffer pointer – safe. | +| Memory Safety | ✅ OK | All buffers static, no dynamic allocation. Deferred port unregistration waits for at least one RT cycle after `active=0` (via `global_rt_cycles`), preventing use‑after‑unregister. FIFO reader uses stack‑allocated line buffer. No leaks. | +| Thread Safety / Race | ✅ Good | Three SPSC queues, each with a single producer: `cmd_queue` (MIDI handler only), `cmd_queue_main_midi` (MIDI handler only), `cmd_queue_main_fifo` (FIFO thread only). All consumers are single‑threaded (RT callback or main loop). Atomic ordering correct (`acquire`/`release`). `global_rt_cycles` prevents RT‑thread‑still‑using‑port race. All shared state (`state`, `active`, `control_key_active`, `bind_channel`) uses atomics. `prev_state` is a plain `int` but accessed only from the RT callback – safe. | +| Performance | ✅ Good | No syscalls, locks, or allocations in RT callback. O(1) queue operations. Linear audio processing. The RT callback drains `cmd_queue` (usually 0–2 commands), processes per‑channel audio, and handles MIDI clock events. The main loop runs every 50 ms and drains two auxiliary queues – negligible overhead. | +| Architectural Soundness | ✅ Good | Clean separation: each input source has its own SPSC queue for non‑RT commands. RT callback performs only RT‑safe operations; main loop handles channel add/remove. All commands use a uniform `command_t` enum. The code is easily extensible – adding another input source (e.g., UDP socket) requires only a new SPSC queue and a drain loop. | ## Detailed Remarks ### 1. Mocked / Left Undone -- `CMD_STOP` is only reachable via the FIFO pipe (`"stop"`). The MIDI handler never sends it. This is not a bug, just an unused feature path. -- The FIFO pipe is completely untested in the main integration suite. All existing tests use MIDI notes. Adding a FIFO‑based test would increase coverage. -- No other functionality is missing. +- **Nothing remaining.** + - `CMD_STOP` is now sent by MIDI (note 65, control‑key section) and recognised by FIFO (`"stop"`). + - FIFO pipe add/remove is tested in `test_fifo_pipe()`. + - All other command types (`CYCLE`, `BIND`, `UNBIND`, `ADD_CHANNEL`, `REMOVE_CHANNEL`) are available from both MIDI and FIFO. ### 2. Potential Segfaults -- Every `jack_port_get_buffer()` call is followed by a null check (`if (!out) continue;`). -- Array accesses are bounded by `MAX_CHANNELS` and `QUEUE_CAPACITY`. -- No use of `malloc` or variable‑length arrays in the real‑time callback. -- The only unguarded `jack_port_get_buffer()` is in `midi_handle_events` where the caller already checks `midi_ctrl_buf` against NULL. Safe. +- Every `jack_port_get_buffer()` is followed by a null check. +- No array overruns: loops over `MAX_CHANNELS` (16) and `QUEUE_CAPACITY` (256). +- No dynamic memory in RT context. +- The only unchecked `jack_port_get_buffer()` is in `midi_handle_events` – the caller already ensures `midi_ctrl_buf` is not NULL. ### 3. Memory Safety -- All `loop_buffer` arrays and command queue buffers are static globals. No heap allocation in RT context. -- Port unregistration is deferred until after the RT thread has surely passed the `active=0` check (via `global_rt_cycles`). No use‑after‑unregister possible. -- The FIFO reader thread uses a stack‑allocated line buffer – safe. -- No memory leaks are present. +- All `loop_buffer` arrays and command queue buffers are static global arrays – no heap allocation. +- Port unregistration is deferred until `global_rt_cycles` has advanced by at least 1 after marking `active=0`. This guarantees the RT thread has started a new cycle after seeing `active=0`, so it will not dereference the port pointers after they are unregistered. +- FIFO reader thread uses a stack‑allocated `char line[256]` – safe. +- No memory leaks exist. ### 4. Thread Safety / Race Conditions -- **RT‑safe commands queue (`cmd_queue`)** – single writer (MIDI handler, called from RT callback) and single reader (the same callback, immediately after writing). Correct. -- **Add/remove command queues** – two separate SPSC queues: - - `cmd_queue_main_midi`: written only by the RT callback (via `midi_handle_events`). - - `cmd_queue_main_fifo`: written only by the FIFO reader thread (non‑RT). - Both are read only by the main loop (single consumer). No concurrent writes to the same queue. -- `global_rt_cycles` is incremented with `memory_order_release` at the end of every process callback. The main loop reads it with implicit acquire. This ensures visibility of the store to `active` and prevents unregistering ports while the RT thread may still be using them. -- `channel_add()` and `channel_remove()` are called only from the main loop, never from the RT callback. The RT callback reads `active`, `state`, `audio_in`, `audio_out` (all atomic). Safe. -- `prev_state` is a plain `int` but written and read only from the RT callback – no data race. +- **Three SPSC queues, each with a single writer and single reader:** + - `cmd_queue` – writer: `midi_handle_events` (called from RT callback), reader: same RT callback (immediately after writing). + - `cmd_queue_main_midi` – writer: RT callback (via `midi_handle_events`), reader: main loop. + - `cmd_queue_main_fifo` – writer: FIFO reader thread, reader: main loop. +- All queue operations use correct `memory_order_acquire`/`release` – no data races. +- `global_rt_cycles` is incremented with `memory_order_release` at the end of every process callback. The main loop reads it with implicit acquire (via `atomic_load`). The condition `current_cycle - pending_unregister_cycle >= 1` ensures the RT thread has finished a cycle after `active=0` before port unregistration. +- `channel_add()` and `channel_remove()` are called only from the main loop. The RT callback reads `active`, `state`, `audio_in`, `audio_out` – all atomic. No concurrent modification. +- `prev_state` is a plain `int` but only accessed from the RT callback – safe. ### 5. Performance -- The RT callback performs: - 1. MIDI event processing (may push to `cmd_queue` and `cmd_queue_main_midi`). - 2. Drain `cmd_queue` (O(1) per command, usually 0–2 commands). - 3. Per‑channel audio processing (linear buffer copy or playback). - 4. MIDI clock event handling (rare). - 5. Increment `global_rt_cycles` (atomic store). -- No syscalls, no locks, no `printf` in the RT path. -- The main loop runs at 50 ms intervals; draining two queues is negligible. +- The RT callback performs in order: + 1. MIDI event processing (may push to `cmd_queue` and `cmd_queue_main_midi`). + 2. Drain `cmd_queue` (usually empty or 1 command). + 3. Per‑channel audio processing (linear buffer copy or playback, no conditionals for common state). + 4. MIDI clock events (rare). + 5. Increment `global_rt_cycles`. +- No syscalls, no locks, no `printf` in the RT path. +- The main loop sleeps 50 ms between iterations; draining two queues adds negligible overhead. ### 6. Architectural Soundness -- The design cleanly separates RT‑safe command handling (immediate in the process callback) from non‑RT operations (deferred to the main loop). -- Each input source (MIDI, FIFO) has its own dedicated SPSC queue for commands that must be processed outside the RT thread. This avoids the multi‑producer race that existed before. -- All commands are represented by a uniform `command_t` structure with a typed enum. No ad‑hoc atomic flags remain. -- The FIFO pipe reader runs in a detached thread – simple and non‑blocking. -- The code is easy to extend: adding a new input source (e.g., network socket) would involve creating a new SPSC queue and another drain loop. +- The design is clean and consistent: + - All commands flow through a `command_t` struct. + - Each input source has its own SPSC queue for commands that must be processed outside the RT thread (e.g., add/remove). + - The RT callback handles only RT‑safe state transitions (cycle, stop, bind, unbind). + - The main loop handles add/remove and deferred port unregistration. +- The FIFO pipe reader runs in a detached thread – simple and non‑blocking. +- Adding a new input source (e.g., a network socket) would require: + - Creating a new SPSC queue. + - A producer thread that pushes commands to the appropriate queue. + - Adding a drain loop in `looper_process_commands()`. ## Overall Verdict -The code is **safe, race‑free, and architecturally sound**. It meets all real‑time constraints and correctly implements the looper’s state machine with unified command handling from MIDI and a FIFO pipe. +The code is **complete, race‑free, memory‑safe, and architecturally sound**. -**Minor remaining items:** -- The FIFO pipe is untested in the integration suite. -- `CMD_STOP` is not triggered from MIDI (only from FIFO). -- The existing `evaluation.md` is outdated and should be replaced with this evaluation. +- No missing features. +- No segfaults or use‑after‑free. +- All input sources (MIDI, FIFO) can send any command. +- The unified command‑queue architecture is fully realised. + +The only minor observation is that the test suite does not verify the MIDI `CMD_STOP` (note 65) – but that would be trivial to add. + +**Final note:** The evaluation file itself (`evaluation.md`) should be updated to remove the “FIFO untested” and “CMD_STOP not triggered” remarks. The content above can replace it. -- 2.49.1 From 98c851f05128f061c0c3cb5712f0d65942e28034 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 00:37:21 +0000 Subject: [PATCH 13/17] test: add MIDI stop and full record-loop-stop integration tests Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 213 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/tests/integration.c b/tests/integration.c index 1091027..154d26e 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -909,6 +909,207 @@ static int test_fifo_pipe(void) { return 0; } +/* test stop via MIDI (control key + note 65) */ +static int test_stop_midi(void) { + printf("Test: MIDI stop (note 65 under control key)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_stop", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + jack_port_t *audio_out = jack_port_register(client, "out", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + jack_port_t *audio_in = jack_port_register(client, "in", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + if (!audio_out || !audio_in) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_stop:out"); + snprintf(my_in, sizeof(my_in), "test_stop:in"); + if (jack_connect(client, my_out, "looper:input") || + jack_connect(client, "looper:output", my_in)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + /* start recording: send note 1 */ + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note1 failed\n"); + return 1; + } + safe_usleep(200000); + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.2f * sr); /* 0.2s beep while recording */ + bursts = 0; + prev_above = 0; + passthrough_output_port = audio_out; + passthrough_input_port = audio_in; + passthrough_phase = 0.0f; + passthrough_freq = 440.0f; + passthrough_sample_rate = sr; + passthrough_total_samples = 0; + passthrough_sum_sq = 0.0; + passthrough_done = 0; + jack_set_process_callback(client, passthrough_process, NULL); + if (jack_activate(client)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(150000); + /* loop: send note 1 again */ + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: loop note1\n"); + return 1; + } + safe_usleep(500000); + /* stop: control key then note 65 */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: control key\n"); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 65, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: stop note 65\n"); + return 1; + } + safe_usleep(200000); + int bursts_before = bursts; + safe_usleep(500000); + int bursts_after = bursts; + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (bursts_after > bursts_before) { + fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n", + bursts_before, bursts_after); + return 1; + } + printf(" PASS (stop stopped playback)\n"); + return 0; +} + +/* full flow: record 1s, loop 5 times, stop, verify at least 5 bursts */ +static int test_record_loop_stop(void) { + printf("Test: full record‑loop‑stop (≥5 repetitions)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_full", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + jack_port_t *audio_out = jack_port_register(client, "out", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + jack_port_t *audio_in = jack_port_register(client, "in", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + if (!audio_out || !audio_in) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_full:out"); + snprintf(my_in, sizeof(my_in), "test_full:in"); + if (jack_connect(client, my_out, "looper:input") || + jack_connect(client, "looper:output", my_in)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + /* start recording */ + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note1\n"); + return 1; + } + safe_usleep(500000); + /* generate a 0.5 s beep while recording */ + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.5f * sr); + bursts = 0; + prev_above = 0; + passthrough_output_port = audio_out; + passthrough_input_port = audio_in; + passthrough_phase = 0.0f; + passthrough_freq = 440.0f; + passthrough_sample_rate = sr; + passthrough_total_samples = 0; + passthrough_sum_sq = 0.0; + passthrough_done = 0; + jack_set_process_callback(client, passthrough_process, NULL); + if (jack_activate(client)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + /* end recording -> loop */ + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: loop note1\n"); + return 1; + } + /* wait for about 5 loops (assuming 0.5s recorded -> ~2.5s loop) */ + safe_usleep(2500000); + /* stop via control+65 */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: control key\n"); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 65, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: stop note 65\n"); + return 1; + } + safe_usleep(200000); + int total_bursts = bursts; + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (total_bursts < 5) { + fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts); + return 1; + } + printf(" PASS (≥5 repetitions, stopped cleanly)\n"); + return 0; +} + int main(void) { /* 1. binary must exist */ if (system("test -x ./looper") != 0) { @@ -965,6 +1166,18 @@ int main(void) { failures++; } + /* 11. Test MIDI stop */ + if (test_stop_midi() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + /* 12. Test full record‑loop‑stop flow */ + if (test_record_loop_stop() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + if (failures > 0) { fprintf(stderr, "%d test(s) FAILED\n", failures); return 1; -- 2.49.1 From 900619a71416ae0447e15ba08b62235dc9b2b6e4 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 01:08:11 +0000 Subject: [PATCH 14/17] 12-command-art --- src/channel.o | Bin 0 -> 9640 bytes src/looper.c | 66 +++++++++++++++++------------- src/looper.o | Bin 0 -> 22328 bytes src/main.o | Bin 0 -> 9984 bytes src/midi.c | 50 +++++++++++------------ src/midi.o | Bin 0 -> 8552 bytes src/pipe.c | 108 +++++++++++++++++++++++++------------------------- src/pipe.o | Bin 0 -> 10560 bytes src/queue.c | 37 ++++++++--------- src/queue.o | Bin 0 -> 5952 bytes 10 files changed, 134 insertions(+), 127 deletions(-) create mode 100644 src/channel.o create mode 100644 src/looper.o create mode 100644 src/main.o create mode 100644 src/midi.o create mode 100644 src/pipe.o create mode 100644 src/queue.o diff --git a/src/channel.o b/src/channel.o new file mode 100644 index 0000000000000000000000000000000000000000..72477e157ecd441c4010799e2e0f8dd8e3ac1a48 GIT binary patch literal 9640 zcmbuF30RCR=jlD?{LcUXKmYST%bfRRCDVJR42?#h zL?fmX-)e{wME96)Wml@`N{k{#5(`=4N368oS`x9ur&$971L9+z&yHz{hSQS^cGfZt;Dj#qZ~O+Rz|I8 z%>Qgo62O7x$W!hhuP%Qd47`VYO=<`d9oiLEdaWEwoDoBgupz^rd;(iJR7qS%jtMkm zX+AX}1M-76gPRKYY6=8V$`okG()_`SWXPl^;*9XGs+bp`VJOuGIZd*4U~s!+qr}u; zasEe*RG`vpX=19-kl{o2B^ekf^$j~qaWNG!Ru1z6hc!{MZbkCc>S;Vjt?=|-21}fJ zOPq8AU)anWtZ(x_Xh@lT4a{C@7|dQUDa^hs%?4&Smy~2GY!Y>&I``cxMp!N{RUIX zy<02flxUcf=Rn?jvwaz<0|PGxzWjA+>-70bivzrBC$F93w&SAL%NH*`S#7v<^|;~K zdzC(xmU*dGYjPjlyFmYwvBcH;SWpBH@)MEye z24?FS9or zzD;9rmM^=c*EDuSn&-J;uDe~5GhbKSvzX(2O`+56#K9H^<83ho z>*%UxH*;d+Yg3{}2aoc(`|jQTFuxb)rfL4J6Sd&4@L!a(q8(ZJ99@%()@s^1dNY+$ zbTx**u`w{y<(O+5upPa8*p4$3GTFIZCg+kJPlVPnmP zaw6@$?mYYHgmpv0@g$}59+`>a>vSXQ?R&P%XR3;|YE8~@UYle$s1RCf5F*`TMh7C9SE- z50BW^M2_$Kbg($}lH>AyTbLIs>U4r1Mienb7HjUWQO@nw`gCGv>DvQirjMHaDv7N+ z?ZD9+Wlua8Pfp!jcWBWDo1LbqORG&Ex8K`nrcv`-k?x#?$y}{79c7F2_N>@9uh;b4 zTPOPkrp<--I-EA;OjO%=Z^~M|3#}hFS_HPfO5gva@uXv(+KHagRb4YfJf}Vg>bIMk z_Hg^9>I>%MZ@=#QkIRXk4+qIgsLsm?SM1GIUn;iWOy7Xk{b^W0_2lg#zL)&!7@GZ? z?TV`%d{$pk3bNgGfA(YpSG~T+ZvuajHtw#i>UZc)GgI ztCqJk$^0azvHZcR-a!93>|-;xUvjFJId2x0e$`mFRIzy*tg5ctUh2M?4ZuL zfpqUJCGrW+jX#x{x&$?hzb~*U552Kow6h`s2&lx7R4QyO}#?EKi7n%2m+$`=Zo;f>?Z%Lj5;1`l0 zl&B|qaAi^H#~j5?>4!K*vN_@5+^+D85#izC9M0dD8dOVk-JB~|IZu-{sy%Psa=xXW z@Krb6;wopG9iEnRs7~3T(sKWB+40)VnzPqwEqWa7eY@^}jrVAi1Ac1Wmbu=e-47f# zPz@+OYp~FGkuN=KZKTY88jHSqp^+!8>cD#M2_*-bz5j~v=F`92tTXs?>e3@kv_qN= zMvKz+(39d_Rdw611?2a>IOsB59{=B}Dxy^Z$ zpTZ2;Y{MxE_U4W2Dl~#uxSH`gV>mt)T{XWhIUKg6x9w)e?Y6R-IWm8XvPa zpIH8%EmQs&Yos1`^pSYK=acNU?TbFHcieGB%aB*U<1HgAcUMVT(zca6uj$1juC6zC zI$rjqu)MPE#GfZjPgy?MUnn2bacD+o@+T(NK~>J;C3{v@xxzPGIU*5Q~7+lv#EcTV9dbS-IG|KjysxjSAu zS&V`R^8)L~!&iCQFP%N`+PvN0vPXO1fsauZ+tH!(dM;DjrRk|slYaFgy?@QWyl3Fz z0>-KO(58mgpF(ELcxdo!W7b$(m)5}g{3yNY8$I)#`+Vb1e}3h)+i1qY)>%4ja*kSC z+tuGZm|LnJ;h#B>`{K+eo$#JN-?u%A@U+e2y>eKbF*DX~^VurBFQZF~6pmgg7~{Rw zeOlqFf(4e{^1brrW0tp-eeCOgyffmmxvKjg_cR^v(C%jOFJIbVzoyse;7SX__%5eJ zm6(8S$Io1$*@ z+urLdw`iBf%?o#__BwUMbWG#kzL#c~mon&ezid9}d?>p)Ff7{PgNnQGZ2h}89m-ts zr(2!9>C4`)`T2G0l9YvYob`Wi4vH-gxZU&o*W#YBI>*vPhTgB_wjB!%BxZSdIMK~# z&0APX2*w7b!)aBeYt;&^T#WU59J~iI(C=(2_N2 z`f7?w`J}J`$-o~-_rYQ0rlFZBgFOf(TakxSu5d}q&dZZwUFCxOi5=B5a zGA1r8io8rCHPQ}Gu#^j20m(v=QNNGiGW|Drxgir={uBp4lZelQ@?Z~uxkKC^aRb<4E2`ic81AQ;Gu*k^gg~IEW8A1A-w3eh!n8PnMDg z{vbQ&hTt;5aWWzQk-Q7WB~kjL>Ck5=pbm#Z$xWu75_#!U*FlPde$o87V0=Ctk0-{tu#-6$kAU)i7}td33dA^) zi^q5%l>Zsyj?g~ZzbJ0kAQxlud61)hjpW}#|0^(gv|f*6+y?sdZ;ZP_-iGlE$U8AE z19>;bOJV&5#!aF88;qZVToLAITxh=4G5!gT7u}bTJX+s+n7lPuz+@VO@x#y#@(bA+ z5BWq)J`38h!#G;6(=cuU=L^LZ*)N51L74n6(0(|^!CeTXD2#VNKjSejg8e39JOb9! zF+M|LoH6iLvdmb#?4?u0LF75N9PB!Uk~Mkm^|8VD>1GL$CZomZLq!-CbWM6sl%dKbq1U>u%fd>^#`8RPL#UJ>SKehXoJ7{<|lu*LWQ zwCjTL<(2g9u3?X^y z@=ofFfE>jq3&wLS#vegH9WkyCe)A!Bf;qCk7uG#7j^68LWBd%P zqyCYdM##bYJ4lFMl~joUm?Pc}>me9NagM+^y8lIE{0pp0-;=?3<=}dgz9$2&2Kfr8 zhtC%sa=MC=GM7YQYK3@Qn^@oXy*`DKBy;;=<*!YU#$k+2d9 zBo(Ta!;MIaCjW^pN0Fxgc$aJ2l{_zbW$J07VQ^g z&J`B$Z~t&wkQnto2lf||M-4v1U) z|H|Ya>L2+J_WfY~$3Q`Jf0kJLrjP32JQ=Ki2J9c*3qk*?NFnvw #include #include @@ -9,8 +11,6 @@ #include #include #include -#include "command.h" -#include "queue.h" /* Global state (shared across files) */ struct channel_t channels[MAX_CHANNELS]; @@ -30,38 +30,48 @@ static int pending_unregister_idx = -1; static int pending_unregister_cycle = 0; static void apply_command(command_t cmd) { - switch (cmd.type) { - case CMD_CYCLE: - if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) { - int cur = atomic_load(&channels[cmd.channel].state); - int next; - switch (cur) { - case STATE_IDLE: next = STATE_RECORD; break; - case STATE_RECORD: next = STATE_LOOPING; break; - case STATE_LOOPING: next = STATE_PAUSED; break; - case STATE_PAUSED: next = STATE_LOOPING; break; - default: next = STATE_IDLE; break; - } - atomic_store(&channels[cmd.channel].state, next); - } + switch (cmd.type) { + case CMD_CYCLE: + if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) { + int cur = atomic_load(&channels[cmd.channel].state); + int next; + switch (cur) { + case STATE_IDLE: + next = STATE_RECORD; break; - case CMD_STOP: - if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) - atomic_store(&channels[cmd.channel].state, STATE_IDLE); - else { - for (int i = 0; i < MAX_CHANNELS; i++) - atomic_store(&channels[i].state, STATE_IDLE); - } + case STATE_RECORD: + next = STATE_LOOPING; break; - case CMD_BIND_CHANNEL: - atomic_store(&bind_channel, cmd.data); + case STATE_LOOPING: + next = STATE_PAUSED; break; - case CMD_UNBIND: - atomic_store(&bind_channel, 0); + case STATE_PAUSED: + next = STATE_LOOPING; break; - default: + default: + next = STATE_IDLE; break; + } + atomic_store(&channels[cmd.channel].state, next); } + break; + case CMD_STOP: + if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) + atomic_store(&channels[cmd.channel].state, STATE_IDLE); + else { + for (int i = 0; i < MAX_CHANNELS; i++) + atomic_store(&channels[i].state, STATE_IDLE); + } + break; + case CMD_BIND_CHANNEL: + atomic_store(&bind_channel, cmd.data); + break; + case CMD_UNBIND: + atomic_store(&bind_channel, 0); + break; + default: + break; + } } /* ---------------------------------------------------------------- diff --git a/src/looper.o b/src/looper.o new file mode 100644 index 0000000000000000000000000000000000000000..026da08b44037a5415a1b6888e6a5291c127a2c7 GIT binary patch literal 22328 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 literal 0 HcmV?d00001 diff --git a/src/main.o b/src/main.o new file mode 100644 index 0000000000000000000000000000000000000000..fd3175c3b29da921af0b722dc9e5c3f8d5431b84 GIT binary patch literal 9984 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 UhufWfFiWmZ;e8v0^sW7W07Gp-0{{R3 literal 0 HcmV?d00001 diff --git a/src/midi.c b/src/midi.c index 4395550..6cfddaf 100644 --- a/src/midi.c +++ b/src/midi.c @@ -36,36 +36,34 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { if (ck) { atomic_store(&control_key_active, 0); if (note < 16) { - command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = note }; + command_t cmd = { + .type = CMD_BIND_CHANNEL, .channel = -1, .data = note}; queue_push(&cmd_queue, cmd); } else { switch (note) { - case 60: - { - command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + case 60: { + command_t cmd = { + .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; - case 61: - { - command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + case 61: { + command_t cmd = { + .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; - case 62: - { + case 62: { int bch = atomic_load(&bind_channel); if (bch >= 0 && bch < MAX_CHANNELS) { - command_t cmd = { .type = CMD_CYCLE, .channel = bch, .data = 0 }; + command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0}; queue_push(&cmd_queue, cmd); } } break; - case 63: - { - command_t cmd = { .type = CMD_UNBIND, .channel = -1, .data = 0 }; + case 63: { + command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0}; queue_push(&cmd_queue, cmd); } break; - case 65: - { - command_t cmd = { .type = CMD_STOP, .channel = -1, .data = 0 }; + case 65: { + command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0}; queue_push(&cmd_queue, cmd); } break; default: @@ -75,19 +73,17 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { } else { /* direct mapping */ switch (note) { - case 1: - { - command_t cmd = { .type = CMD_CYCLE, .channel = 0, .data = 0 }; - queue_push(&cmd_queue, cmd); - } break; - case 60: - { - command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; + case 1: { + command_t cmd = {.type = CMD_CYCLE, .channel = 0, .data = 0}; + queue_push(&cmd_queue, cmd); + } break; + case 60: { + command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; - case 61: - { - command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; + case 61: { + command_t cmd = { + .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; default: diff --git a/src/midi.o b/src/midi.o new file mode 100644 index 0000000000000000000000000000000000000000..c89e0e577a535ed7cb554f0b0ddfd4aaa1c0c043 GIT binary patch literal 8552 zcmbuD3p|urAIBf}YsRH$i>OVJ8l>EbdPftnA+0XktjoxCh#_o>P$T40(MGLGrBrm; zRu@7Ss&y$6X=Brcq{3U-?czOW<{YN8=JW2p@A-V@Ip_a9zyJUIpYxpOJkK+m8A~0+ z#l$FF#3);8)WVdaiYAY??f6YQN`q3TJeVm@nTglpnJIC+l*lAsW~!vF9_VMLFoujP znW+pOGo_{*I;1f2YZ!ZAt7oqSMWuS|VWv9snCXm8W=cI4C0Ejtxu6#~b~6))#A#at z1lAkcd5Xu_4LE4oiSZaYs4324=;ftK7SPEql3xfCs_i{Hwzi=4(x zmx5!1yeexL&F~ac8O>o6!FwVV{jLuRT6i1m<}J`VaYQ(C~tDGmI2>EQu6lqC+? z(~@k!Ah40418CyV0$S2L-~byLN_=8VOWMLGwzQ0oe6clGK5@u~YuVEWOfb46=tiU0 zFdhN}%%3s#UEl*Q%RfkKkgw)n+`oS=6~IX_{WjcliB-CX7AF>-X)_qKs?Bgt3GPO8zNF4-VBjnk6y7_Fcv%! zym)K3Dfdcd_KIaDGWLk2GWJ+8@@ZREeHh1C z(b2S;v^5-N>+r|l25;RByd5qVJZ`^qR+ELy)N}hjm&+{c5i_lNKbRgdea4Eno-Rwz zAFN2}W3AtiaV)3T=afaksq^*z$}48n?kg(MFS=kF72fA&uUAmjGyXxg%aWsOEAKqz z*l%)Img3xbecW+x!<^*q>O#%Ow&!O|Rmn&-n7iWQMU80P;nT<7-I{u1S>ZlcC9&)C z2jmJ8(zm`?hwxTCJz}V$s)nZW9})pM0nxr4?B-Ma!K24Fg&gm+ybJc6woOiAMrSgZfOR60`nx3b4cEks2 zOwiyOe*u%{ViK3lOMhz>7T?o!&Gep0nnAYKvyJ&H zk7%nNiPl~bE4GUHx;Wpnd|aP*M*iBgeD50}UCpNgkA5rX-Zih~<0XBI<&C#5`!Apj?VY^<~FC=PEqsh?Ac|(vI^5z zXEr+O#0DDL{5v6j@-3M+W*a3xbv=-i4IhlQrZvq>`>^ESw>#D(yU1}{zJGMKKVIeT zFST@cmz}m!mG!%yY?X*TG;`>>-LSK|(^vV)QxqrZ8~s{&vuFG2&)dpUD{otAJ^n3V za&c+uUen`!J9mamV#Z5dEyYrh>l4 z#dGV_W7LXVo9uq2nXCD0hq)}5(%L@VR@>WEdhX|yD(!~;+k9eR;wBB+J<4s+}@p!-kJu znq8jj<<+h`E|gK-Zf=)7>!6W^UA%aT_=b7PZg(4Zs$FY5x?)-Gcps#p^{Puen+#G{GefRdTf6svZ<=uGt>*t@DwU`(+t^!H096ELnc~Rqx_sYx8|Wr!fXJ_g_uCYv}Ln zlezY$@%d{7RugpgE=W?$ns8M1imGn))ay!?)mAo%Fu<<6T}1yss~l9&StAHh4B(zy4iUz(Ts;Wv#xQyDS^H2kcw)4a{R*J0DqS z9yT9ne6(u)am(S#Hc=SH^hVPwW@$cisO2Hz>>-!~I@Yn{=sr29FtBn+s zJ}eBGSo9xPsKYv)Tpuy=)uYD~C)=QkX? z)l#=6{`|pYN&7yg$!y*m^NienUxf$EmD*7^<*#;2n{C{&Y4Ulw&}Tz2JZmR^&U|NU z_UQ+UAAPHRN~>aEiMdLYgG<1zil*-px1M@^rrN`@g4*BWYECU)w8&CNZ?TJqzK*de z-I#8yGuLRYsnI+WBal0MsnY>J__zfUumgxyLWa*rjIhUpK3K$J-6*kWWwFVMGP0RK ztO*$W!NT7$?3I+aiZ2=`EzvIlB!Z4Hj&K}Fs(*NlE=pn~i!Xa6_}PBRpd~?#Y(HOg zN>K7&*w~Pu6mav4T7u%gO@G0lAwenqg~2mZ70MIumhiJi`7dgUnlFMc5y97p;C>?b zdJ#NX1kV=1sgMBQ0J;yqV)=V6tY6R0yzBmly?Bfm*oRS z=dgn*7At}aNA~d#V)=Om1cSsunIWk7>lj{T@NUU3PC^`BWz0_?4!2cBAzm+n4~XE# z=mvpt;A+9~Z4rm}5L_*omk8-;Lj1A_{Sy)Tei8alLVCFF@VE}>`9bFCjyRd;4tT@Vu!ZPR2JE(hJWULxet1gnokveTk4>cs;xk(yM@6@qFT) z46ZepD=v7)fE?~ZW~f60)sREq3CPBOdti<*rHvZ!a-u2Z6GOzIUK+JwWs2H(+#<9N zYzY1s+5aN=d8Ehp3y#x>bZ&(H5K3w#!8wQr5nO=#Ss1pk0I-UqNg1?esl`hCc*BDg(@Q%mqFlxH2mYmxpr!8ahgkKk{S z4gY$81?Q=X;`|}NBb3L0fNs&@lO(59r23C!qL^h~xNk5PwB*3&e+sI68MjXdKhV=G? z9~L-jip%zA`GtFhu+a(spOp^qhjWS7I}T@u8u)t{ zbEp+A@I4sb(ssfSwEfYib{M)C@VyZF!nF>|L_x*>{)1fj9)n$`XrDl`k%}CDHt2yd z|5tzb{>_hx6GJ1Jqw$lzSWKh&BN=538e^S*E7~aZP+WNaaI3+?;PYPy*jVFNq6;zr z`h^Sj#|++w;;4aDdPg^Sos^F{Ns^7%wJ(N?8y2{1`JA!h`@Go0D`hl#P}Fz z0?k`;RCDjqNa5Q1E|p0&8^sus?hc!fmX9^%3MR%wU_; zUH}YUDq@*aMaUmtf9MMf&R<|r)E&@(F-iYcmeP`Gfy0jOAa8@^2sU6f{Wx F{{Ru}yR!fQ literal 0 HcmV?d00001 diff --git a/src/pipe.c b/src/pipe.c index c9a4e8b..da043f8 100644 --- a/src/pipe.c +++ b/src/pipe.c @@ -1,14 +1,14 @@ #include "pipe.h" -#include "queue.h" #include "command.h" +#include "queue.h" +#include +#include +#include #include #include #include -#include -#include #include -#include -#include +#include #define FIFO_PATH "/tmp/looper_cmd" #define LINE_MAX 256 @@ -18,57 +18,57 @@ extern spsc_queue_t cmd_queue; extern spsc_queue_t cmd_queue_main_fifo; static void *pipe_thread_func(void *arg) { - (void)arg; - FILE *fifo = fopen(FIFO_PATH, "r"); - if (!fifo) { - perror("fopen fifo"); - return NULL; - } - char line[LINE_MAX]; - while (fgets(line, sizeof(line), fifo)) { - /* strip newline */ - size_t len = strlen(line); - if (len > 0 && line[len-1] == '\n') - line[len-1] = '\0'; - - if (strcmp(line, "add") == 0) { - command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main_fifo, cmd); - } else if (strcmp(line, "remove") == 0) { - command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 }; - queue_push(&cmd_queue_main_fifo, cmd); - } else if (strncmp(line, "record ", 7) == 0) { - int ch = atoi(line + 7); - command_t cmd = { .type = CMD_CYCLE, .channel = ch, .data = 0 }; - queue_push(&cmd_queue, cmd); - } else if (strcmp(line, "stop") == 0) { - command_t cmd = { .type = CMD_STOP, .channel = -1, .data = 0 }; - queue_push(&cmd_queue, cmd); - } else if (strncmp(line, "bind ", 5) == 0) { - int ch = atoi(line + 5); - command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = ch }; - queue_push(&cmd_queue, cmd); - } else if (strcmp(line, "unbind") == 0) { - command_t cmd = { .type = CMD_UNBIND, .channel = -1, .data = 0 }; - queue_push(&cmd_queue, cmd); - } - /* ignore unknown lines */ - } - fclose(fifo); + (void)arg; + FILE *fifo = fopen(FIFO_PATH, "r"); + if (!fifo) { + perror("fopen fifo"); return NULL; + } + char line[LINE_MAX]; + while (fgets(line, sizeof(line), fifo)) { + /* strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + if (strcmp(line, "add") == 0) { + command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "remove") == 0) { + command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strncmp(line, "record ", 7) == 0) { + int ch = atoi(line + 7); + command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0}; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "stop") == 0) { + command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0}; + queue_push(&cmd_queue, cmd); + } else if (strncmp(line, "bind ", 5) == 0) { + int ch = atoi(line + 5); + command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch}; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "unbind") == 0) { + command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0}; + queue_push(&cmd_queue, cmd); + } + /* ignore unknown lines */ + } + fclose(fifo); + return NULL; } int pipe_start_reader(void) { - /* create FIFO if it doesn't exist */ - if (mkfifo(FIFO_PATH, 0666) != 0 && errno != EEXIST) { - perror("mkfifo"); - return -1; - } - pthread_t tid; - if (pthread_create(&tid, NULL, pipe_thread_func, NULL) != 0) { - perror("pthread_create"); - return -1; - } - pthread_detach(tid); /* we don't need to join */ - return 0; + /* create FIFO if it doesn't exist */ + if (mkfifo(FIFO_PATH, 0666) != 0 && errno != EEXIST) { + perror("mkfifo"); + return -1; + } + pthread_t tid; + if (pthread_create(&tid, NULL, pipe_thread_func, NULL) != 0) { + perror("pthread_create"); + return -1; + } + pthread_detach(tid); /* we don't need to join */ + return 0; } diff --git a/src/pipe.o b/src/pipe.o new file mode 100644 index 0000000000000000000000000000000000000000..7c189fbb2386cb354a72996558f0e8622aee2edf GIT binary patch literal 10560 zcmbuF3p|wB|HmJ9Np87TA`&9ElvQFXYE(p|i)tEUFqpxZDb>G-rZ8LSViPL5m!->= z?SiCMDoG_>)rz9hmabIvf6g=K@a*~V-`8*d=k+q@eBbADzUO}CInPM+nl(*cPL4o9 zj&LV>Ekp?-$-Fnsppp#2fY2xAGSi+j*S7hQWteHDXS#p<2xUqYEv!fhGtH~Rv7EV4 z56GDtm8dAKo|$%~RhE?0l5%EBDToM7NuxYXCexe&!k2^s$dJ4P>fXZlWraaRh(g9ja!--S_{;`K(4gI?wJ_OzQdk?{I{k?~adV8qU+rz4! z9+J~Qt6nMrIcc(K`=>d4P5SP?dmge8Bv&NO|9>%lkk3C0iwz6WHcS*U(j4CB1QaZFn z`aK&G?liE{Uo*+BkN!@!V58T&vt$>EGVp;KNV@2|m^M@D^$w~2kLC&qdl5g@J~}2Q zmMdg&_@P7?nG%?W@xo#Vc4#Oe62x>vrqetW#?v-qa#mm0$lVk1ta>dw6* zE*YAS9l`2kx@E8{4V{?3XB1tQs$A9EYqcU|`(~AP#}M=V{lt2m zJGM;ts;G3$s;+wvU$?EUIeUHa0iA13=|zqXO&^~L3%g5(?9#sQSiCag_%0&ldVKVl z;_<;Z>e)GdzRjk)>wTO96N>k|nyx=v`GLFM)KStT^9-)T(c9i__jhSVZEsC+4m6sr za-`8}cKFG6bCnP3F1W@?bl&k{dty%I+mAz)K1_8D+dpRUL6diTOY}B9mT)X2^PgpT z+Abdw|CIA%k$867o@3czUfq&m-y=EaB5GGSg&oqHcjJXQ``K!x^p{ipa+99zsNCwh z#9HNo>w`A;LkR%m@uc> z*jR92;`KM1Z(id2=JU-RHjJ=w*pWXcPm&+BboiyvwL$A&%6~A*R(fctuWB1Q->kOg z_>8<1c1ro)_Xc(0hsw8}oTpeSKlAMC(k8=S90Qls>jbmcGtaP=a#tF;swf8S2+D}} zTfB${zSFElX+lAn_9FfJ6%C8^?q^rT$Ii@) z`q}Gk-K4bwftZoEq45MGrq;72kFlq7O3h5yc}+o$s!>1PHdOqs<&l5a`Cw=2EqfD3 zA9pjB^Q9AC^dg-P$GscnJt))6FL~bP(m4sd1nZ~oYv&D2Yn$0>`Ny5vD~bpI_Gszn z@@AtEw=;&U`!sz`ccbyhvBOuz4V&=OQT;!Xm4B!|J6t8*bk0orWUYq%`Ud@DxofKO z!&UR+uQK-(E4Yo`bH*T)5qfBs)s5Fp&Ly`N`G<84bqhAQk=|I?bf(C+c#gWqc?D&b z&dM!(jgW2otHpMAx`K^()fb|;?^2D^4R)7zeUAJQ`pktpfunRLf6Xj|!rH?VF8^Au zcwtAQc1p-Z98B5CTQFf?V91X3H~MK!@k@Ow|j8+Ug4{m#}}XL zIG5M0vumU0w(b!RhZR=sT{6z1;b!(XCEuN2_nf=r-%y#DI(|K;&_;Y{gZQm?gORy) zyU*_u^MPTTRJTV~1-E;oe;p9TD^a8GM+a~+lc9Hiiv4fRsbHu-j$LG3sef(3!@;L^ zCw~oOW%+NBV|^Rbootey7+s_kJh3TwYs1K#L0*3CzdY^8wE0j`&e-Ral%2G=bzh@e z^{ODpycT08t?0ap!hNGmn#)`LRcG$halaQ(X<^)Ua=Q~^j zru?==(|qr>4%6{YE5p}%7v8rvdRe)5fx?|{D}!&FUi5w%9ni95&rF;6<7Ew}{@B+l z4I7#<>eQw|iQ0()dFft_*YBujY4UE`ete;2Z#nkz{Fi;iFXRwTOHbM$uW`*l`1t}=wl3cHt8dv@jML^$WF-3^uNfjU}!xq<YlS;&jGSJvwkjNT|WqeD2%=Lapei9bppal6Z$NeXi85?W^`@ z(gyh(_Nk$*8AsZ6ZqBe7Im7jt?i;;V15+cfZrQIIy7_*|X|1l;Z%#RGIr~LA@wd`~ z=)<>to0}i}w4h?bqfXEHNnQ0$HQF8V*LKT28Rz_QX@lx9i!Wms(*I@gF%xggxRg@Ebm7R&%Pk8>9MC$wxjs)id0bpl-q+Pl*@r)VUXb|MUC^9y zL(X~K!Utvb7u0lXOXDUFbUJ?_&%bcb@Q+_xvPT=POH?kaJHd1MGw{&+j5jBgX1x8q zS?gnx_shzh@z~mxx6L z%2(VbOm?=IfA>_II1a!Pwb3UhX$j_Uc-+%J9bgn6weSSw|>6s%h z3JN;!mMmOkA9U|}&g;l}&VblQQ<9o(yPU5s`~8B)uVDq;aZz&zymhIanyj#Ek)c;r zgid;(!`5$~yE?C*pLFwXiRPn2r>cD1@2@tRe`}!XK#%NLMW+y_z{htiwyS@7COWgT zP3O|M@RsPGCR(NGbXy*YVsIvEBp(vJa+yA@PWeV?@p7ZNoYl=~UmRBowB!?p_@%yz z^sez9;KNIOYq(F!@7}R=q1VF9EevEe)aNPG~d zwP_1W_4b@2ll~le%c@cCM!|*vhMnJgTcvUChx40#c|S*#_)XWD_2ixR%#^Gr2eMN5 zZiTiD9gBdFEm~a`3(w z2eS%c+twq4{sJi+LXTPX}oQ%|6+*YTK`KGC9(`oR*76!;bFWVrJ2> zY-DDhZbLxhrOVET?ORgwJy=DD>h!F4^WE0v*4ssWwYqZ3IAC)1=SeH%_@kq8i0Mqn)FvlY`TE2j{U41lSg)bD$b>ymjO$gP}1 z$R%jW4OLfB%^=0YNCy5uSOcDdCu?Y}m!CRFSz(0&DWUS*s9b_L24a*ie-vc*8$df0 ziFWFmg^Lt=TKV@J1*$iHXP^qQZ#|G|BeWC<altgCjIbH{qQmUaOZwFvmYMN4-f8#FYSj%_QPZQ;miBssU+7S=L^jf zwPr|`-H$%MAAYhQez6};#PVXfb{r~V!P`WbSiq6pNC@(NB8(A|7Z71IR{$aoKb9ab zEUY*&SIlMc**pP@x{1i_SaEy=PzivHEsEjwYKgFLt|*=e<3z{Aa|sqJUc}}^u{aS? ztS~k&T2?>Fgd!HW(Qt+6io;@&O%TMeqGLF05idqSV0Rv5hH^=V5yVKcpCEu|2Cyy3 zOB@w!03-wACuW_NYjH|f%Ne-u1@2pAxD0|J^+FIs-^-s zd%!M2e!66sYA2`@aW%M=fnJlw^&v<8jbV8!nx5`|2~AJ;A4SvC{V%8K!FZza-9*!a zeT~|AfX20H{3*?k4vn|b^z?c=XnIgDcn$fDLA@?it>nu+nw~yiCO||6kWKe*3pu_XrqlRf zvJ9FR7L9}NG{h5V9K7v8JPUGAFG!;Oa)72cpy`Wf+>pjgFhA~O9tf9ddip+UrE#!V zPP`aaX8hKZBKp5{J>W>6q!Qfpx)WgW@7w1Y>zL-g^&kgd<7gY zbdI5ZeTTZGn4Sgw@G$NKJ0ZaM0%)gTd>GVcVEhr}yDw10Ozj?;~${@F2?_a zb|c2O!+xPhBs30#;P@!Pe8iW*@`Evc3)(|4{uQ=o1jeFhuS$D%_GLQKyHe0F|@~Eyb|gsU>u#B(=i?hZ4{&Sq(DDx zOpnfe0mieSeig>kpq+~GGC0ne7!QGbFUBpQ|6>?0g}emg9?;KKjJrTykMVVoKgak8 zXurpJDxAk37)R?{9r{D#c@Wx07@q;lkHUB%?AKU~heFQ4_-SbS;2io#`wX?G9P)5X ze+T*(VH`att;KjCv^QZq8v4({_&sPJ#yAJ+i!uHJ+7%dAhx_gt#yy~)zcBtYv^y|9 z4cgk!9~z%%I1WQG4(@XxSYUh)tk(_W=zL7WxXcTAkHR?m{w~HidTy)6_-E+o4aQU8 z`cwcf6v%-3bwFkk<`}Pr(7@tcsxVcY@w`32*@ zKz^jvVa7O?E>17r{n85@X2k?Ao3hhxaMtmH! zM`PR-+IAS932i5guYxwZ&m#ZZAfJHg^PugC@e|PY#yItBCduf}nV?@6pe_K@UxoI3 zjNgLxVvIk4b_m9wK|37d?a)T&2kI|+aTbT^mEn3o&v8hv1vwf�?-%!u*&(F2Oi@ zo=eBLBh+t)9GGxPQN$E-!04QTakL)^yZ9A+5j%v8MM7B|fl@+lG}}(ZO%M@wp==SG zunUQgC+vhVGK0zu<%WpESv&#xJF}fE%?=3>a+i@ZP#7X1TM*6#MSEE^PryY*$ofzc zVDeQCX>(%ue6E13S`Z`R+L5nT$53y0pdBs{+rulggP5S{a+j0&hcvcY+R_X*@>fkTQ64zj4{ z6W48t&wrm`ZaJZL{~iRwq~2j@*+?XQCMcgx)Le4_FQfOD*``u$903M)9VpnmX9 z0hLGfuO=C&-vMQiX%MR5xVvEs1If@hq>|dc%B#W`AeSj`y&rh_EL6T%LZzx9}{DJp)s63j#zUl@gV`2U1UI*S;fhonygJp(oQIZfuhZYqPwX&$^ W6CXcqGN-Tdhhh8CeFZtd%l|KeOvp|E literal 0 HcmV?d00001 diff --git a/src/queue.c b/src/queue.c index bca85c6..6e7f6e3 100644 --- a/src/queue.c +++ b/src/queue.c @@ -3,28 +3,29 @@ #include void queue_init(spsc_queue_t *q) { - /* nothing to allocate, just ensure head/tail start at 0 */ - q->head = 0; - q->tail = 0; + /* nothing to allocate, just ensure head/tail start at 0 */ + q->head = 0; + q->tail = 0; } bool queue_push(spsc_queue_t *q, command_t cmd) { - int h = atomic_load_explicit(&q->head, memory_order_relaxed); - int t = atomic_load_explicit(&q->tail, memory_order_acquire); - int next = (h + 1) % QUEUE_CAPACITY; - if (next == t) - return false; /* queue full */ - q->buffer[h] = cmd; - atomic_store_explicit(&q->head, next, memory_order_release); - return true; + int h = atomic_load_explicit(&q->head, memory_order_relaxed); + int t = atomic_load_explicit(&q->tail, memory_order_acquire); + int next = (h + 1) % QUEUE_CAPACITY; + if (next == t) + return false; /* queue full */ + q->buffer[h] = cmd; + atomic_store_explicit(&q->head, next, memory_order_release); + return true; } bool queue_pop(spsc_queue_t *q, command_t *cmd) { - int t = atomic_load_explicit(&q->tail, memory_order_relaxed); - int h = atomic_load_explicit(&q->head, memory_order_acquire); - if (t == h) - return false; /* queue empty */ - *cmd = q->buffer[t]; - atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY, memory_order_release); - return true; + int t = atomic_load_explicit(&q->tail, memory_order_relaxed); + int h = atomic_load_explicit(&q->head, memory_order_acquire); + if (t == h) + return false; /* queue empty */ + *cmd = q->buffer[t]; + atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY, + memory_order_release); + return true; } diff --git a/src/queue.o b/src/queue.o new file mode 100644 index 0000000000000000000000000000000000000000..216d259aba9b8a8812193a4444992b5ff00fac4d GIT binary patch literal 5952 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 literal 0 HcmV?d00001 -- 2.49.1 From d47fddbeb31245d63b0068e2243dcbe2f757e47d Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 01:11:46 +0000 Subject: [PATCH 15/17] docs: add command architecture documentation --- docs/12-command-architecture.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/12-command-architecture.md diff --git a/docs/12-command-architecture.md b/docs/12-command-architecture.md new file mode 100644 index 0000000..e69de29 -- 2.49.1 From 69859a62942bfaab8704c5b7c85d3d9e397d5c02 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 01:11:47 +0000 Subject: [PATCH 16/17] docs: add command architecture documentation Co-authored-by: aider (deepseek/deepseek-reasoner) --- docs/12-command-architecture.md | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/12-command-architecture.md b/docs/12-command-architecture.md index e69de29..ec399b5 100644 --- a/docs/12-command-architecture.md +++ b/docs/12-command-architecture.md @@ -0,0 +1,65 @@ +# Command Architecture + +## Overview + +The looper uses a **lock‑free, single‑producer single‑consumer (SPSC)** command queue to communicate between the real‑time JACK audio thread and the main (non‑RT) thread. +There are two families of queues: + +- **`cmd_queue`** (RT‑safe) – used for commands that can be handled directly inside the process callback (`CMD_CYCLE`, `CMD_STOP`, `CMD_BIND_CHANNEL`, `CMD_UNBIND`). + The producer is the MIDI handler (`midi_handle_events`) or the FIFO pipe reader (`pipe_thread_func`); the consumer is `process_callback`. + +- **`cmd_queue_main_midi`** / **`cmd_queue_main_fifo`** – used for commands that require memory allocation or JACK API calls (`CMD_ADD_CHANNEL`, `CMD_REMOVE_CHANNEL`). + The producer is the MIDI handler (or FIFO reader), and the consumer is `looper_process_commands`, which runs in the main loop approximately every 50 ms. + +## Command Types + +The `command_t` struct (defined in `command.h`) contains: + +- `type` – one of the `cmd_type_t` enumerators. +- `channel` – target channel index; `-1` means “current bind channel” for some commands. +- `data` – extra parameter (e.g., bind channel number for `CMD_BIND_CHANNEL`). + +### RT‑safe Commands (pushed to `cmd_queue`) + +| Type | Effect | +|--------------------|---------------------------------------------------------------------| +| `CMD_CYCLE` | Toggle the state machine of the target channel (IDLE→RECORD→LOOPING→PAUSED→LOOPING…). | +| `CMD_STOP` | Force the target channel (or all channels, if `channel == -1`) to `STATE_IDLE`. | +| `CMD_BIND_CHANNEL` | Set the global `bind_channel` index to `data`. | +| `CMD_UNBIND` | Reset `bind_channel` to 0. | + +### Main‑thread Commands (pushed to `cmd_queue_main_midi` / `cmd_queue_main_fifo`) + +| Type | Effect | +|---------------------|---------------------------------------------------------------------| +| `CMD_ADD_CHANNEL` | Create a new dynamic channel (port registration). | +| `CMD_REMOVE_CHANNEL`| Remove the highest‑numbered active dynamic channel (excluding channel 0). | + +## Command Flow + +1. **MIDI input** – `midi_handle_events` parses incoming note‑on events and decides which command to push. + RT‑safe commands are pushed to `cmd_queue`; add/remove commands are pushed to `cmd_queue_main_midi`. + +2. **FIFO input** – `pipe_thread_func` reads lines from `/tmp/looper_cmd` and pushes the corresponding command. + RT‑safe commands go to `cmd_queue`; add/remove go to `cmd_queue_main_fifo`. + +3. **Process callback** – `process_callback` is invoked by JACK for each audio cycle. It drains `cmd_queue` and applies each command via `apply_command`. This function modifies the channel state and bind index atomically. + +4. **Main loop** – `looper_process_commands` is called in the main loop (≈ every 50 ms). It drains `cmd_queue_main_midi` and `cmd_queue_main_fifo`, performing the necessary port registrations/unregistrations and calling `channel_add` / `channel_remove`. + +## Deferred Port Unregistration + +When a dynamic channel is removed, the RT thread first sets `active = 0`. The main thread waits until it has seen at least one full RT cycle pass (using `global_rt_cycles`) before calling `jack_port_unregister`. This prevents a race between the RT thread still holding a reference to the port buffer and the port being unregistered. + +## SPSC Queue Implementation + +The queue itself (defined in `queue.c`/`queue.h`) is a simple circular buffer with head and tail indices. It uses C11 atomic loads/stores with appropriate memory ordering (`memory_order_acquire`/`memory_order_release`) to guarantee visibility without locks. Capacity is fixed at `QUEUE_CAPACITY` (256 commands). Push/pop operations are O(1) and never block. + +## Thread Safety + +- The JACK process callback runs in an RT thread. +- The MIDI handler runs inside the process callback (it is called from `process_callback`). +- The FIFO reader lives in a separate POSIX thread. +- The main thread runs the rest of the program. + +The two‑queue design ensures that memory‑allocating operations never happen inside the RT thread, while RT‑pertinent commands are processed with minimal latency. -- 2.49.1 From 3a4aac33562e6b47f02ff6efc54b1d0019cb0bf3 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 01:12:07 +0000 Subject: [PATCH 17/17] Documentation --- docs/12-command-architecture | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/12-command-architecture diff --git a/docs/12-command-architecture b/docs/12-command-architecture new file mode 100644 index 0000000..e69de29 -- 2.49.1