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++;