From 74a190c70c613ba6ec7c7dcf49ff5154a0907f0a Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 8 May 2026 21:11:22 +0000 Subject: [PATCH 01/22] #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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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 10/22] 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 11/22] 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 12/22] 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 13/22] 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 14/22] 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 15/22] 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 16/22] 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 17/22] 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 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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