From 8892acd3d237e6ee95f714b19f8292794c6fa1db Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 18:22:38 +0000 Subject: [PATCH] refactor: split integration.c into modular test files Co-authored-by: aider (deepseek/deepseek-reasoner) --- tests/main.c | 32 +++ tests/test_audio.c | 89 +++++++ tests/test_channel.c | 611 +++++++++++++++++++++++++++++++++++++++++++ tests/test_fifo.c | 160 +++++++++++ tests/test_loop.c | 190 ++++++++++++++ 5 files changed, 1082 insertions(+) create mode 100644 tests/main.c create mode 100644 tests/test_audio.c create mode 100644 tests/test_channel.c create mode 100644 tests/test_fifo.c create mode 100644 tests/test_loop.c diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..0db10d6 --- /dev/null +++ b/tests/main.c @@ -0,0 +1,32 @@ +#include "test_common.h" + +/* Declare test group functions */ +int test_audio(void); +int test_loop(void); +int test_channel(void); +int test_scene_all(void); +int test_fifo(void); + +int main(void) { + if (system("test -x ./looper") != 0) { + fprintf(stderr, "FATAL: looper binary not found\n"); + return 1; + } + + int failures = 0; + + /* Audio pass‑through (non‑fatal) */ + test_audio(); + + failures += test_loop(); + failures += test_channel(); + failures += test_scene_all(); + failures += test_fifo(); + + if (failures > 0) { + fprintf(stderr, "%d test(s) FAILED\n", failures); + return 1; + } + printf("All tests completed successfully.\n"); + return 0; +} diff --git a/tests/test_audio.c b/tests/test_audio.c new file mode 100644 index 0000000..deb9444 --- /dev/null +++ b/tests/test_audio.c @@ -0,0 +1,89 @@ +#include "test_common.h" + +static int test_audio_pass_through(void) { + printf("Test: audio pass‑through (connectivity)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_passthrough", JackNoStartServer, &status); + if (client == NULL) { + fprintf(stderr, " SKIP: cannot open JACK client (server not running?)\n"); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + jack_port_t *output_port = jack_port_register(client, "output", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + jack_port_t *input_port = jack_port_register(client, "input", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + if (!output_port || !input_port) { + fprintf(stderr, " FAIL: could not register ports\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + const char *looper_input = "looper:input"; + const char *looper_output = "looper:output"; + char my_output[64], my_input[64]; + snprintf(my_output, sizeof(my_output), "test_passthrough:output"); + snprintf(my_input, sizeof(my_input), "test_passthrough:input"); + if (jack_connect(client, my_output, looper_input) != 0) { + fprintf(stderr, " FAIL: cannot connect\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + if (jack_connect(client, looper_output, my_input) != 0) { + fprintf(stderr, " FAIL: cannot connect\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + passthrough_output_port = output_port; + passthrough_input_port = input_port; + passthrough_phase = 0.0f; + passthrough_freq = 440.0f; + passthrough_sample_rate = jack_get_sample_rate(client); + passthrough_total_samples = 0; + passthrough_sum_sq = 0.0; + passthrough_done = 0; + continuous_sine = 1; + beep_remaining = 0; + jack_set_process_callback(client, passthrough_process, NULL); + if (jack_activate(client) != 0) { + fprintf(stderr, " FAIL: cannot activate client\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(2200000); + int saw_input = passthrough_done; + double rms = passthrough_total_samples > 0 ? + sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0; + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (!saw_input) { + fprintf(stderr, " FAIL: looper did not produce output (no callback run?)\n"); + return 1; + } + if (rms < 0.001) { + fprintf(stderr, " FAIL: looper output RMS too small (%.6f)\n", rms); + return 1; + } + printf(" PASS (RMS %.6f)\n", rms); + return 0; +} + +int test_audio(void) { + return test_audio_pass_through(); +} diff --git a/tests/test_channel.c b/tests/test_channel.c new file mode 100644 index 0000000..9e8045d --- /dev/null +++ b/tests/test_channel.c @@ -0,0 +1,611 @@ +#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; +} diff --git a/tests/test_fifo.c b/tests/test_fifo.c new file mode 100644 index 0000000..6b67557 --- /dev/null +++ b/tests/test_fifo.c @@ -0,0 +1,160 @@ +#include "test_common.h" + +static int test_fifo_pipe(void) { + printf("Test: FIFO pipe add/remove\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_fifo", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "add\n", 4); + 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); + } + write(fd, "remove\n", 7); + close(fd); + safe_usleep(1500000); + ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); + int still_found = 0; + if (ports) { + for (int i = 0; ports[i]; i++) { + if (strstr(ports[i], "looper:channel1_input")) { + still_found = 1; + break; + } + } + jack_free(ports); + } + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (!found) { + fprintf(stderr, " FAIL: channel not added via FIFO\n"); + return 1; + } + if (still_found) { + fprintf(stderr, " FAIL: channel not removed via FIFO\n"); + return 1; + } + printf(" PASS (FIFO add/remove works)\n"); + return 0; +} + +static int test_fifo_stop_bind_unbind(void) { + printf("Test: FIFO stop, bind, unbind\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_fifo_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_fifo_stop:out"); + snprintf(my_in, sizeof(my_in), "test_fifo_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.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(150000); + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "stop\n", 5); + write(fd, "bind 0\n", 7); + write(fd, "unbind\n", 7); + close(fd); + 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 < 1) { + fprintf(stderr, " FAIL: no burst detected (probably no recording)\n"); + return 1; + } + printf(" PASS (FIFO stop, bind, unbind executed)\n"); + return 0; +} + +int test_fifo(void) { + int failures = 0; + failures += test_fifo_pipe(); + failures += test_fifo_stop_bind_unbind(); + return failures; +} diff --git a/tests/test_loop.c b/tests/test_loop.c new file mode 100644 index 0000000..d27aa34 --- /dev/null +++ b/tests/test_loop.c @@ -0,0 +1,190 @@ +#include "test_common.h" + +static int test_looper_looping(void) { + printf("Test: loop recording and playback (expect ≥3 repetitions)\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_looping", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: JACK not running?\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_looping:out"); + snprintf(my_in, sizeof(my_in), "test_looping: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(500000); + 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(150000); + safe_usleep(800000); + 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(4000000); + 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 (at least 3 repetitions)\n"); + return 0; +} + +static int test_record_loop_stop(void) { + printf("Test: full record‑loop‑stop (≥5 repetitions)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + 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_full", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + jack_port_t *audio_out = jack_port_register(client, "out", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + jack_port_t *audio_in = jack_port_register(client, "in", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + if (!audio_out || !audio_in) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + char my_out[64], my_in[64]; + snprintf(my_out, sizeof(my_out), "test_full:out"); + snprintf(my_in, sizeof(my_in), "test_full:in"); + if (jack_connect(client, my_out, "looper:input") || + jack_connect(client, "looper:output", my_in)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + 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); + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.5f * sr); + bursts = 0; + prev_above = 0; + passthrough_output_port = audio_out; + passthrough_input_port = audio_in; + passthrough_phase = 0.0f; + passthrough_freq = 440.0f; + passthrough_sample_rate = sr; + passthrough_total_samples = 0; + passthrough_sum_sq = 0.0; + passthrough_done = 0; + jack_set_process_callback(client, passthrough_process, NULL); + if (jack_activate(client)) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + 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(2500000); + 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; + } + safe_usleep(200000); + int total_bursts = bursts; + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (total_bursts < 5) { + fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts); + return 1; + } + printf(" PASS (≥5 repetitions, stopped cleanly)\n"); + return 0; +} + +int test_loop(void) { + int failures = 0; + failures += test_looper_looping(); + failures += test_record_loop_stop(); + return failures; +}