From e824f6df73aabf7c1ada38a44ababa6d0674bb0b Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:03:49 +0000 Subject: [PATCH 01/31] feat: add test for dynamic channel creation via MIDI command Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/integration.c b/tests/integration.c index 73a4b50..aebece8 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -366,6 +366,52 @@ static int test_looper_looping(void) { return 0; } +/* test multiple channels */ +static int test_multiple_channels(void) { + printf("Test: dynamic channel creation via MIDI command\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_multi", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + + if (send_jack_note_on("looper:control", 60, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 60 failed\n"); + return 1; + } + usleep(500000); + + int found = 0; + const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); + if (ports) { + for (int i = 0; ports[i]; i++) { + if (strstr(ports[i], "looper:channel1_input")) { + found = 1; + break; + } + } + jack_free(ports); + } + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + + if (!found) { + fprintf(stderr, " FAIL: channel1_input port not created after add command\n"); + return 1; + } + printf(" PASS (channel created)\n"); + return 0; +} + int main(void) { /* 1. binary must exist */ @@ -385,6 +431,12 @@ int main(void) { return 1; } + /* 5. Test multiple dynamic channels */ + if (test_multiple_channels() != 0) { + fprintf(stderr, " FAILED\n"); + return 1; + } + printf("All tests completed successfully.\n"); return 0; } From a7106f55d5d4df0648909dda0e87c2d0be679885 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:31:02 +0000 Subject: [PATCH 02/31] docs: add evaluation documentation --- evaluation.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 evaluation.md diff --git a/evaluation.md b/evaluation.md new file mode 100644 index 0000000..e69de29 From 07962c2a095122935dffd896b19b5e21a1c2ef6d Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:31:04 +0000 Subject: [PATCH 03/31] feat: add test evaluation section to evaluation.md Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/evaluation.md b/evaluation.md index e69de29..3585f4c 100644 --- a/evaluation.md +++ b/evaluation.md @@ -0,0 +1,24 @@ +# Code Evaluation + +## Summary Table + +| Category | Rating | Remarks | +|--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Mocked / Left Undone | ❌ Issue | The test `test_multiple_channels` expects dynamic channel creation via MIDI note 60, but the looper does not implement this feature. Also the control key is supposed to be 64, but code uses note 1. No multiple channels. | +| Potential Segfaults | ✅ OK | No obvious segfaults: all buffer accesses are bounds‑checked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. | +| 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 | ❌ Issue | The current design is single‑channel, static, and not extensible. A dynamic multi‑channel system is required. Global state and singular port pairs prevent scaling. No abstraction layer for channels exists. | + +## Test Evaluation + +| 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. | From 6b6f2dee3c741f2d01fd90c7f30e165b17861a4e Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:38:37 +0000 Subject: [PATCH 04/31] refactor: restructure looper into multi-channel architecture Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/main.c | 340 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 217 insertions(+), 123 deletions(-) diff --git a/src/main.c b/src/main.c index 5e5a2de..a605bdd 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include #define LOOP_BUF_SIZE (5 * 48000) /* 5 seconds at 48 kHz, mono */ +#define MAX_CHANNELS 16 typedef enum { STATE_IDLE, @@ -16,151 +17,187 @@ typedef enum { STATE_PAUSED } looper_state; -static atomic_int current_state = STATE_IDLE; +/* per‑channel state */ +struct channel_t { + atomic_int state; + int prev_state; + float loop_buffer[LOOP_BUF_SIZE]; + int loop_count; + int record_pos; + int playback_pos; + int active; /* 1 = channel in use */ + jack_port_t *audio_in; + jack_port_t *audio_out; +}; -/* loop buffer and playback state */ -static float loop_buffer[LOOP_BUF_SIZE]; -static int loop_count = 0; /* number of recorded samples */ -static int record_pos = 0; /* next write index while recording */ -static int playback_pos = 0; /* next read index while looping */ -static int prev_state = -1; /* for detecting state changes */ +static struct channel_t channels[MAX_CHANNELS]; +static atomic_int channel_count = 0; /* number of active channels */ +static int next_channel_id = 1; /* for port naming */ +static atomic_int cmd_add = 0; /* set by MIDI note 60 in process() */ +static atomic_int cmd_remove = 0; /* set by MIDI note 61 */ -static jack_port_t *input_port; -static jack_port_t *output_port; static jack_port_t *midi_control_port; static jack_port_t *midi_clock_port; - static jack_client_t *client; +/* --------------------------------------------------------------- + * process callback – runs in real‑time context + * --------------------------------------------------------------- */ static int process(jack_nframes_t nframes, void *arg) { (void)arg; - jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) jack_port_get_buffer(input_port, nframes); - jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) jack_port_get_buffer(output_port, nframes); - /* ----- state change detection ----- */ - int state = atomic_load(¤t_state); - if (state != prev_state) { - if (state == STATE_RECORD) { - record_pos = 0; - loop_count = 0; - } else if (state == STATE_LOOPING) { - if (record_pos > 0) { - loop_count = record_pos; /* what we recorded */ - } - playback_pos = 0; /* restart from beginning */ - } - } - - /* ----- handle MIDI control port (state transitions) ----- */ + /* handle MIDI commands on the global control port */ void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); if (midi_ctrl_buf) { jack_nframes_t nevents = jack_midi_get_event_count(midi_ctrl_buf); jack_midi_event_t ev; for (jack_nframes_t i = 0; i < nevents; i++) { - if (jack_midi_event_get(&ev, midi_ctrl_buf, i) == 0) { - /* note on with note number 1 */ - if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) { - unsigned char note = ev.buffer[1]; - if (note == 1) { - int cur_state = atomic_load(¤t_state); - switch (cur_state) { - case STATE_IDLE: - atomic_store(¤t_state, STATE_RECORD); - break; - case STATE_RECORD: - atomic_store(¤t_state, STATE_LOOPING); - break; - case STATE_LOOPING: - atomic_store(¤t_state, STATE_PAUSED); - break; - case STATE_PAUSED: - atomic_store(¤t_state, STATE_LOOPING); - break; - } + if (jack_midi_event_get(&ev, midi_ctrl_buf, i) != 0) continue; + if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) { + unsigned char note = ev.buffer[1]; + switch (note) { + case 1: /* toggle state of channel 0 (backward compatible) */ + { + 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 60: /* add channel – send to main thread */ + atomic_store(&cmd_add, 1); + break; + case 61: /* remove channel – send to main thread */ + atomic_store(&cmd_remove, 1); + break; + default: + break; } } } } - /* ----- audio output based on current state ----- */ - if (!out) return 0; /* cannot happen, but safe */ + /* process each active channel */ + for (int c = 0; c < MAX_CHANNELS; c++) { + if (!channels[c].active) continue; - jack_nframes_t i; + jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) + jack_port_get_buffer(channels[c].audio_in, nframes); + jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) + jack_port_get_buffer(channels[c].audio_out, nframes); + if (!out) continue; /* safety */ - switch (state) { - case STATE_RECORD: - if (in) { - const float *inf = (const float *)in; - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - if (record_pos < LOOP_BUF_SIZE) { - loop_buffer[record_pos] = inf[i]; - record_pos++; + int state = atomic_load(&channels[c].state); + + /* transition initialisation */ + if (state != channels[c].prev_state) { + switch (state) { + case STATE_RECORD: + channels[c].record_pos = 0; + channels[c].loop_count = 0; + break; + case STATE_LOOPING: + if (channels[c].record_pos > 0) + channels[c].loop_count = channels[c].record_pos; + channels[c].playback_pos = 0; + break; + default: + break; + } + } + + jack_nframes_t i; + + switch (state) { + case STATE_RECORD: + if (in) { + const float *inf = (const float *)in; + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + if (channels[c].record_pos < LOOP_BUF_SIZE) { + channels[c].loop_buffer[channels[c].record_pos] = inf[i]; + channels[c].record_pos++; + } + outf[i] = inf[i]; /* monitor input */ } - outf[i] = inf[i]; /* monitor input */ + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; + break; - case STATE_LOOPING: - if (loop_count > 0) { - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - outf[i] = loop_buffer[playback_pos]; - playback_pos = (playback_pos + 1) % loop_count; + case STATE_LOOPING: + if (channels[c].loop_count > 0) { + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; + channels[c].playback_pos = + (channels[c].playback_pos + 1) % channels[c].loop_count; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; + break; - case STATE_PAUSED: - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - break; - - default: /* IDLE */ - if (in) { - memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); - } else { + case STATE_PAUSED: memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + break; + + default: /* IDLE */ + if (in) { + memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; } - break; + + channels[c].prev_state = state; } - /* ----- MIDI clock events (unchanged) ----- */ + /* ----- MIDI clock events (still affect channel 0 only) ----- */ void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); if (midi_clock_buf) { jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); jack_midi_event_t cev; for (jack_nframes_t j = 0; j < n_clock_events; j++) { - if (jack_midi_event_get(&cev, midi_clock_buf, j) == 0) { - if (cev.size >= 1) { - unsigned char msg = cev.buffer[0]; - if (msg == 0xFA) { - int s = atomic_load(¤t_state); - if (s == STATE_IDLE) { - atomic_store(¤t_state, STATE_RECORD); - } - } else if (msg == 0xFC) { - atomic_store(¤t_state, STATE_IDLE); - } else if (msg == 0xFB) { - int s = atomic_load(¤t_state); - if (s == STATE_PAUSED) { - atomic_store(¤t_state, STATE_LOOPING); - } - } + if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; + if (cev.size >= 1) { + unsigned char msg = cev.buffer[0]; + switch (msg) { + case 0xFA: { + int s = atomic_load(&channels[0].state); + if (s == STATE_IDLE) + atomic_store(&channels[0].state, STATE_RECORD); + break; + } + case 0xFC: + atomic_store(&channels[0].state, STATE_IDLE); + break; + case 0xFB: { + int s = atomic_load(&channels[0].state); + if (s == STATE_PAUSED) + atomic_store(&channels[0].state, STATE_LOOPING); + break; + } + default: + break; } } } } - /* update prev_state after all state changes */ - prev_state = atomic_load(¤t_state); - return 0; } @@ -182,35 +219,46 @@ int main(int argc, char *argv[]) client = jack_client_open(client_name, options, &status); if (client == NULL) { fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); - if (status & JackServerFailed) { + if (status & JackServerFailed) fprintf(stderr, "Unable to connect to JACK server\n"); - } return 1; } - if (status & JackNameNotUnique) { + if (status & JackNameNotUnique) client_name = jack_get_client_name(client); - } jack_set_process_callback(client, process, NULL); jack_on_shutdown(client, jack_shutdown, NULL); - input_port = jack_port_register(client, "input", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - output_port = jack_port_register(client, "output", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); + /* ------------------ channel 0 (the default channel) ------------------ */ + channels[0].active = 1; + atomic_store(&channels[0].state, STATE_IDLE); + channels[0].prev_state = -1; + channels[0].loop_count = 0; + channels[0].record_pos = 0; + channels[0].playback_pos = 0; + + channels[0].audio_in = jack_port_register(client, "input", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + channels[0].audio_out = jack_port_register(client, "output", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!channels[0].audio_in || !channels[0].audio_out) { + fprintf(stderr, "Could not create audio ports for channel 0\n"); + return 1; + } + channel_count = 1; + + /* MIDI control & clock ports (shared across all channels) */ midi_control_port = jack_port_register(client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); - midi_clock_port = jack_port_register(client, "clock", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - - if ((input_port == NULL) || (output_port == NULL) || - (midi_control_port == NULL) || (midi_clock_port == NULL)) { - fprintf(stderr, "Could not create ports\n"); + midi_clock_port = jack_port_register(client, "clock", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + if (!midi_control_port || !midi_clock_port) { + fprintf(stderr, "Could not create MIDI ports\n"); return 1; } @@ -221,10 +269,56 @@ int main(int argc, char *argv[]) fprintf(stderr, "looper running (client name '%s')\n", client_name); - - prev_state = -1; /* initialise change detection */ - while (1) { + /* process pending add‑channel / remove‑channel commands + * (only safe outside the real‑time callback) */ + if (atomic_exchange(&cmd_add, 0)) { + int idx; + for (idx = 0; idx < MAX_CHANNELS; idx++) + if (!channels[idx].active) break; + if (idx < MAX_CHANNELS) { + channels[idx].active = 1; + atomic_store(&channels[idx].state, STATE_IDLE); + channels[idx].prev_state = -1; + channels[idx].loop_count = 0; + channels[idx].record_pos = 0; + channels[idx].playback_pos = 0; + + char in_name[64], out_name[64]; + snprintf(in_name, sizeof(in_name), + "channel%d_input", next_channel_id); + snprintf(out_name, sizeof(out_name), + "channel%d_output", next_channel_id); + + channels[idx].audio_in = + jack_port_register(client, in_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + channels[idx].audio_out = + jack_port_register(client, out_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!channels[idx].audio_in || !channels[idx].audio_out) + fprintf(stderr, "Failed to register ports for channel %d\n", + next_channel_id); + next_channel_id++; + channel_count++; + } + } + + if (atomic_exchange(&cmd_remove, 0)) { + /* find the highest active channel index >0 */ + int remove_idx = -1; + for (int idx = 1; idx < MAX_CHANNELS; idx++) + if (channels[idx].active) remove_idx = idx; + if (remove_idx != -1) { + jack_port_unregister(client, channels[remove_idx].audio_in); + jack_port_unregister(client, channels[remove_idx].audio_out); + channels[remove_idx].active = 0; + channel_count--; + } + } + sleep(1); } From 9eb264aab86252c5c6ce7d4dc62ded1a1bed6038 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:51:34 +0000 Subject: [PATCH 05/31] feat: implement control key (note 64) and trigger looper command (note 62) Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 4 +- src/main.c | 102 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/evaluation.md b/evaluation.md index 3585f4c..a68fd13 100644 --- a/evaluation.md +++ b/evaluation.md @@ -4,12 +4,12 @@ | Category | Rating | Remarks | |--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Mocked / Left Undone | ❌ Issue | The test `test_multiple_channels` expects dynamic channel creation via MIDI note 60, but the looper does not implement this feature. Also the control key is supposed to be 64, but code uses note 1. No multiple channels. | +| 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 | ✅ OK | No obvious segfaults: all buffer accesses are bounds‑checked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. | | 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 | ❌ Issue | The current design is single‑channel, static, and not extensible. A dynamic multi‑channel system is required. Global state and singular port pairs prevent scaling. No abstraction layer for channels exists. | +| 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. | ## Test Evaluation diff --git a/src/main.c b/src/main.c index a605bdd..60e588b 100644 --- a/src/main.c +++ b/src/main.c @@ -40,6 +40,9 @@ static jack_port_t *midi_control_port; static jack_port_t *midi_clock_port; static jack_client_t *client; +/* control key mechanism – note 64 acts as a modifier */ +static atomic_int control_key_active = 0; + /* --------------------------------------------------------------- * process callback – runs in real‑time context * --------------------------------------------------------------- */ @@ -54,37 +57,78 @@ static int process(jack_nframes_t nframes, void *arg) jack_midi_event_t ev; for (jack_nframes_t i = 0; i < nevents; i++) { if (jack_midi_event_get(&ev, midi_ctrl_buf, i) != 0) continue; - if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) { - unsigned char note = ev.buffer[1]; - switch (note) { - case 1: /* toggle state of channel 0 (backward compatible) */ - { - 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; + if (ev.size < 3) continue; + unsigned char status = ev.buffer[0]; + unsigned char note = ev.buffer[1]; + unsigned char vel = ev.buffer[2]; + + /* note‑on */ + if ((status & 0xf0) == 0x90 && vel > 0) { + if (note == 64) { + /* control key pressed – activate modifier */ + atomic_store(&control_key_active, 1); + } else { + int ck = atomic_load(&control_key_active); + if (ck) { + /* command selected by control key */ + atomic_store(&control_key_active, 0); + switch (note) { + case 60: atomic_store(&cmd_add, 1); break; + case 61: atomic_store(&cmd_remove, 1); break; + case 62: /* trigger looper – 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; + default: + break; + } + } else { + /* direct mapping (backward compatible) */ + switch (note) { + case 1: /* toggle state of 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 60: atomic_store(&cmd_add, 1); break; + case 61: atomic_store(&cmd_remove, 1); break; + default: + break; + } } - break; - } - case 60: /* add channel – send to main thread */ - atomic_store(&cmd_add, 1); - break; - case 61: /* remove channel – send to main thread */ - atomic_store(&cmd_remove, 1); - break; - default: - break; } + } else if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) { + /* note‑off – clear control key state */ + atomic_store(&control_key_active, 0); } } } From f1a92f1e95e60a0f3fb02d331e85477c82f3c346 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 20:56:56 +0000 Subject: [PATCH 06/31] fix: increase sleep duration in dynamic channel test to allow processing Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration.c b/tests/integration.c index aebece8..6ae382b 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -387,7 +387,9 @@ static int test_multiple_channels(void) { fprintf(stderr, " FAIL: send note 60 failed\n"); return 1; } - usleep(500000); + /* wait long enough for the looper's main loop to process the add command + (it sleeps for 1 second between checks, so 1.5 s is safe) */ + usleep(1500000); int found = 0; const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); From 96295fdb4c8e99fd644f8e9257401c4ccbb7985a Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 21:03:10 +0000 Subject: [PATCH 07/31] refactor: split monolithic main.c into modular source files Co-authored-by: aider (deepseek/deepseek-reasoner) --- makefile | 16 ++- src/channel.c | 38 ++++++ src/channel.h | 39 ++++++ src/looper.c | 208 ++++++++++++++++++++++++++++++ src/looper.h | 18 +++ src/main.c | 341 ++------------------------------------------------ src/midi.c | 91 ++++++++++++++ src/midi.h | 8 ++ 8 files changed, 423 insertions(+), 336 deletions(-) create mode 100644 src/channel.c create mode 100644 src/channel.h create mode 100644 src/looper.c create mode 100644 src/looper.h create mode 100644 src/midi.c create mode 100644 src/midi.h diff --git a/makefile b/makefile index b2049a0..0556ab9 100644 --- a/makefile +++ b/makefile @@ -1,9 +1,15 @@ CC ?= gcc -CFLAGS ?= -Wall -Wextra -g -LDFLAGS ?= -ljack +CFLAGS ?= -Wall -Wextra -g -Isrc +LDFLAGS ?= -ljack -lm -looper: src/main.c - $(CC) $(CFLAGS) -o looper src/main.c $(LDFLAGS) +SRC = src/main.c src/looper.c src/channel.c src/midi.c +OBJ = $(SRC:.c=.o) + +looper: $(OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +src/%.o: src/%.c + $(CC) $(CFLAGS) -c -o $@ $< integration: looper tests/integration.c $(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm @@ -13,4 +19,4 @@ test: integration .PHONY: clean integration test clean: - rm -f looper integration_test + rm -f looper integration_test src/*.o diff --git a/src/channel.c b/src/channel.c new file mode 100644 index 0000000..9673317 --- /dev/null +++ b/src/channel.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include "channel.h" + +void channel_add(jack_client_t *client, int idx) +{ + channels[idx].active = 1; + atomic_store(&channels[idx].state, STATE_IDLE); + channels[idx].prev_state = -1; + channels[idx].loop_count = 0; + channels[idx].record_pos = 0; + channels[idx].playback_pos = 0; + + char in_name[64], out_name[64]; + snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id); + snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id); + + channels[idx].audio_in = jack_port_register(client, in_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + channels[idx].audio_out = jack_port_register(client, out_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!channels[idx].audio_in || !channels[idx].audio_out) + fprintf(stderr, "Failed to register ports for channel %d\n", next_channel_id); + next_channel_id++; + channel_count++; +} + +void channel_remove(jack_client_t *client, int idx) +{ + jack_port_unregister(client, channels[idx].audio_in); + jack_port_unregister(client, channels[idx].audio_out); + channels[idx].active = 0; + channel_count--; +} diff --git a/src/channel.h b/src/channel.h new file mode 100644 index 0000000..d6f0d06 --- /dev/null +++ b/src/channel.h @@ -0,0 +1,39 @@ +#ifndef CHANNEL_H +#define CHANNEL_H + +#include +#include + +#define LOOP_BUF_SIZE (5 * 48000) +#define MAX_CHANNELS 16 + +typedef enum { + STATE_IDLE, + STATE_RECORD, + STATE_LOOPING, + STATE_PAUSED +} looper_state; + +struct channel_t { + atomic_int state; + int prev_state; + float loop_buffer[LOOP_BUF_SIZE]; + int loop_count; + int record_pos; + int playback_pos; + int active; + jack_port_t *audio_in; + jack_port_t *audio_out; +}; + +/* Globals declared in looper.c */ +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); + +#endif diff --git a/src/looper.c b/src/looper.c new file mode 100644 index 0000000..5a8afaa --- /dev/null +++ b/src/looper.c @@ -0,0 +1,208 @@ +#include +#include +#include +#include +#include +#include +#include +#include "looper.h" +#include "channel.h" +#include "midi.h" + +/* Global state (shared across files) */ +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; +jack_port_t *midi_control_port = NULL; +jack_port_t *midi_clock_port = NULL; +atomic_int control_key_active = 0; + +/* ---------------------------------------------------------------- + * process callback + * ---------------------------------------------------------------- */ +int process_callback(jack_nframes_t nframes, void *arg) +{ + (void)arg; + + void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); + if (midi_ctrl_buf) { + midi_handle_events(midi_ctrl_buf, nframes); + } + + /* process each active channel */ + for (int c = 0; c < MAX_CHANNELS; c++) { + if (!channels[c].active) continue; + + jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) + jack_port_get_buffer(channels[c].audio_in, nframes); + jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) + jack_port_get_buffer(channels[c].audio_out, nframes); + if (!out) continue; + + int state = atomic_load(&channels[c].state); + + if (state != channels[c].prev_state) { + switch (state) { + case STATE_RECORD: + channels[c].record_pos = 0; + channels[c].loop_count = 0; + break; + case STATE_LOOPING: + if (channels[c].record_pos > 0) + channels[c].loop_count = channels[c].record_pos; + channels[c].playback_pos = 0; + break; + default: + break; + } + } + + jack_nframes_t i; + switch (state) { + case STATE_RECORD: + if (in) { + for (i = 0; i < nframes; i++) { + if (channels[c].record_pos < LOOP_BUF_SIZE) + channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i]; + ((float *)out)[i] = ((const float *)in)[i]; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + + case STATE_LOOPING: + if (channels[c].loop_count > 0) { + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; + channels[c].playback_pos = (channels[c].playback_pos + 1) % channels[c].loop_count; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + + case STATE_PAUSED: + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + break; + + default: /* IDLE */ + if (in) { + memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + } + + channels[c].prev_state = state; + } + + /* MIDI clock events – affect channel 0 only */ + void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); + if (midi_clock_buf) { + jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); + jack_midi_event_t cev; + for (jack_nframes_t j = 0; j < n_clock_events; j++) { + if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; + if (cev.size >= 1) { + unsigned char msg = cev.buffer[0]; + switch (msg) { + case 0xFA: { + int s = atomic_load(&channels[0].state); + if (s == STATE_IDLE) atomic_store(&channels[0].state, STATE_RECORD); + break; + } + case 0xFC: + atomic_store(&channels[0].state, STATE_IDLE); + break; + case 0xFB: { + int s = atomic_load(&channels[0].state); + if (s == STATE_PAUSED) atomic_store(&channels[0].state, STATE_LOOPING); + break; + } + default: + break; + } + } + } + } + + return 0; +} + +/* ---------------------------------------------------------------- + * shutdown callback + * ---------------------------------------------------------------- */ +void jack_shutdown_cb(void *arg) +{ + (void)arg; + fprintf(stderr, "JACK shutdown\n"); + exit(0); +} + +/* ---------------------------------------------------------------- + * looper initialisation + * ---------------------------------------------------------------- */ +int looper_init(jack_client_t *client) +{ + /* channel 0 */ + channels[0].active = 1; + atomic_store(&channels[0].state, STATE_IDLE); + channels[0].prev_state = -1; + channels[0].loop_count = 0; + channels[0].record_pos = 0; + channels[0].playback_pos = 0; + + channels[0].audio_in = jack_port_register(client, "input", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + channels[0].audio_out = jack_port_register(client, "output", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!channels[0].audio_in || !channels[0].audio_out) { + fprintf(stderr, "Could not create audio ports for channel 0\n"); + return -1; + } + channel_count = 1; + + midi_control_port = jack_port_register(client, "control", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + midi_clock_port = jack_port_register(client, "clock", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + if (!midi_control_port || !midi_clock_port) { + fprintf(stderr, "Could not create MIDI ports\n"); + return -1; + } + + return 0; +} + +/* ---------------------------------------------------------------- + * main‑loop command processing + * ---------------------------------------------------------------- */ +void looper_process_commands(jack_client_t *client) +{ + 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); + } + } + + 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) { + channel_remove(client, remove_idx); + } + } +} diff --git a/src/looper.h b/src/looper.h new file mode 100644 index 0000000..7fb68d4 --- /dev/null +++ b/src/looper.h @@ -0,0 +1,18 @@ +#ifndef LOOPER_H +#define LOOPER_H + +#include + +/* Initialisation – must be called after setting process callback */ +int looper_init(jack_client_t *client); + +/* Process callback – to be called by JACK */ +int process_callback(jack_nframes_t nframes, void *arg); + +/* Shutdown callback */ +void jack_shutdown_cb(void *arg); + +/* Main‑loop command processing (add/remove channels) */ +void looper_process_commands(jack_client_t *client); + +#endif diff --git a/src/main.c b/src/main.c index 60e588b..06704ca 100644 --- a/src/main.c +++ b/src/main.c @@ -1,256 +1,8 @@ #include #include #include -#include #include -#include -#include -#include - -#define LOOP_BUF_SIZE (5 * 48000) /* 5 seconds at 48 kHz, mono */ -#define MAX_CHANNELS 16 - -typedef enum { - STATE_IDLE, - STATE_RECORD, - STATE_LOOPING, - STATE_PAUSED -} looper_state; - -/* per‑channel state */ -struct channel_t { - atomic_int state; - int prev_state; - float loop_buffer[LOOP_BUF_SIZE]; - int loop_count; - int record_pos; - int playback_pos; - int active; /* 1 = channel in use */ - jack_port_t *audio_in; - jack_port_t *audio_out; -}; - -static struct channel_t channels[MAX_CHANNELS]; -static atomic_int channel_count = 0; /* number of active channels */ -static int next_channel_id = 1; /* for port naming */ -static atomic_int cmd_add = 0; /* set by MIDI note 60 in process() */ -static atomic_int cmd_remove = 0; /* set by MIDI note 61 */ - -static jack_port_t *midi_control_port; -static jack_port_t *midi_clock_port; -static jack_client_t *client; - -/* control key mechanism – note 64 acts as a modifier */ -static atomic_int control_key_active = 0; - -/* --------------------------------------------------------------- - * process callback – runs in real‑time context - * --------------------------------------------------------------- */ -static int process(jack_nframes_t nframes, void *arg) -{ - (void)arg; - - /* handle MIDI commands on the global control port */ - void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); - if (midi_ctrl_buf) { - jack_nframes_t nevents = jack_midi_get_event_count(midi_ctrl_buf); - jack_midi_event_t ev; - for (jack_nframes_t i = 0; i < nevents; i++) { - if (jack_midi_event_get(&ev, midi_ctrl_buf, i) != 0) continue; - if (ev.size < 3) continue; - unsigned char status = ev.buffer[0]; - unsigned char note = ev.buffer[1]; - unsigned char vel = ev.buffer[2]; - - /* note‑on */ - if ((status & 0xf0) == 0x90 && vel > 0) { - if (note == 64) { - /* control key pressed – activate modifier */ - atomic_store(&control_key_active, 1); - } else { - int ck = atomic_load(&control_key_active); - if (ck) { - /* command selected by control key */ - atomic_store(&control_key_active, 0); - switch (note) { - case 60: atomic_store(&cmd_add, 1); break; - case 61: atomic_store(&cmd_remove, 1); break; - case 62: /* trigger looper – 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; - default: - break; - } - } else { - /* direct mapping (backward compatible) */ - switch (note) { - case 1: /* toggle state of 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 60: atomic_store(&cmd_add, 1); break; - case 61: atomic_store(&cmd_remove, 1); break; - default: - break; - } - } - } - } else if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) { - /* note‑off – clear control key state */ - atomic_store(&control_key_active, 0); - } - } - } - - /* process each active channel */ - for (int c = 0; c < MAX_CHANNELS; c++) { - if (!channels[c].active) continue; - - jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) - jack_port_get_buffer(channels[c].audio_in, nframes); - jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) - jack_port_get_buffer(channels[c].audio_out, nframes); - if (!out) continue; /* safety */ - - int state = atomic_load(&channels[c].state); - - /* transition initialisation */ - if (state != channels[c].prev_state) { - switch (state) { - case STATE_RECORD: - channels[c].record_pos = 0; - channels[c].loop_count = 0; - break; - case STATE_LOOPING: - if (channels[c].record_pos > 0) - channels[c].loop_count = channels[c].record_pos; - channels[c].playback_pos = 0; - break; - default: - break; - } - } - - jack_nframes_t i; - - switch (state) { - case STATE_RECORD: - if (in) { - const float *inf = (const float *)in; - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - if (channels[c].record_pos < LOOP_BUF_SIZE) { - channels[c].loop_buffer[channels[c].record_pos] = inf[i]; - channels[c].record_pos++; - } - outf[i] = inf[i]; /* monitor input */ - } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - - case STATE_LOOPING: - if (channels[c].loop_count > 0) { - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; - channels[c].playback_pos = - (channels[c].playback_pos + 1) % channels[c].loop_count; - } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - - case STATE_PAUSED: - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - break; - - default: /* IDLE */ - if (in) { - memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - } - - channels[c].prev_state = state; - } - - /* ----- MIDI clock events (still affect channel 0 only) ----- */ - void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); - if (midi_clock_buf) { - jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); - jack_midi_event_t cev; - for (jack_nframes_t j = 0; j < n_clock_events; j++) { - if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; - if (cev.size >= 1) { - unsigned char msg = cev.buffer[0]; - switch (msg) { - case 0xFA: { - int s = atomic_load(&channels[0].state); - if (s == STATE_IDLE) - atomic_store(&channels[0].state, STATE_RECORD); - break; - } - case 0xFC: - atomic_store(&channels[0].state, STATE_IDLE); - break; - case 0xFB: { - int s = atomic_load(&channels[0].state); - if (s == STATE_PAUSED) - atomic_store(&channels[0].state, STATE_LOOPING); - break; - } - default: - break; - } - } - } - } - - return 0; -} - -static void jack_shutdown(void *arg) -{ - (void)arg; - fprintf(stderr, "JACK shutdown\n"); - exit(0); -} +#include "looper.h" int main(int argc, char *argv[]) { @@ -260,7 +12,7 @@ int main(int argc, char *argv[]) jack_options_t options = JackNullOption; jack_status_t status; - client = jack_client_open(client_name, options, &status); + jack_client_t *client = jack_client_open(client_name, options, &status); if (client == NULL) { fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); if (status & JackServerFailed) @@ -271,99 +23,26 @@ int main(int argc, char *argv[]) if (status & JackNameNotUnique) client_name = jack_get_client_name(client); - jack_set_process_callback(client, process, NULL); - jack_on_shutdown(client, jack_shutdown, NULL); + jack_set_process_callback(client, process_callback, NULL); + jack_on_shutdown(client, jack_shutdown_cb, NULL); - /* ------------------ channel 0 (the default channel) ------------------ */ - channels[0].active = 1; - atomic_store(&channels[0].state, STATE_IDLE); - channels[0].prev_state = -1; - channels[0].loop_count = 0; - channels[0].record_pos = 0; - channels[0].playback_pos = 0; - - channels[0].audio_in = jack_port_register(client, "input", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - channels[0].audio_out = jack_port_register(client, "output", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (!channels[0].audio_in || !channels[0].audio_out) { - fprintf(stderr, "Could not create audio ports for channel 0\n"); - return 1; - } - channel_count = 1; - - /* MIDI control & clock ports (shared across all channels) */ - midi_control_port = jack_port_register(client, "control", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - midi_clock_port = jack_port_register(client, "clock", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - if (!midi_control_port || !midi_clock_port) { - fprintf(stderr, "Could not create MIDI ports\n"); + if (looper_init(client) != 0) { + fprintf(stderr, "looper initialisation failed\n"); + jack_client_close(client); return 1; } if (jack_activate(client)) { fprintf(stderr, "Cannot activate client\n"); + jack_client_close(client); return 1; } fprintf(stderr, "looper running (client name '%s')\n", client_name); while (1) { - /* process pending add‑channel / remove‑channel commands - * (only safe outside the real‑time callback) */ - if (atomic_exchange(&cmd_add, 0)) { - int idx; - for (idx = 0; idx < MAX_CHANNELS; idx++) - if (!channels[idx].active) break; - if (idx < MAX_CHANNELS) { - channels[idx].active = 1; - atomic_store(&channels[idx].state, STATE_IDLE); - channels[idx].prev_state = -1; - channels[idx].loop_count = 0; - channels[idx].record_pos = 0; - channels[idx].playback_pos = 0; - - char in_name[64], out_name[64]; - snprintf(in_name, sizeof(in_name), - "channel%d_input", next_channel_id); - snprintf(out_name, sizeof(out_name), - "channel%d_output", next_channel_id); - - channels[idx].audio_in = - jack_port_register(client, in_name, - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - channels[idx].audio_out = - jack_port_register(client, out_name, - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (!channels[idx].audio_in || !channels[idx].audio_out) - fprintf(stderr, "Failed to register ports for channel %d\n", - next_channel_id); - next_channel_id++; - channel_count++; - } - } - - if (atomic_exchange(&cmd_remove, 0)) { - /* find the highest active channel index >0 */ - int remove_idx = -1; - for (int idx = 1; idx < MAX_CHANNELS; idx++) - if (channels[idx].active) remove_idx = idx; - if (remove_idx != -1) { - jack_port_unregister(client, channels[remove_idx].audio_in); - jack_port_unregister(client, channels[remove_idx].audio_out); - channels[remove_idx].active = 0; - channel_count--; - } - } - - sleep(1); + looper_process_commands(client); + usleep(50000); /* check commands every 50 ms */ } jack_client_close(client); diff --git a/src/midi.c b/src/midi.c new file mode 100644 index 0000000..095fc3d --- /dev/null +++ b/src/midi.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include "midi.h" +#include "channel.h" + +extern atomic_int control_key_active; +extern atomic_int cmd_add; +extern atomic_int cmd_remove; + +void midi_handle_events(void *port_buffer, jack_nframes_t nframes) +{ + (void)nframes; + jack_nframes_t nevents = jack_midi_get_event_count(port_buffer); + jack_midi_event_t ev; + + for (jack_nframes_t i = 0; i < nevents; i++) { + if (jack_midi_event_get(&ev, port_buffer, i) != 0) continue; + if (ev.size < 3) continue; + + unsigned char status = ev.buffer[0]; + unsigned char note = ev.buffer[1]; + unsigned char vel = ev.buffer[2]; + + /* note‑on */ + if ((status & 0xf0) == 0x90 && vel > 0) { + if (note == 64) { + atomic_store(&control_key_active, 1); + } else { + int ck = atomic_load(&control_key_active); + if (ck) { + atomic_store(&control_key_active, 0); + switch (note) { + case 60: atomic_store(&cmd_add, 1); break; + case 61: atomic_store(&cmd_remove, 1); break; + case 62: /* trigger looper – 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; + default: + break; + } + } 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 60: atomic_store(&cmd_add, 1); break; + case 61: atomic_store(&cmd_remove, 1); break; + default: + break; + } + } + } + } else if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) { + atomic_store(&control_key_active, 0); + } + } +} diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 0000000..08b5eeb --- /dev/null +++ b/src/midi.h @@ -0,0 +1,8 @@ +#ifndef MIDI_H +#define MIDI_H + +#include + +void midi_handle_events(void *port_buffer, jack_nframes_t nframes); + +#endif From b73719e8bca01d7337db98efcbb6ff36d3f9d503 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 21:08:37 +0000 Subject: [PATCH 08/31] fix: guard against NULL audio ports and defer channel activation until port registration succeeds Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 21 +++++++++++++-------- src/looper.c | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/channel.c b/src/channel.c index 9673317..0313260 100644 --- a/src/channel.c +++ b/src/channel.c @@ -6,13 +6,6 @@ void channel_add(jack_client_t *client, int idx) { - channels[idx].active = 1; - atomic_store(&channels[idx].state, STATE_IDLE); - channels[idx].prev_state = -1; - channels[idx].loop_count = 0; - channels[idx].record_pos = 0; - channels[idx].playback_pos = 0; - char in_name[64], out_name[64]; snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id); snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id); @@ -23,8 +16,20 @@ void channel_add(jack_client_t *client, int idx) channels[idx].audio_out = jack_port_register(client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - if (!channels[idx].audio_in || !channels[idx].audio_out) + if (!channels[idx].audio_in || !channels[idx].audio_out) { fprintf(stderr, "Failed to register ports for channel %d\n", next_channel_id); + /* Do NOT mark channel active – process loop will skip it */ + channels[idx].active = 0; + return; + } + + channels[idx].active = 1; + atomic_store(&channels[idx].state, STATE_IDLE); + channels[idx].prev_state = -1; + channels[idx].loop_count = 0; + channels[idx].record_pos = 0; + channels[idx].playback_pos = 0; + next_channel_id++; channel_count++; } diff --git a/src/looper.c b/src/looper.c index 5a8afaa..6c0bd53 100644 --- a/src/looper.c +++ b/src/looper.c @@ -35,6 +35,12 @@ int process_callback(jack_nframes_t nframes, void *arg) for (int c = 0; c < MAX_CHANNELS; c++) { if (!channels[c].active) continue; + /* Guard against NULL ports (e.g. if port registration failed) */ + if (!channels[c].audio_in || !channels[c].audio_out) { + fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c); + continue; + } + jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) jack_port_get_buffer(channels[c].audio_in, nframes); jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) From d10aeebd13a82c16398fb021f572be7213c1699a Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 21:09:13 +0000 Subject: [PATCH 09/31] fix: update segfault evaluation to reflect null pointer fixes Co-authored-by: aider (deepseek/deepseek-reasoner) --- evaluation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation.md b/evaluation.md index a68fd13..7943b9e 100644 --- a/evaluation.md +++ b/evaluation.md @@ -5,7 +5,7 @@ | 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 | ✅ OK | No obvious segfaults: all buffer accesses are bounds‑checked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. | +| 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. | From 74a190c70c613ba6ec7c7dcf49ff5154a0907f0a Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 21:11:22 +0000 Subject: [PATCH 10/31] #1: multichannel --- evalutation.md | 0 integration_test | Bin 30608 -> 0 bytes looper | Bin 25160 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 evalutation.md delete mode 100755 integration_test delete mode 100755 looper diff --git a/evalutation.md b/evalutation.md deleted file mode 100644 index e69de29..0000000 diff --git a/integration_test b/integration_test deleted file mode 100755 index cc334db5d7d2a0f1b253f73ac1538825d96174e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30608 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 KhD!1R|Nj@bc!AFV diff --git a/looper b/looper deleted file mode 100755 index 3bae2e14bb3360a80974060d549f1f4ff22753cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25160 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 P{^$oaM>s Date: Sat, 9 May 2026 09:57:36 +0000 Subject: [PATCH 11/31] fix: add null-checks for MIDI ports and use atomic access for channel active flag Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 4 +-- src/channel.h | 2 +- src/looper.c | 60 ++++++++++++++++++++++++--------------------- tests/integration.c | 1 + 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/channel.c b/src/channel.c index 0313260..0d94656 100644 --- a/src/channel.c +++ b/src/channel.c @@ -23,7 +23,7 @@ void channel_add(jack_client_t *client, int idx) return; } - channels[idx].active = 1; + atomic_store(&channels[idx].active, 1); atomic_store(&channels[idx].state, STATE_IDLE); channels[idx].prev_state = -1; channels[idx].loop_count = 0; @@ -38,6 +38,6 @@ void channel_remove(jack_client_t *client, int idx) { jack_port_unregister(client, channels[idx].audio_in); jack_port_unregister(client, channels[idx].audio_out); - channels[idx].active = 0; + atomic_store(&channels[idx].active, 0); channel_count--; } diff --git a/src/channel.h b/src/channel.h index d6f0d06..cb51698 100644 --- a/src/channel.h +++ b/src/channel.h @@ -21,7 +21,7 @@ struct channel_t { int loop_count; int record_pos; int playback_pos; - int active; + atomic_int active; jack_port_t *audio_in; jack_port_t *audio_out; }; diff --git a/src/looper.c b/src/looper.c index 6c0bd53..f72bf35 100644 --- a/src/looper.c +++ b/src/looper.c @@ -26,14 +26,16 @@ int process_callback(jack_nframes_t nframes, void *arg) { (void)arg; - void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); - if (midi_ctrl_buf) { - midi_handle_events(midi_ctrl_buf, nframes); + if (midi_control_port) { + void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); + if (midi_ctrl_buf) { + midi_handle_events(midi_ctrl_buf, nframes); + } } /* process each active channel */ for (int c = 0; c < MAX_CHANNELS; c++) { - if (!channels[c].active) continue; + if (!atomic_load(&channels[c].active)) continue; /* Guard against NULL ports (e.g. if port registration failed) */ if (!channels[c].audio_in || !channels[c].audio_out) { @@ -108,30 +110,32 @@ int process_callback(jack_nframes_t nframes, void *arg) } /* MIDI clock events – affect channel 0 only */ - void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); - if (midi_clock_buf) { - jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); - jack_midi_event_t cev; - for (jack_nframes_t j = 0; j < n_clock_events; j++) { - if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; - if (cev.size >= 1) { - unsigned char msg = cev.buffer[0]; - switch (msg) { - case 0xFA: { - int s = atomic_load(&channels[0].state); - if (s == STATE_IDLE) atomic_store(&channels[0].state, STATE_RECORD); - break; - } - case 0xFC: - atomic_store(&channels[0].state, STATE_IDLE); - break; - case 0xFB: { - int s = atomic_load(&channels[0].state); - if (s == STATE_PAUSED) atomic_store(&channels[0].state, STATE_LOOPING); - break; - } - default: - break; + if (midi_clock_port) { + void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); + if (midi_clock_buf) { + jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); + jack_midi_event_t cev; + for (jack_nframes_t j = 0; j < n_clock_events; j++) { + if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; + if (cev.size >= 1) { + unsigned char msg = cev.buffer[0]; + switch (msg) { + case 0xFA: { + int s = atomic_load(&channels[0].state); + if (s == STATE_IDLE) atomic_store(&channels[0].state, STATE_RECORD); + break; + } + case 0xFC: + atomic_store(&channels[0].state, STATE_IDLE); + break; + case 0xFB: { + int s = atomic_load(&channels[0].state); + if (s == STATE_PAUSED) atomic_store(&channels[0].state, STATE_LOOPING); + break; + } + default: + break; + } } } } diff --git a/tests/integration.c b/tests/integration.c index 6ae382b..6f88fe6 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -34,6 +34,7 @@ static unsigned char midi_inject_velocity = 0; static int midi_inject_process(jack_nframes_t nframes, void *arg) { (void)arg; + if (!midi_inject_port) return 0; void *port_buf = jack_port_get_buffer(midi_inject_port, nframes); if (!port_buf) return 0; jack_midi_clear_buffer(port_buf); From b0dda3d8ed74b2413445b229d70081bda8a7ed03 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 10:00:33 +0000 Subject: [PATCH 12/31] fix: defer port unregistration to avoid race condition in channel removal Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 5 ++--- src/looper.c | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/channel.c b/src/channel.c index 0d94656..713024f 100644 --- a/src/channel.c +++ b/src/channel.c @@ -19,7 +19,7 @@ void channel_add(jack_client_t *client, int idx) if (!channels[idx].audio_in || !channels[idx].audio_out) { fprintf(stderr, "Failed to register ports for channel %d\n", next_channel_id); /* Do NOT mark channel active – process loop will skip it */ - channels[idx].active = 0; + atomic_store(&channels[idx].active, 0); return; } @@ -36,8 +36,7 @@ void channel_add(jack_client_t *client, int idx) void channel_remove(jack_client_t *client, int idx) { - jack_port_unregister(client, channels[idx].audio_in); - jack_port_unregister(client, channels[idx].audio_out); + (void)client; atomic_store(&channels[idx].active, 0); channel_count--; } diff --git a/src/looper.c b/src/looper.c index f72bf35..564886e 100644 --- a/src/looper.c +++ b/src/looper.c @@ -19,6 +19,9 @@ jack_port_t *midi_control_port = NULL; jack_port_t *midi_clock_port = NULL; atomic_int control_key_active = 0; +/* Deferred removal index (1 second grace) */ +static int pending_unregister_idx = -1; + /* ---------------------------------------------------------------- * process callback * ---------------------------------------------------------------- */ @@ -198,6 +201,18 @@ int looper_init(jack_client_t *client) * ---------------------------------------------------------------- */ 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++) @@ -212,7 +227,9 @@ void looper_process_commands(jack_client_t *client) 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; } } } From 740ebaa969f9226dfb0a16d5d0431eff59491859 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 10:09:51 +0000 Subject: [PATCH 13/31] test: add integration tests for control-key modifier and channel removal Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 193 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/tests/integration.c b/tests/integration.c index 6f88fe6..11d0d47 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -415,6 +415,177 @@ static int test_multiple_channels(void) { return 0; } +/* test control‑key modifier (note 64 + note 62) */ +static int test_control_key_modifier(void) { + printf("Test: control‑key modifier triggers state transition via note 62\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_ctrl_key", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + /* connect same as in test_looper_looping but no beep generation */ + 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; + } + usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_ctrl_key:out"); + snprintf(my_in, sizeof(my_in), "test_ctrl_key: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; + } + /* First send note 64 (control key) */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 64 failed\n"); + return 1; + } + usleep(200000); + /* Now send note 62 (toggle channel 0) */ + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 62 failed\n"); + return 1; + } + /* Wait for looper to enter RECORD and detect audio */ + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.1f * sr); /* 0.1 second beep */ + 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; + } + usleep(200000); /* allow beep */ + /* send note 62 again under control key to move RECORD->LOOPING */ + 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 re‑send\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 62 for loop\n"); + return 1; + } + usleep(2000000); /* wait for a few loops */ + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + int got_bursts = bursts; + printf(" detected bursts: %d\n", got_bursts); + if (got_bursts < 3) { + fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts); + return 1; + } + printf(" PASS (control‑key modifier works)\n"); + return 0; +} + +/* test remove channel */ +static int test_remove_channel(void) { + printf("Test: dynamic channel removal via MIDI command\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_remove", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + /* add channel */ + if (send_jack_note_on("looper:control", 60, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 60 failed\n"); + return 1; + } + usleep(1500000); + /* verify channel1_input exists */ + 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); + } + if (!found) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: channel1_input not created\n"); + return 1; + } + printf(" channel1_input created\n"); + /* remove channel */ + if (send_jack_note_on("looper:control", 61, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 61 failed\n"); + return 1; + } + usleep(1500000); + /* verify channel1_input has disappeared */ + 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 (still_found) { + fprintf(stderr, " FAIL: channel1_input not removed after remove command\n"); + return 1; + } + printf(" PASS (channel removed)\n"); + return 0; +} + int main(void) { /* 1. binary must exist */ @@ -428,18 +599,36 @@ int main(void) { /* 3. Audio pass‑through test – must work for basic connectivity */ test_audio_pass_through(); + int failures = 0; + /* 4. Test that looping feature is now implemented */ if (test_looper_looping() != 0) { fprintf(stderr, " FAILED\n"); - return 1; + failures++; } /* 5. Test multiple dynamic channels */ if (test_multiple_channels() != 0) { fprintf(stderr, " FAILED\n"); - return 1; + failures++; } + /* 6. Test control‑key modifier */ + if (test_control_key_modifier() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + /* 7. Test channel removal */ + if (test_remove_channel() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + if (failures > 0) { + fprintf(stderr, "%d test(s) FAILED\n", failures); + return 1; + } printf("All tests completed successfully.\n"); return 0; } From 4bacab68c6ee310ce3dc0400f264f2cdf9ff4c8f Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 10:22:33 +0000 Subject: [PATCH 14/31] feat: implement bind feature for associating channels with MIDI notes Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/looper.c | 1 + src/midi.c | 52 +++++++++++-------- tests/integration.c | 121 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 151 insertions(+), 23 deletions(-) diff --git a/src/looper.c b/src/looper.c index 564886e..5eb0ec8 100644 --- a/src/looper.c +++ b/src/looper.c @@ -18,6 +18,7 @@ atomic_int cmd_remove = 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; /* Deferred removal index (1 second grace) */ static int pending_unregister_idx = -1; diff --git a/src/midi.c b/src/midi.c index 095fc3d..901455d 100644 --- a/src/midi.c +++ b/src/midi.c @@ -7,6 +7,7 @@ extern atomic_int control_key_active; extern atomic_int cmd_add; extern atomic_int cmd_remove; +extern atomic_int bind_channel; void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { @@ -30,30 +31,37 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) int ck = atomic_load(&control_key_active); if (ck) { atomic_store(&control_key_active, 0); - switch (note) { - case 60: atomic_store(&cmd_add, 1); break; - case 61: atomic_store(&cmd_remove, 1); break; - case 62: /* trigger looper – 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; + if (note < 16) { + atomic_store(&bind_channel, note); + } else { + switch (note) { + case 60: atomic_store(&cmd_add, 1); break; + case 61: atomic_store(&cmd_remove, 1); break; + case 62: /* trigger looper – channel via bind_channel */ + { + 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; + } + } } + break; + default: + break; } - break; - default: - break; } } else { /* direct mapping */ diff --git a/tests/integration.c b/tests/integration.c index 11d0d47..c0c79d5 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -515,6 +515,119 @@ static int test_control_key_modifier(void) { return 0; } +/* test bind channel */ +static int test_bind_channel(void) { + printf("Test: control‑key bind channel (note 0) and toggle\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_bind", 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; + } + usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_bind:out"); + snprintf(my_in, sizeof(my_in), "test_bind: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; + } + /* Send control key + note 0 to bind to channel 0 */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send control key failed\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 0, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send bind note 0 failed\n"); + return 1; + } + usleep(200000); + /* Now toggle using control+note62 – should toggle channel 0 */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send control key again failed\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send toggle note 62 failed\n"); + return 1; + } + /* Wait and detect bursts as before */ + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.1f * 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; + } + usleep(200000); /* allow beep */ + /* send control+note62 again to move RECORD->LOOPING */ + 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 for loop\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: toggle for loop\n"); + return 1; + } + usleep(2000000); + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + int got_bursts = bursts; + printf(" detected bursts: %d\n", got_bursts); + if (got_bursts < 3) { + fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts); + return 1; + } + printf(" PASS (bind and toggle)\n"); + return 0; +} + /* test remove channel */ static int test_remove_channel(void) { printf("Test: dynamic channel removal via MIDI command\n"); @@ -619,7 +732,13 @@ int main(void) { failures++; } - /* 7. Test channel removal */ + /* 7. Test bind channel */ + if (test_bind_channel() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + /* 8. Test channel removal */ if (test_remove_channel() != 0) { fprintf(stderr, " FAILED\n"); failures++; From 4ad16824f17d4a2d7ecec5db733d21bf58ca0fe6 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 10:28:25 +0000 Subject: [PATCH 15/31] docs: add multichannel documentation --- docs/1-multichannel.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/1-multichannel.md diff --git a/docs/1-multichannel.md b/docs/1-multichannel.md new file mode 100644 index 0000000..e69de29 From ac9ce26e9af3896b5fbad9c990adeb3974478bf5 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 10:28:26 +0000 Subject: [PATCH 16/31] docs: add multi-channel and bind feature documentation Co-authored-by: aider (deepseek/deepseek-reasoner) --- docs/1-multichannel.md | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/1-multichannel.md b/docs/1-multichannel.md index e69de29..a3d5503 100644 --- a/docs/1-multichannel.md +++ b/docs/1-multichannel.md @@ -0,0 +1,71 @@ +# Multi‑Channel & Bind Feature + +The looper supports up to 16 independent channels (numbered 0–15). +Channel 0 is always present and connected to the `looper:input` / `looper:output` audio ports. +Additional channels can be created and removed dynamically using MIDI commands. + +## MIDI Ports + +- **`looper:control`** – receives MIDI note‑on events for channel management and state toggling. +- **`looper:clock`** – receives MIDI clock messages (0xFA, 0xFC, 0xFB) that affect channel 0 only. + +## Control‑Key Modifier + +Hold the **control key** (MIDI note 64) pressed *before* sending another note to put the looper in “command mode”. +While control‑key is active, the next note‑on (with velocity > 0) performs a special action instead of its direct mapping. +The control key is released either by sending note‑off (note 64 or any note) or by sending a note‑on while control‑key is already active (the action is performed and control‑key is cleared). + +## Available Commands (under control key) + +| Note | Action | +|------|----------------------------------------------------------------------------------------------| +| 0–15 | **Bind** the next `control+62` toggle to the channel with that index. | +| 60 | **Add** a new dynamic channel (creates `channelX_input` / `channelX_output` ports). | +| 61 | **Remove** the highest‑numbered active channel (excluding channel 0). | +| 62 | **Toggle** the current bound channel through its state machine: | +| | IDLE → RECORD → LOOPING → PAUSED → LOOPING → … (each press advances one step). | + +> **Notes:** +> - The default bound channel is **0**. If you never send a bind command, `control+62` controls channel 0. +> - To bind a different channel, send `control + note <16>` (e.g., control + note 5 binds channel 5). +> - Bind is sticky – it stays until overwritten by another bind command. +> - There is **no unbind** command; you can rebind to channel 0 if needed. + +## Direct Mapping (without control key) + +For backward compatibility, the following notes work **without** the control‑key modifier: + +| Note | Action | +|------|----------------------------------------------------------------------------------------------| +| 1 | Toggle channel 0 state (IDLE→RECORD→LOOPING→PAUSED→LOOPING…). | +| 60 | Add a dynamic channel (same as `control+60`). | +| 61 | Remove the highest‑numbered active channel (same as `control+61`). | + +## Example Usage + +1. **Record a loop on channel 0 (using direct note 1)** + - Send note‑on, note 1, velocity 127 → channel 0 enters RECORD. + - Play some audio into `looper:input`. + - Send note‑on, note 1, velocity 127 again → channel 0 enters LOOPING. + - The recorded audio repeats indefinitely. + +2. **Use the control‑key to toggle channel 0** + - Send `note‑on, note 64` (control key). + - Then send `note‑on, note 62` → toggles channel 0 (IDLE→RECORD). + - Send `note‑on, note 64` again, then `note‑on, note 62` again → RECORD→LOOPING. + +3. **Add a new channel and bind it** + - Send `note‑on, note 64` + `note‑on, note 60` → creates channel 1. + - Send `note‑on, note 64` + `note‑on, note 1` → binds channel 1. + - Now `control+62` toggles channel 1 instead of channel 0. + - Record audio on channel 1 by sending `control+62` twice. + +4. **Remove a dynamic channel** + - Send `note‑on, note 64` + `note‑on, note 61` → removes the highest‑numbered active channel (e.g., channel 1). + +## Notes + +- The looper must be connected to a running JACK server. +- Channel buffers hold up to 5 seconds of audio at 48 kHz. +- After removal, the channel’s audio ports are unregistered on the next main‑loop cycle (deferred to avoid race conditions). +- The bind index is stored as an integer (0–15); values outside 0–15 are ignored (the note is processed as a command rather than a bind). From 60a8bdcfe814cb9c1ba6d68e6a2a1f94640b4a7c Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 11:14:52 +0000 Subject: [PATCH 17/31] feat: add unbind command (note 63) to reset bind_channel to 0 Co-authored-by: aider (deepseek/deepseek-reasoner) --- docs/1-multichannel.md | 3 +- src/midi.c | 3 + tests/integration.c | 136 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/docs/1-multichannel.md b/docs/1-multichannel.md index a3d5503..705720c 100644 --- a/docs/1-multichannel.md +++ b/docs/1-multichannel.md @@ -24,12 +24,13 @@ The control key is released either by sending note‑off (note 64 or any note) | 61 | **Remove** the highest‑numbered active channel (excluding channel 0). | | 62 | **Toggle** the current bound channel through its state machine: | | | IDLE → RECORD → LOOPING → PAUSED → LOOPING → … (each press advances one step). | +| 63 | **Unbind** – reset the bound channel back to **0**. | > **Notes:** > - The default bound channel is **0**. If you never send a bind command, `control+62` controls channel 0. > - To bind a different channel, send `control + note <16>` (e.g., control + note 5 binds channel 5). > - Bind is sticky – it stays until overwritten by another bind command. -> - There is **no unbind** command; you can rebind to channel 0 if needed. +> - To **unbind** (reset to channel 0), send `control + note 63`. ## Direct Mapping (without control key) diff --git a/src/midi.c b/src/midi.c index 901455d..df70369 100644 --- a/src/midi.c +++ b/src/midi.c @@ -59,6 +59,9 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) } } break; + case 63: /* unbind – reset bind to channel 0 */ + atomic_store(&bind_channel, 0); + break; default: break; } diff --git a/tests/integration.c b/tests/integration.c index c0c79d5..f27b1b5 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -628,6 +628,134 @@ static int test_bind_channel(void) { return 0; } +/* test unbind */ +static int test_bind_unbind(void) { + printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_unbind", 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; + } + usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_unbind:out"); + snprintf(my_in, sizeof(my_in), "test_unbind: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; + } + /* Bind to channel 5 */ + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send control key failed\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 5, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: bind to 5 failed\n"); + return 1; + } + usleep(200000); + /* Unbind (reset to 0) */ + 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 for unbind\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 63, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send unbind note 63 failed\n"); + return 1; + } + usleep(200000); + /* Now toggle with control+62 – should affect channel 0 */ + 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 for toggle\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: toggle note 62\n"); + return 1; + } + /* Wait for beep and loop */ + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.1f * 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; + } + usleep(200000); /* allow beep */ + /* second control+62 -> loop */ + 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 for loop\n"); + return 1; + } + usleep(200000); + if (send_jack_note_on("looper:control", 62, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: toggle for loop\n"); + return 1; + } + usleep(2000000); + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + int got_bursts = bursts; + printf(" detected bursts: %d\n", got_bursts); + if (got_bursts < 3) { + fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts); + return 1; + } + printf(" PASS (unbind works, toggle channel 0)\n"); + return 0; +} + /* test remove channel */ static int test_remove_channel(void) { printf("Test: dynamic channel removal via MIDI command\n"); @@ -738,7 +866,13 @@ int main(void) { failures++; } - /* 8. Test channel removal */ + /* 8. Test unbind */ + if (test_bind_unbind() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + /* 9. Test channel removal */ if (test_remove_channel() != 0) { fprintf(stderr, " FAILED\n"); failures++; From 5532b8cd5069a2a23d5bdfaeb735d8ef166a0699 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 11:36:38 +0000 Subject: [PATCH 18/31] chore: Add pre-push hooks --- makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/makefile b/makefile index 0556ab9..4bb2808 100644 --- a/makefile +++ b/makefile @@ -20,3 +20,10 @@ test: integration .PHONY: clean integration test clean: rm -f looper integration_test src/*.o + +check: + cppcheck --enable=all --error-exitcode=1 src/*.c + +# Optional: Format code using clang-format +format: + clang-format -i src/*.c From e7761c4b5301e55305d236c0b5f0f9da24e4cf5f Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 11:44:06 +0000 Subject: [PATCH 19/31] fix: replace usleep with nanosleep and fix const correctness in tests Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/integration.c b/tests/integration.c index f27b1b5..cf5eef5 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -10,6 +10,7 @@ #include #include #include +#include /* static variables for passthrough test */ static jack_port_t *passthrough_output_port = NULL; @@ -32,6 +33,13 @@ static jack_client_t *midi_inject_client = NULL; static unsigned char midi_inject_note = 0; static unsigned char midi_inject_velocity = 0; +static void safe_usleep(unsigned int usec) { + struct timespec ts; + ts.tv_sec = usec / 1000000; + ts.tv_nsec = (usec % 1000000) * 1000L; + nanosleep(&ts, NULL); +} + static int midi_inject_process(jack_nframes_t nframes, void *arg) { (void)arg; if (!midi_inject_port) return 0; @@ -57,8 +65,8 @@ static int passthrough_process(jack_nframes_t nframes, void *arg) { (void)arg; jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_output_port, nframes); - jack_default_audio_sample_t *in = - (jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_input_port, nframes); + const jack_default_audio_sample_t *in = + (const jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_input_port, nframes); if (!out || !in) return 0; float *outf = out; const float *inf = in; @@ -195,7 +203,7 @@ static int test_audio_pass_through(void) { waitpid(pid, NULL, 0); return 1; } - usleep(2200000); /* 2.2 seconds */ + safe_usleep(2200000); int saw_input = passthrough_done; double rms = passthrough_total_samples > 0 ? sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0; @@ -349,7 +357,7 @@ static int test_looper_looping(void) { } /* wait enough time for several loops (4 seconds to be safe) */ - usleep(4000000); + safe_usleep(4000000); jack_deactivate(client); jack_client_close(client); @@ -390,7 +398,7 @@ static int test_multiple_channels(void) { } /* wait long enough for the looper's main loop to process the add command (it sleeps for 1 second between checks, so 1.5 s is safe) */ - usleep(1500000); + safe_usleep(1500000); int found = 0; const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); @@ -500,7 +508,7 @@ static int test_control_key_modifier(void) { fprintf(stderr, " FAIL: send note 62 for loop\n"); return 1; } - usleep(2000000); /* wait for a few loops */ + safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); @@ -613,7 +621,7 @@ static int test_bind_channel(void) { fprintf(stderr, " FAIL: toggle for loop\n"); return 1; } - usleep(2000000); + safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); @@ -741,7 +749,7 @@ static int test_bind_unbind(void) { fprintf(stderr, " FAIL: toggle for loop\n"); return 1; } - usleep(2000000); + safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); @@ -776,7 +784,7 @@ static int test_remove_channel(void) { fprintf(stderr, " FAIL: send note 60 failed\n"); return 1; } - usleep(1500000); + safe_usleep(1500000); /* verify channel1_input exists */ const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); int found = 0; @@ -803,7 +811,7 @@ static int test_remove_channel(void) { fprintf(stderr, " FAIL: send note 61 failed\n"); return 1; } - usleep(1500000); + safe_usleep(1500000); /* verify channel1_input has disappeared */ ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); int still_found = 0; From e8d679c1af0f225c28619662848c11e47f262eae Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 11:45:32 +0000 Subject: [PATCH 20/31] fix: replace usleep with safe_usleep in integration tests Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/integration.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration.c b/tests/integration.c index cf5eef5..de4b3ff 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -321,7 +321,7 @@ static int test_looper_looping(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(500000); /* allow state to change (500ms) */ + safe_usleep(500000); /* allow state to change (500ms) */ int sr = jack_get_sample_rate(client); continuous_sine = 0; /* disable continuous tone */ @@ -345,10 +345,10 @@ static int test_looper_looping(void) { return 1; } - usleep(150000); /* let beep start */ + safe_usleep(150000); /* let beep start */ /* ensure beep is fully captured */ - usleep(800000); /* 0.8s after start of beep */ + safe_usleep(800000); /* 0.8s after start of beep */ if (send_jack_note_on("looper:control", 1, 127) != 0) { jack_client_close(client); From 1db9735e1bccb6a3963d8bfa3075ea8001af9c4b Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 12:11:55 +0000 Subject: [PATCH 21/31] style: reformat code and reorder includes in looper.c and main.c --- src/looper.c | 379 ++++++++++++++++++++++++++------------------------- src/main.c | 79 ++++++----- 2 files changed, 230 insertions(+), 228 deletions(-) diff --git a/src/looper.c b/src/looper.c index 5eb0ec8..2c52255 100644 --- a/src/looper.c +++ b/src/looper.c @@ -1,24 +1,24 @@ -#include -#include -#include -#include -#include -#include -#include #include "looper.h" #include "channel.h" #include "midi.h" +#include +#include +#include +#include +#include +#include +#include /* Global state (shared across files) */ 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; +atomic_int channel_count = 0; +int next_channel_id = 1; +atomic_int cmd_add = 0; +atomic_int cmd_remove = 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; +atomic_int control_key_active = 0; +atomic_int bind_channel = 0; /* Deferred removal index (1 second grace) */ static int pending_unregister_idx = -1; @@ -26,211 +26,214 @@ static int pending_unregister_idx = -1; /* ---------------------------------------------------------------- * process callback * ---------------------------------------------------------------- */ -int process_callback(jack_nframes_t nframes, void *arg) -{ - (void)arg; +int process_callback(jack_nframes_t nframes, void *arg) { + (void)arg; - if (midi_control_port) { - void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); - if (midi_ctrl_buf) { - midi_handle_events(midi_ctrl_buf, nframes); - } + if (midi_control_port) { + void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); + if (midi_ctrl_buf) { + midi_handle_events(midi_ctrl_buf, nframes); + } + } + + /* process each active channel */ + for (int c = 0; c < MAX_CHANNELS; c++) { + if (!atomic_load(&channels[c].active)) + continue; + + /* Guard against NULL ports (e.g. if port registration failed) */ + if (!channels[c].audio_in || !channels[c].audio_out) { + fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c); + continue; } - /* process each active channel */ - for (int c = 0; c < MAX_CHANNELS; c++) { - if (!atomic_load(&channels[c].active)) continue; + jack_default_audio_sample_t *in = + (jack_default_audio_sample_t *)jack_port_get_buffer( + channels[c].audio_in, nframes); + jack_default_audio_sample_t *out = + (jack_default_audio_sample_t *)jack_port_get_buffer( + channels[c].audio_out, nframes); + if (!out) + continue; - /* Guard against NULL ports (e.g. if port registration failed) */ - if (!channels[c].audio_in || !channels[c].audio_out) { - fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c); - continue; - } + int state = atomic_load(&channels[c].state); - jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) - jack_port_get_buffer(channels[c].audio_in, nframes); - jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) - jack_port_get_buffer(channels[c].audio_out, nframes); - if (!out) continue; - - int state = atomic_load(&channels[c].state); - - if (state != channels[c].prev_state) { - switch (state) { - case STATE_RECORD: - channels[c].record_pos = 0; - channels[c].loop_count = 0; - break; - case STATE_LOOPING: - if (channels[c].record_pos > 0) - channels[c].loop_count = channels[c].record_pos; - channels[c].playback_pos = 0; - break; - default: - break; - } - } - - jack_nframes_t i; - switch (state) { - case STATE_RECORD: - if (in) { - for (i = 0; i < nframes; i++) { - if (channels[c].record_pos < LOOP_BUF_SIZE) - channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i]; - ((float *)out)[i] = ((const float *)in)[i]; - } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - - case STATE_LOOPING: - if (channels[c].loop_count > 0) { - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; - channels[c].playback_pos = (channels[c].playback_pos + 1) % channels[c].loop_count; - } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - - case STATE_PAUSED: - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - break; - - default: /* IDLE */ - if (in) { - memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; - } - - channels[c].prev_state = state; + if (state != channels[c].prev_state) { + switch (state) { + case STATE_RECORD: + channels[c].record_pos = 0; + channels[c].loop_count = 0; + break; + case STATE_LOOPING: + if (channels[c].record_pos > 0) + channels[c].loop_count = channels[c].record_pos; + channels[c].playback_pos = 0; + break; + default: + break; + } } - /* MIDI clock events – affect channel 0 only */ - if (midi_clock_port) { - void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); - if (midi_clock_buf) { - jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); - jack_midi_event_t cev; - for (jack_nframes_t j = 0; j < n_clock_events; j++) { - if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; - if (cev.size >= 1) { - unsigned char msg = cev.buffer[0]; - switch (msg) { - case 0xFA: { - int s = atomic_load(&channels[0].state); - if (s == STATE_IDLE) atomic_store(&channels[0].state, STATE_RECORD); - break; - } - case 0xFC: - atomic_store(&channels[0].state, STATE_IDLE); - break; - case 0xFB: { - int s = atomic_load(&channels[0].state); - if (s == STATE_PAUSED) atomic_store(&channels[0].state, STATE_LOOPING); - break; - } - default: - break; - } - } - } + jack_nframes_t i; + switch (state) { + case STATE_RECORD: + if (in) { + for (i = 0; i < nframes; i++) { + if (channels[c].record_pos < LOOP_BUF_SIZE) + channels[c].loop_buffer[channels[c].record_pos++] = + ((const float *)in)[i]; + ((float *)out)[i] = ((const float *)in)[i]; } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + + case STATE_LOOPING: + if (channels[c].loop_count > 0) { + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; + channels[c].playback_pos = + (channels[c].playback_pos + 1) % channels[c].loop_count; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + + case STATE_PAUSED: + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + break; + + default: /* IDLE */ + if (in) { + memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; } - return 0; + channels[c].prev_state = state; + } + + /* MIDI clock events – affect channel 0 only */ + if (midi_clock_port) { + void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); + if (midi_clock_buf) { + jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); + jack_midi_event_t cev; + for (jack_nframes_t j = 0; j < n_clock_events; j++) { + if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) + continue; + if (cev.size >= 1) { + unsigned char msg = cev.buffer[0]; + switch (msg) { + case 0xFA: { + int s = atomic_load(&channels[0].state); + if (s == STATE_IDLE) + atomic_store(&channels[0].state, STATE_RECORD); + break; + } + case 0xFC: + atomic_store(&channels[0].state, STATE_IDLE); + break; + case 0xFB: { + int s = atomic_load(&channels[0].state); + if (s == STATE_PAUSED) + atomic_store(&channels[0].state, STATE_LOOPING); + break; + } + default: + break; + } + } + } + } + } + + return 0; } /* ---------------------------------------------------------------- * shutdown callback * ---------------------------------------------------------------- */ -void jack_shutdown_cb(void *arg) -{ - (void)arg; - fprintf(stderr, "JACK shutdown\n"); - exit(0); +void jack_shutdown_cb(void *arg) { + (void)arg; + fprintf(stderr, "JACK shutdown\n"); + exit(0); } /* ---------------------------------------------------------------- * looper initialisation * ---------------------------------------------------------------- */ -int looper_init(jack_client_t *client) -{ - /* channel 0 */ - channels[0].active = 1; - atomic_store(&channels[0].state, STATE_IDLE); - channels[0].prev_state = -1; - channels[0].loop_count = 0; - channels[0].record_pos = 0; - channels[0].playback_pos = 0; +int looper_init(jack_client_t *client) { + /* channel 0 */ + channels[0].active = 1; + atomic_store(&channels[0].state, STATE_IDLE); + channels[0].prev_state = -1; + channels[0].loop_count = 0; + channels[0].record_pos = 0; + channels[0].playback_pos = 0; - channels[0].audio_in = jack_port_register(client, "input", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - channels[0].audio_out = jack_port_register(client, "output", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (!channels[0].audio_in || !channels[0].audio_out) { - fprintf(stderr, "Could not create audio ports for channel 0\n"); - return -1; - } - channel_count = 1; + channels[0].audio_in = jack_port_register( + client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + channels[0].audio_out = jack_port_register( + client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (!channels[0].audio_in || !channels[0].audio_out) { + fprintf(stderr, "Could not create audio ports for channel 0\n"); + return -1; + } + channel_count = 1; - midi_control_port = jack_port_register(client, "control", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - midi_clock_port = jack_port_register(client, "clock", - JACK_DEFAULT_MIDI_TYPE, - JackPortIsInput, 0); - if (!midi_control_port || !midi_clock_port) { - fprintf(stderr, "Could not create MIDI ports\n"); - return -1; - } + midi_control_port = jack_port_register( + client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + midi_clock_port = jack_port_register(client, "clock", JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + if (!midi_control_port || !midi_clock_port) { + fprintf(stderr, "Could not create MIDI ports\n"); + return -1; + } - return 0; + return 0; } /* ---------------------------------------------------------------- * 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; - } +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); - } + 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); } + } - 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; - } + 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; } + } } diff --git a/src/main.c b/src/main.c index 06704ca..fee36a8 100644 --- a/src/main.c +++ b/src/main.c @@ -1,50 +1,49 @@ +#include "looper.h" +#include #include #include #include -#include -#include "looper.h" -int main(int argc, char *argv[]) -{ - (void)argc; - (void)argv; - const char *client_name = "looper"; - jack_options_t options = JackNullOption; - jack_status_t status; +int main(int argc, char *argv[]) { + (void)argc; + (void)argv; + const char *client_name = "looper"; + jack_options_t options = JackNullOption; + jack_status_t status; - jack_client_t *client = jack_client_open(client_name, options, &status); - if (client == NULL) { - fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); - if (status & JackServerFailed) - fprintf(stderr, "Unable to connect to JACK server\n"); - return 1; - } + jack_client_t *client = jack_client_open(client_name, options, &status); + if (client == NULL) { + fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); + if (status & JackServerFailed) + fprintf(stderr, "Unable to connect to JACK server\n"); + return 1; + } - if (status & JackNameNotUnique) - client_name = jack_get_client_name(client); + if (status & JackNameNotUnique) + client_name = jack_get_client_name(client); - jack_set_process_callback(client, process_callback, NULL); - jack_on_shutdown(client, jack_shutdown_cb, NULL); - - if (looper_init(client) != 0) { - fprintf(stderr, "looper initialisation failed\n"); - jack_client_close(client); - return 1; - } - - if (jack_activate(client)) { - fprintf(stderr, "Cannot activate client\n"); - jack_client_close(client); - return 1; - } - - fprintf(stderr, "looper running (client name '%s')\n", client_name); - - while (1) { - looper_process_commands(client); - usleep(50000); /* check commands every 50 ms */ - } + jack_set_process_callback(client, process_callback, NULL); + jack_on_shutdown(client, jack_shutdown_cb, NULL); + if (looper_init(client) != 0) { + fprintf(stderr, "looper initialisation failed\n"); jack_client_close(client); - return 0; + return 1; + } + + if (jack_activate(client)) { + fprintf(stderr, "Cannot activate client\n"); + jack_client_close(client); + return 1; + } + + fprintf(stderr, "looper running (client name '%s')\n", client_name); + + while (1) { + looper_process_commands(client); + usleep(50000); /* check commands every 50 ms */ + } + + jack_client_close(client); + return 0; } From 7e9224cdc700eb89380909962b4144070003ce07 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 12:11:57 +0000 Subject: [PATCH 22/31] fix: replace usleep with nanosleep and fix const correctness Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/looper.c | 5 +++-- src/main.c | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/looper.c b/src/looper.c index 2c52255..e343b77 100644 --- a/src/looper.c +++ b/src/looper.c @@ -47,8 +47,8 @@ int process_callback(jack_nframes_t nframes, void *arg) { continue; } - jack_default_audio_sample_t *in = - (jack_default_audio_sample_t *)jack_port_get_buffer( + const jack_default_audio_sample_t *in = + (const jack_default_audio_sample_t *)jack_port_get_buffer( channels[c].audio_in, nframes); jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)jack_port_get_buffer( @@ -82,6 +82,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { if (channels[c].record_pos < LOOP_BUF_SIZE) channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i]; + // cppcheck-suppress unreadVariable ((float *)out)[i] = ((const float *)in)[i]; } } else { diff --git a/src/main.c b/src/main.c index fee36a8..b5c816c 100644 --- a/src/main.c +++ b/src/main.c @@ -3,6 +3,7 @@ #include #include #include +#include int main(int argc, char *argv[]) { (void)argc; @@ -41,7 +42,7 @@ int main(int argc, char *argv[]) { while (1) { looper_process_commands(client); - usleep(50000); /* 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); From d2e39f34515c2443dacb46ccba0769276b2af737 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 12:38:35 +0000 Subject: [PATCH 23/31] chore: add install-hooks target and update cppcheck library flag --- makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index 4bb2808..7c28436 100644 --- a/makefile +++ b/makefile @@ -22,8 +22,11 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 src/*.c + cppcheck --enable=all --error-exitcode=1 src/*.c --library=posix . # Optional: Format code using clang-format format: clang-format -i src/*.c + +install-hooks: + git config core.hooksPath .githooks From c2df024350e13ffe8836063af72b4129bc1309a9 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 12:46:31 +0000 Subject: [PATCH 24/31] style: reformat code and update cppcheck suppressions --- makefile | 2 +- src/channel.c | 69 ++++++++++--------- src/midi.c | 178 ++++++++++++++++++++++++++------------------------ 3 files changed, 127 insertions(+), 122 deletions(-) diff --git a/makefile b/makefile index 7c28436..9009737 100644 --- a/makefile +++ b/makefile @@ -22,7 +22,7 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 src/*.c --library=posix . + cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=usleepCalled --suppress=unreadVariable --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . # Optional: Format code using clang-format format: diff --git a/src/channel.c b/src/channel.c index 713024f..e68e371 100644 --- a/src/channel.c +++ b/src/channel.c @@ -1,42 +1,39 @@ -#include -#include +#include "channel.h" #include #include -#include "channel.h" +#include +#include -void channel_add(jack_client_t *client, int idx) -{ - char in_name[64], out_name[64]; - snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id); - snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id); +void channel_add(jack_client_t *client, int idx) { + char in_name[64], out_name[64]; + snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id); + snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id); - channels[idx].audio_in = jack_port_register(client, in_name, - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - channels[idx].audio_out = jack_port_register(client, out_name, - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (!channels[idx].audio_in || !channels[idx].audio_out) { - fprintf(stderr, "Failed to register ports for channel %d\n", next_channel_id); - /* Do NOT mark channel active – process loop will skip it */ - atomic_store(&channels[idx].active, 0); - return; - } - - atomic_store(&channels[idx].active, 1); - atomic_store(&channels[idx].state, STATE_IDLE); - channels[idx].prev_state = -1; - channels[idx].loop_count = 0; - channels[idx].record_pos = 0; - channels[idx].playback_pos = 0; - - next_channel_id++; - channel_count++; -} - -void channel_remove(jack_client_t *client, int idx) -{ - (void)client; + channels[idx].audio_in = jack_port_register( + client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + channels[idx].audio_out = jack_port_register( + client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (!channels[idx].audio_in || !channels[idx].audio_out) { + fprintf(stderr, "Failed to register ports for channel %d\n", + next_channel_id); + /* Do NOT mark channel active – process loop will skip it */ atomic_store(&channels[idx].active, 0); - channel_count--; + return; + } + + atomic_store(&channels[idx].active, 1); + atomic_store(&channels[idx].state, STATE_IDLE); + channels[idx].prev_state = -1; + channels[idx].loop_count = 0; + channels[idx].record_pos = 0; + channels[idx].playback_pos = 0; + + next_channel_id++; + channel_count++; +} + +void channel_remove(jack_client_t *client, int idx) { + (void)client; + atomic_store(&channels[idx].active, 0); + channel_count--; } diff --git a/src/midi.c b/src/midi.c index df70369..e3471cf 100644 --- a/src/midi.c +++ b/src/midi.c @@ -1,102 +1,110 @@ +#include "midi.h" +#include "channel.h" #include #include #include -#include "midi.h" -#include "channel.h" extern atomic_int control_key_active; extern atomic_int cmd_add; extern atomic_int cmd_remove; extern atomic_int bind_channel; -void midi_handle_events(void *port_buffer, jack_nframes_t nframes) -{ - (void)nframes; - jack_nframes_t nevents = jack_midi_get_event_count(port_buffer); - jack_midi_event_t ev; +void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { + (void)nframes; + jack_nframes_t nevents = jack_midi_get_event_count(port_buffer); + jack_midi_event_t ev; - for (jack_nframes_t i = 0; i < nevents; i++) { - if (jack_midi_event_get(&ev, port_buffer, i) != 0) continue; - if (ev.size < 3) continue; + for (jack_nframes_t i = 0; i < nevents; i++) { + if (jack_midi_event_get(&ev, port_buffer, i) != 0) + continue; + if (ev.size < 3) + continue; - unsigned char status = ev.buffer[0]; - unsigned char note = ev.buffer[1]; - unsigned char vel = ev.buffer[2]; + unsigned char status = ev.buffer[0]; + unsigned char note = ev.buffer[1]; + unsigned char vel = ev.buffer[2]; - /* note‑on */ - if ((status & 0xf0) == 0x90 && vel > 0) { - if (note == 64) { - atomic_store(&control_key_active, 1); - } else { - int ck = atomic_load(&control_key_active); - if (ck) { - atomic_store(&control_key_active, 0); - if (note < 16) { - atomic_store(&bind_channel, note); - } else { - switch (note) { - case 60: atomic_store(&cmd_add, 1); break; - case 61: atomic_store(&cmd_remove, 1); break; - case 62: /* trigger looper – channel via bind_channel */ - { - 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; - } - } - } - break; - case 63: /* unbind – reset bind to channel 0 */ - atomic_store(&bind_channel, 0); - break; - default: - break; - } - } - } 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 60: atomic_store(&cmd_add, 1); break; - case 61: atomic_store(&cmd_remove, 1); break; - default: - break; - } + /* note‑on */ + if ((status & 0xf0) == 0x90 && vel > 0) { + if (note == 64) { + atomic_store(&control_key_active, 1); + } else { + int ck = atomic_load(&control_key_active); + if (ck) { + atomic_store(&control_key_active, 0); + if (note < 16) { + atomic_store(&bind_channel, note); + } else { + switch (note) { + case 60: + atomic_store(&cmd_add, 1); + break; + case 61: + atomic_store(&cmd_remove, 1); + break; + case 62: /* trigger looper – channel via bind_channel */ + { + 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; } + } + } break; + case 63: /* unbind – reset bind to channel 0 */ + atomic_store(&bind_channel, 0); + break; + default: + break; } - } else if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) { - atomic_store(&control_key_active, 0); + } + } 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 60: + atomic_store(&cmd_add, 1); + break; + case 61: + atomic_store(&cmd_remove, 1); + break; + default: + break; + } } + } + } else if ((status & 0xf0) == 0x80 || + ((status & 0xf0) == 0x90 && vel == 0)) { + atomic_store(&control_key_active, 0); } + } } From aa0b191b460561e65cc0f1cdfae40601d817800a Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 12:46:50 +0000 Subject: [PATCH 25/31] makefile ```makefile <<<<<<< SEARCH check: cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=usleepCalled --suppress=unreadVariable --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . ======= check: cppcheck --enable=all --error-exitcode=1 --suppress=unreadVariable src/*.c --library=posix . >>>>>>> REPLACE ``` src/channel.c ```c <<<<<<< SEARCH #include "channel.h" #include #include #include #include ======= // cppcheck-suppress missingIncludeSystem #include "channel.h" #include #include #include #include >>>>>>> REPLACE ``` src/channel.h ```c <<<<<<< SEARCH #include #include ======= // cppcheck-suppress missingIncludeSystem #include #include >>>>>>> REPLACE ``` src/looper.c ```c <<<<<<< SEARCH #include "looper.h" #include "channel.h" #include "midi.h" #include #include #include #include #include #include #include ======= // cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "midi.h" #include #include #include #include #include #include #include >>>>>>> REPLACE ``` src/looper.h ```c <<<<<<< SEARCH #include ======= // cppcheck-suppress missingIncludeSystem #include >>>>>>> REPLACE ``` src/main.c ```c <<<<<<< SEARCH #include "looper.h" #include #include #include #include #include ======= // cppcheck-suppress missingIncludeSystem #include "looper.h" #include #include #include #include #include >>>>>>> REPLACE ``` src/midi.c ```c <<<<<<< SEARCH #include "midi.h" #include "channel.h" #include #include #include ======= // cppcheck-suppress missingIncludeSystem #include "midi.h" #include "channel.h" #include #include #include >>>>>>> REPLACE ``` src/midi.h ```c <<<<<<< SEARCH #include ======= // cppcheck-suppress missingIncludeSystem #include >>>>>>> REPLACE ``` tests/integration.c ```c <<<<<<< SEARCH #include #include #include #include #include #include #include #include #include #include #include #include #include ======= // cppcheck-suppress missingIncludeSystem // cppcheck-suppress usleepCalled // cppcheck-suppress normalCheckLevelMaxBranches #include #include #include #include #include #include #include #include #include #include #include #include #include >>>>>>> REPLACE ``` Co-authored-by: aider (deepseek/deepseek-reasoner) --- makefile | 2 +- src/channel.c | 1 + src/channel.h | 1 + src/looper.c | 1 + src/looper.h | 1 + src/main.c | 1 + src/midi.c | 1 + src/midi.h | 1 + tests/integration.c | 3 +++ 9 files changed, 11 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index 9009737..e291721 100644 --- a/makefile +++ b/makefile @@ -22,7 +22,7 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=usleepCalled --suppress=unreadVariable --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . + cppcheck --enable=all --error-exitcode=1 --suppress=unreadVariable src/*.c --library=posix . # Optional: Format code using clang-format format: diff --git a/src/channel.c b/src/channel.c index e68e371..8eaf4d7 100644 --- a/src/channel.c +++ b/src/channel.c @@ -1,3 +1,4 @@ +// cppcheck-suppress missingIncludeSystem #include "channel.h" #include #include diff --git a/src/channel.h b/src/channel.h index cb51698..a0c2f89 100644 --- a/src/channel.h +++ b/src/channel.h @@ -1,6 +1,7 @@ #ifndef CHANNEL_H #define CHANNEL_H +// cppcheck-suppress missingIncludeSystem #include #include diff --git a/src/looper.c b/src/looper.c index e343b77..299a8a2 100644 --- a/src/looper.c +++ b/src/looper.c @@ -1,3 +1,4 @@ +// cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "midi.h" diff --git a/src/looper.h b/src/looper.h index 7fb68d4..9e064a5 100644 --- a/src/looper.h +++ b/src/looper.h @@ -1,6 +1,7 @@ #ifndef LOOPER_H #define LOOPER_H +// cppcheck-suppress missingIncludeSystem #include /* Initialisation – must be called after setting process callback */ diff --git a/src/main.c b/src/main.c index b5c816c..9a82edd 100644 --- a/src/main.c +++ b/src/main.c @@ -1,3 +1,4 @@ +// cppcheck-suppress missingIncludeSystem #include "looper.h" #include #include diff --git a/src/midi.c b/src/midi.c index e3471cf..bac71c5 100644 --- a/src/midi.c +++ b/src/midi.c @@ -1,3 +1,4 @@ +// cppcheck-suppress missingIncludeSystem #include "midi.h" #include "channel.h" #include diff --git a/src/midi.h b/src/midi.h index 08b5eeb..d5e57d2 100644 --- a/src/midi.h +++ b/src/midi.h @@ -1,6 +1,7 @@ #ifndef MIDI_H #define MIDI_H +// cppcheck-suppress missingIncludeSystem #include void midi_handle_events(void *port_buffer, jack_nframes_t nframes); diff --git a/tests/integration.c b/tests/integration.c index de4b3ff..07f144b 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -1,3 +1,6 @@ +// cppcheck-suppress missingIncludeSystem +// cppcheck-suppress usleepCalled +// cppcheck-suppress normalCheckLevelMaxBranches #include #include #include From b02c45e7abd9b5e9dc72cb340034adf57da69ecc Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 13:19:08 +0000 Subject: [PATCH 26/31] fix: suppress cppcheck warnings for missing includes and usleep Co-authored-by: aider (deepseek/deepseek-reasoner) --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index e291721..5d708a5 100644 --- a/makefile +++ b/makefile @@ -22,7 +22,7 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 --suppress=unreadVariable src/*.c --library=posix . + cppcheck --enable=all --error-exitcode=1 --suppress=unreadVariable --suppress=missingIncludeSystem --suppress=usleepCalled --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . # Optional: Format code using clang-format format: From 982dd4dc02a249da9af78fca7fbf030b583b2b32 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 13:20:36 +0000 Subject: [PATCH 27/31] refactor: replace usleep with safe_usleep in integration tests --- tests/integration.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/integration.c b/tests/integration.c index 07f144b..0119b0e 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -168,7 +168,7 @@ static int test_audio_pass_through(void) { waitpid(pid, NULL, 0); return 1; } - usleep(200000); + safe_usleep(200000); const char *looper_input = "looper:input"; const char *looper_output = "looper:output"; char my_output[64], my_input[64]; @@ -262,7 +262,7 @@ static int send_jack_note_on(const char *target_port, unsigned char note, unsign } /* wait for the process callback to clear the flag (event delivered) */ for (int attempts = 0; attempts < 50; attempts++) { /* ~50 * 10ms = 500ms */ - usleep(10000); + safe_usleep(10000); if (!midi_inject_pending) break; } jack_deactivate(midi_inject_client); @@ -306,7 +306,7 @@ static int test_looper_looping(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); /* wait for ports to appear */ + safe_usleep(200000); /* wait for ports to appear */ /* connect test:out -> looper:input, looper:output -> test:in */ char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_looping:out"); @@ -451,7 +451,7 @@ static int test_control_key_modifier(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); + safe_usleep(200000); char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_ctrl_key:out"); snprintf(my_in, sizeof(my_in), "test_ctrl_key:in"); @@ -468,7 +468,7 @@ static int test_control_key_modifier(void) { fprintf(stderr, " FAIL: send note 64 failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); /* Now send note 62 (toggle channel 0) */ if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); @@ -496,7 +496,7 @@ static int test_control_key_modifier(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); /* allow beep */ + safe_usleep(200000); /* allow beep */ /* send note 62 again under control key to move RECORD->LOOPING */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -504,7 +504,7 @@ static int test_control_key_modifier(void) { fprintf(stderr, " FAIL: control key re‑send\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); @@ -550,7 +550,7 @@ static int test_bind_channel(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); + safe_usleep(200000); char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_bind:out"); snprintf(my_in, sizeof(my_in), "test_bind:in"); @@ -567,14 +567,14 @@ static int test_bind_channel(void) { fprintf(stderr, " FAIL: send control key failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 0, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: send bind note 0 failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); /* Now toggle using control+note62 – should toggle channel 0 */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -582,7 +582,7 @@ static int test_bind_channel(void) { fprintf(stderr, " FAIL: send control key again failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); @@ -609,7 +609,7 @@ static int test_bind_channel(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); /* allow beep */ + safe_usleep(200000); /* allow beep */ /* send control+note62 again to move RECORD->LOOPING */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -617,7 +617,7 @@ static int test_bind_channel(void) { fprintf(stderr, " FAIL: control key for loop\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); @@ -663,7 +663,7 @@ static int test_bind_unbind(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); + safe_usleep(200000); char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_unbind:out"); snprintf(my_in, sizeof(my_in), "test_unbind:in"); @@ -680,14 +680,14 @@ static int test_bind_unbind(void) { fprintf(stderr, " FAIL: send control key failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 5, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: bind to 5 failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); /* Unbind (reset to 0) */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -695,14 +695,14 @@ static int test_bind_unbind(void) { fprintf(stderr, " FAIL: control key for unbind\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 63, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: send unbind note 63 failed\n"); return 1; } - usleep(200000); + safe_usleep(200000); /* Now toggle with control+62 – should affect channel 0 */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -710,7 +710,7 @@ static int test_bind_unbind(void) { fprintf(stderr, " FAIL: control key for toggle\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); @@ -737,7 +737,7 @@ static int test_bind_unbind(void) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } - usleep(200000); /* allow beep */ + safe_usleep(200000); /* allow beep */ /* second control+62 -> loop */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); @@ -745,7 +745,7 @@ static int test_bind_unbind(void) { fprintf(stderr, " FAIL: control key for loop\n"); return 1; } - usleep(200000); + safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); From 2fa1d159e7dfc7dbf940c6b82c513e27edf7ded2 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 13:20:37 +0000 Subject: [PATCH 28/31] refactor: remove global cppcheck suppressions and move usleep suppression locally Co-authored-by: aider (deepseek/deepseek-reasoner) --- makefile | 2 +- tests/integration.c | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/makefile b/makefile index 5d708a5..bc98f22 100644 --- a/makefile +++ b/makefile @@ -22,7 +22,7 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 --suppress=unreadVariable --suppress=missingIncludeSystem --suppress=usleepCalled --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . + cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . # Optional: Format code using clang-format format: diff --git a/tests/integration.c b/tests/integration.c index 0119b0e..1d5f446 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -1,6 +1,3 @@ -// cppcheck-suppress missingIncludeSystem -// cppcheck-suppress usleepCalled -// cppcheck-suppress normalCheckLevelMaxBranches #include #include #include From 934843e9b1b57cb576785e1c496cabee05059057 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 13:23:32 +0000 Subject: [PATCH 29/31] fix: move cppcheck suppression comment to same line as assignment Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/looper.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/looper.c b/src/looper.c index 299a8a2..d045092 100644 --- a/src/looper.c +++ b/src/looper.c @@ -83,8 +83,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { if (channels[c].record_pos < LOOP_BUF_SIZE) channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i]; - // cppcheck-suppress unreadVariable - ((float *)out)[i] = ((const float *)in)[i]; + ((float *)out)[i] = ((const float *)in)[i]; // cppcheck-suppress unreadVariable } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); From 20c0820910f5967c1a14abbf430db151639d5aa7 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 14:46:09 +0000 Subject: [PATCH 30/31] refactor: use explicit pointer casts to clarify type conversions Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/looper.c | 6 ++++-- tests/integration.c | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/looper.c b/src/looper.c index d045092..dae0dd1 100644 --- a/src/looper.c +++ b/src/looper.c @@ -79,11 +79,13 @@ int process_callback(jack_nframes_t nframes, void *arg) { switch (state) { case STATE_RECORD: if (in) { + float *f_out = (float *)out; + 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++] = - ((const float *)in)[i]; - ((float *)out)[i] = ((const float *)in)[i]; // cppcheck-suppress unreadVariable + f_in[i]; + f_out[i] = f_in[i]; } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); diff --git a/tests/integration.c b/tests/integration.c index 1d5f446..1d4eea2 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -68,8 +68,8 @@ static int passthrough_process(jack_nframes_t nframes, void *arg) { const jack_default_audio_sample_t *in = (const jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_input_port, nframes); if (!out || !in) return 0; - float *outf = out; - const float *inf = in; + float *f_out = (float *)out; + const float *f_in = (const float *)in; for (jack_nframes_t i = 0; i < nframes; i++) { /* generate beep while beep_remaining > 0 or continuous sine */ float out_val; @@ -82,17 +82,17 @@ static int passthrough_process(jack_nframes_t nframes, void *arg) { } else { out_val = 0.0f; } - outf[i] = out_val; + f_out[i] = out_val; /* detect bursts on the input (looper output) */ - float sample = inf[i]; + float sample = f_in[i]; int above = (fabsf(sample) > 0.05f); if (above && !prev_above) { bursts++; } prev_above = above; - passthrough_sum_sq += (double)inf[i] * (double)inf[i]; + passthrough_sum_sq += (double)f_in[i] * (double)f_in[i]; passthrough_total_samples++; } if (passthrough_total_samples >= passthrough_sample_rate * 2) { From b4a65a5788ce8db1a924c489da869114c72b129f Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 9 May 2026 14:47:04 +0000 Subject: [PATCH 31/31] chore: add git hooks --- .githooks/pre-push | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..ab512c5 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,4 @@ +#!/bin/bash +make test +make check +make format