#include #include #include #include #include #include #include #include #include #include #include #include #include /* static variables for passthrough test */ static jack_port_t *passthrough_output_port = NULL; static jack_port_t *passthrough_input_port = NULL; static float passthrough_phase = 0.0f; static float passthrough_freq = 440.0f; static int passthrough_sample_rate = 0; static long passthrough_total_samples = 0; static double passthrough_sum_sq = 0.0; static volatile int passthrough_done = 0; static volatile int beep_remaining = 0; static volatile int bursts = 0; static volatile int prev_above = 0; static int continuous_sine = 0; /* variables for MIDI injection (used by send_jack_note_on) */ static volatile int midi_inject_pending = 0; static jack_port_t *midi_inject_port = NULL; 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; void *port_buf = jack_port_get_buffer(midi_inject_port, nframes); if (!port_buf) return 0; jack_midi_clear_buffer(port_buf); if (!midi_inject_pending) return 0; jack_midi_data_t *buf = jack_midi_event_reserve(port_buf, 0, 3); if (!buf) return 0; buf[0] = 0x90; buf[1] = midi_inject_note; buf[2] = midi_inject_velocity; midi_inject_pending = 0; return 0; } /* Helper: initialise the persistent MIDI client (open and connect) */ static int midi_inject_init(const char *target_port) { if (midi_inject_client) return 0; /* already initialised */ jack_status_t st; midi_inject_client = jack_client_open("midi_inject_persistent", JackNoStartServer, &st); if (!midi_inject_client) return -1; midi_inject_port = jack_port_register(midi_inject_client, "out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); if (!midi_inject_port) return -1; jack_set_process_callback(midi_inject_client, midi_inject_process, NULL); if (jack_activate(midi_inject_client) != 0) return -1; char src[64]; snprintf(src, sizeof(src), "midi_inject_persistent:out"); if (jack_connect(midi_inject_client, src, target_port) != 0) return -1; return 0; } /* Helper: close the persistent MIDI client */ static void midi_inject_close(void) { if (midi_inject_client) { jack_deactivate(midi_inject_client); jack_client_close(midi_inject_client); midi_inject_client = NULL; midi_inject_port = NULL; } } /* The test code uses this callback in two ways: - For the audio passthrough test (existing function) it still works. - For the loop test we need a version that respects the static variables beep_remaining and bursts (declared in test_looper_looping). We change the existing function to also handle those globals. */ 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); 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 *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; if (continuous_sine || beep_remaining > 0) { out_val = sinf(passthrough_phase); passthrough_phase += 2.0f * (float)M_PI * passthrough_freq / passthrough_sample_rate; if (passthrough_phase > 2.0f * M_PI) passthrough_phase -= 2.0f * M_PI; if (beep_remaining > 0) beep_remaining--; } else { out_val = 0.0f; } f_out[i] = out_val; /* detect bursts on the input (looper output) */ float sample = f_in[i]; int above = (fabsf(sample) > 0.05f); if (above && !prev_above) { bursts++; } prev_above = above; passthrough_sum_sq += (double)f_in[i] * (double)f_in[i]; passthrough_total_samples++; } if (passthrough_total_samples >= passthrough_sample_rate * 2) { passthrough_done = 1; } return 0; } /* * Integration test for the JACK looper. * * Uses SIGUSR1 to request the looper to report its current state and exit. * Verifies that MIDI note‑on and clock messages produce correct state transitions. */ #define STATE_IDLE 0 #define STATE_RECORD 1 #define STATE_LOOPING 2 #define STATE_PAUSED 3 #define WAIT_SECONDS 1 /* * Start a fresh instance of the looper, wait for JACK ports to appear, * return its PID, or -1 on failure. */ static pid_t start_looper(void) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { /* child: suppress stderr messages */ close(2); open("/dev/null", O_WRONLY); execl("./looper", "looper", NULL); perror("execl"); _exit(1); } printf("Started looper (pid %d)\n", (int)pid); sleep(3); /* wait for JACK ports to register */ return pid; } static int test_audio_pass_through(void) { printf("Test: audio pass‑through (connectivity)\n"); pid_t pid = start_looper(); if (pid < 0) return 1; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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 test_passthrough:output -> looper:input\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 looper:output -> test_passthrough:input\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; /* enable continuous tone for this test */ beep_remaining = 0; /* not needed */ 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; } /* Helper: open a transient JACK client, send a MIDI note‑on, close */ static jack_client_t *midi_persistent_client = NULL; static jack_port_t *midi_persistent_port = NULL; static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) { /* initialise client on first call (per‑test) */ if (midi_inject_init(target_port) != 0) return -1; midi_inject_note = note; midi_inject_velocity = velocity; midi_inject_pending = 1; /* wait for delivery (process callback clears the flag) */ for (int attempts = 0; attempts < 100; attempts++) { safe_usleep(10000); if (!midi_inject_pending) break; } return 0; } /* must be called after all tests */ static void close_persistent_midi(void) { if (midi_persistent_client) { jack_deactivate(midi_persistent_client); jack_client_close(midi_persistent_client); midi_persistent_client = NULL; midi_persistent_port = NULL; } } /* * Full loop recording test: * 1. start looper * 2. open JACK test client (audio) * 3. send note‑on to move IDLE->RECORD * 4. generate a short 440 Hz beep (~0.1 s) while recording * 5. send note‑on to move RECORD->LOOPING * 6. monitor looper output for the beep being repeated (≥3 times) */ 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; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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); /* 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"); 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; } /* first note‑on: IDLE -> RECORD */ 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); /* allow state to change (500ms) */ int sr = jack_get_sample_rate(client); continuous_sine = 0; /* disable continuous tone */ 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; } safe_usleep(150000); /* let beep start */ /* ensure beep is fully captured */ safe_usleep(800000); /* 0.8s after start of beep */ if (send_jack_note_on("looper:control", 1, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } /* wait enough time for several loops (4 seconds to be safe) */ safe_usleep(4000000); 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 (at least 3 repetitions)\n"); return 0; } /* test multiple channels */ static int test_multiple_channels(void) { printf("Test: dynamic channel creation via MIDI command\n"); pid_t pid = start_looper(); if (pid < 0) return 1; /* ensure fresh MIDI connection for this test */ midi_inject_close(); jack_client_t *client; jack_status_t status; client = jack_client_open("test_multi", JackNoStartServer, &status); if (!client) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " SKIP: no JACK\n"); return 1; } if (send_jack_note_on("looper:control", 60, 127) != 0) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: send note 60 failed\n"); return 1; } /* 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) */ safe_usleep(1500000); int found = 0; const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); if (ports) { for (int i = 0; ports[i]; i++) { if (strstr(ports[i], "looper:channel1_input")) { found = 1; break; } } jack_free(ports); } jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); if (!found) { fprintf(stderr, " FAIL: channel1_input port not created after add command\n"); return 1; } printf(" PASS (channel created)\n"); return 0; } /* 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; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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; } 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; } /* 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; } safe_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; } 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); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: control key re‑send\n"); 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); fprintf(stderr, " FAIL: send note 62 for loop\n"); return 1; } safe_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 (control‑key modifier works)\n"); 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; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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; } /* 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; } 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; } 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); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: send control key again failed\n"); 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); 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; } 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); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: control key for loop\n"); 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); fprintf(stderr, " FAIL: toggle for loop\n"); return 1; } safe_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 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; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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; } /* 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; } 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; } safe_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; } 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; } 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); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: control key for toggle\n"); 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); 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; } safe_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; } safe_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; } safe_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"); pid_t pid = start_looper(); if (pid < 0) return 1; /* ensure fresh MIDI connection for this test */ midi_inject_close(); 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; } safe_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; } safe_usleep(3000000); /* 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; } /* ------------------------------------------------------------ * Helper: generate a simple 440 Hz WAV file for load tests * ------------------------------------------------------------ */ static int generate_test_wav(const char *path, unsigned sample_rate, unsigned duration_frames) { int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) return -1; unsigned data_bytes = duration_frames * 2; unsigned file_size = 44 + data_bytes; unsigned char header[44]; memset(header, 0, 44); memcpy(header, "RIFF", 4); unsigned chunk_size = file_size - 8; header[4] = chunk_size & 0xff; header[5] = (chunk_size>>8)&0xff; header[6] = (chunk_size>>16)&0xff; header[7] = (chunk_size>>24)&0xff; memcpy(header+8, "WAVE", 4); memcpy(header+12, "fmt ", 4); header[16]=16; header[17]=0; header[18]=0; header[19]=0; header[20]=1; header[21]=0; /* PCM */ header[22]=1; header[23]=0; /* mono */ header[24]= sample_rate & 0xff; header[25]=(sample_rate>>8)&0xff; header[26]=(sample_rate>>16)&0xff; header[27]=(sample_rate>>24)&0xff; unsigned br = sample_rate * 2; header[28]= br & 0xff; header[29]=(br>>8)&0xff; header[30]=(br>>16)&0xff; header[31]=(br>>24)&0xff; header[32]=2; header[33]=0; header[34]=16; header[35]=0; memcpy(header+36, "data", 4); header[40]= data_bytes & 0xff; header[41]=(data_bytes>>8)&0xff; header[42]=(data_bytes>>16)&0xff; header[43]=(data_bytes>>24)&0xff; if (write(fd, header, 44) != 44) { close(fd); return -1; } for (unsigned i = 0; i < duration_frames; i++) { float sample = sinf(2.0f * (float)M_PI * 440.0f * i / sample_rate); int16_t s = (int16_t)(sample * 32767); if (write(fd, &s, 2) != 2) { close(fd); return -1; } } close(fd); return 0; } /* ------------------------------------------------------------ * Test: load WAV file (note 70 under control key) * ------------------------------------------------------------ */ static int test_wav_load(void) { printf("Test: load WAV file into channel 0 and detect playback\n"); if (generate_test_wav("loop.wav", 48000, 48000) != 0) { fprintf(stderr, " FAIL: could not create test WAV\n"); return 1; } pid_t pid = start_looper(); if (pid < 0) { unlink("loop.wav"); return 1; } /* ensure fresh MIDI connection for this test */ midi_inject_close(); jack_client_t *client; jack_status_t status; client = jack_client_open("test_wav_load", JackNoStartServer, &status); if (!client) { kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); 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); unlink("loop.wav"); return 1; } safe_usleep(200000); if (jack_connect(client, "test_wav_load:out", "looper:input") || jack_connect(client, "looper:output", "test_wav_load:in")) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); return 1; } /* set up passthrough callback before sending load command */ int sr = jack_get_sample_rate(client); continuous_sine = 0; beep_remaining = 0; 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_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); return 1; } /* send control key + note 70 to trigger load */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); return 1; } safe_usleep(1000000); /* 1 second to ensure control key is processed */ if (send_jack_note_on("looper:control", 70, 127) != 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); return 1; } /* wait for the loop to be fully loaded and playing */ safe_usleep(3000000); /* continue listening for the rest of the time */ safe_usleep(6000000); /* total 9 seconds after activation */ jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); unlink("loop.wav"); int got_bursts = bursts; double rms = passthrough_total_samples > 0 ? sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0; printf(" detected bursts: %d, RMS: %.6f\n", got_bursts, rms); if (got_bursts < 3) { fprintf(stderr, " FAIL: expected ≥3 bursts from loaded loop, got %d, RMS=%.6f\n", got_bursts, rms); return 1; } printf(" PASS (loaded loop plays)\n"); return 0; } /* ------------------------------------------------------------ * Test: save WAV file (note 71 under control key) * ------------------------------------------------------------ */ static int test_wav_save(void) { printf("Test: save WAV file from loop\n"); pid_t pid = start_looper(); if (pid < 0) return 1; /* ensure fresh MIDI connection for this test */ midi_inject_close(); jack_client_t *client; jack_status_t status; client = jack_client_open("test_wav_save", 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); if (jack_connect(client, "test_wav_save:out", "looper:input") || jack_connect(client, "looper:output", "test_wav_save:in")) { jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } /* record a beep: send note 1 (toggle channel 0) */ 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); /* start generating a beep */ 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_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(800000); /* toggle again to stop recording and start looping */ if (send_jack_note_on("looper:control", 1, 127) != 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(500000); /* send control key + note 71 to save */ if (send_jack_note_on("looper:control", 64, 127) != 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(200000); if (send_jack_note_on("looper:control", 71, 127) != 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); return 1; } safe_usleep(2000000); /* check save.wav exists and has data */ int fd = open("save.wav", O_RDONLY); if (fd < 0) { jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: save.wav not created\n"); return 1; } unsigned char hdr[44]; if (read(fd, hdr, 44) != 44) { close(fd); unlink("save.wav"); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: short header\n"); return 1; } unsigned data_bytes = hdr[40] | (hdr[41]<<8) | (hdr[42]<<16) | (hdr[43]<<24); close(fd); if (data_bytes == 0) { unlink("save.wav"); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); fprintf(stderr, " FAIL: empty save.wav\n"); return 1; } printf(" save.wav data size: %u bytes\n", data_bytes); unlink("save.wav"); jack_deactivate(client); jack_client_close(client); kill(pid, SIGTERM); waitpid(pid, NULL, 0); printf(" PASS (save.wav created)\n"); return 0; } int main(void) { /* 1. binary must exist */ if (system("test -x ./looper") != 0) { fprintf(stderr, "FATAL: looper binary not found\n"); return 1; } /* 2. MIDI transition tests (skipped – no external tools) */ /* 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"); failures++; } /* 5. Test multiple dynamic channels */ if (test_multiple_channels() != 0) { fprintf(stderr, " FAILED\n"); failures++; } /* 6. Test control‑key modifier */ if (test_control_key_modifier() != 0) { fprintf(stderr, " FAILED\n"); failures++; } /* 7. Test bind channel */ if (test_bind_channel() != 0) { fprintf(stderr, " FAILED\n"); failures++; } /* 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++; } /* 10. Test WAV load */ if (test_wav_load() != 0) { fprintf(stderr, " FAILED\n"); failures++; } /* 11. Test WAV save */ if (test_wav_save() != 0) { fprintf(stderr, " FAILED\n"); failures++; } close_persistent_midi(); if (failures > 0) { fprintf(stderr, "%d test(s) FAILED\n", failures); return 1; } printf("All tests completed successfully.\n"); return 0; }