#include "test_common.h" 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; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); 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); return 1; } int found = 0; for (int retries = 0; retries < 30; retries++) { safe_usleep(100000); 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; jack_free(ports); goto port_found; } } jack_free(ports); } } port_found: ; jack_client_close(client); cleanup_persistent_midi_client(); kill(pid, SIGTERM); waitpid(pid, NULL, 0); if (!found) { fprintf(stderr, " FAIL: channel1_input port not created\n"); return 1; } printf(" PASS (channel created)\n"); return 0; } 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; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); 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; } jack_port_t *audio_out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); jack_port_t *audio_in = jack_port_register(client, "in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (!audio_out || !audio_in) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_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; } if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } 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; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); cleanup_persistent_midi_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; } 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; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); 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; } 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"); 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; } if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 0, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } 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; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); cleanup_persistent_midi_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; } 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; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); 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; } 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"); 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; } if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 5, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 63, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } 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; } safe_usleep(200000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 62, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(2000000); jack_deactivate(client); jack_client_close(client); cleanup_persistent_midi_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; } 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; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); 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; } if (send_jack_note_on("looper:control", 60, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(1500000); 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"); if (send_jack_note_on("looper:control", 61, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } int still_found = 1; for (int retries = 0; retries < 30; retries++) { safe_usleep(100000); ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); 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); } if (!still_found) break; } jack_client_close(client); cleanup_persistent_midi_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; } static int test_stop_midi(void) { printf("Test: MIDI stop (note 65 under control key)\n"); pid_t pid = start_looper(); if (pid < 0) return 1; if (init_persistent_midi_client() != 0) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); return 1; } jack_client_t *client; jack_status_t status; client = jack_client_open("test_stop", JackNoStartServer, &status); if (!client) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " SKIP: no JACK\n"); return 1; } jack_port_t *audio_out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); jack_port_t *audio_in = jack_port_register(client, "in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (!audio_out || !audio_in) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); char my_out[64], my_in[64]; snprintf(my_out, sizeof(my_out), "test_stop:out"); snprintf(my_in, sizeof(my_in), "test_stop:in"); if (jack_connect(client, my_out, "looper:input") || jack_connect(client, "looper:output", my_in)) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } if (send_jack_note_on("looper:control", 1, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); int sr = jack_get_sample_rate(client); continuous_sine = 0; beep_remaining = (int)(0.2f * sr); bursts = 0; prev_above = 0; passthrough_output_port = audio_out; passthrough_input_port = audio_in; passthrough_phase = 0.0f; passthrough_freq = 440.0f; passthrough_sample_rate = sr; passthrough_total_samples = 0; passthrough_sum_sq = 0.0; passthrough_done = 0; jack_set_process_callback(client, passthrough_process, NULL); if (jack_activate(client)) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(150000); if (send_jack_note_on("looper:control", 1, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(500000); if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 65, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } int prev = bursts; for (int retries = 0; retries < 20; retries++) { safe_usleep(100000); int cur = bursts; if (cur == prev) break; prev = cur; } int bursts_before = bursts; safe_usleep(500000); int bursts_after = bursts; jack_deactivate(client); jack_client_close(client); cleanup_persistent_midi_client(); kill(pid, SIGTERM); waitpid(pid, NULL, 0); if (bursts_after > bursts_before + 5) { fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n", bursts_before, bursts_after); return 1; } printf(" PASS (stop stopped playback)\n"); return 0; } static int test_midi_channel_add(void) { printf("Test: MIDI channel creation via FIFO (add_midi)\n"); pid_t pid = start_looper(); if (pid < 0) return 1; jack_client_t *client; jack_status_t status; client = jack_client_open("test_midi_add", JackNoStartServer, &status); if (!client) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " SKIP: no JACK\n"); return 1; } int fd = open("/tmp/looper_cmd", O_WRONLY); if (fd < 0) { perror("open fifo"); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } write(fd, "add_midi\n", 9); close(fd); safe_usleep(1500000); const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_MIDI_TYPE, 0); int found = 0; if (ports) { for (int i = 0; ports[i]; i++) { if (strstr(ports[i], "looper:channel1_midi_in")) { found = 1; break; } } jack_free(ports); } jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); if (!found) { fprintf(stderr, " FAIL: channel1_midi_in port not created\n"); return 1; } printf(" PASS (MIDI channel created)\n"); return 0; } int test_channel(void) { int failures = 0; failures += test_multiple_channels(); failures += test_control_key_modifier(); failures += test_bind_channel(); failures += test_bind_unbind(); failures += test_remove_channel(); failures += test_stop_midi(); failures += test_midi_channel_add(); return failures; }