Files
looper/tests/integration.c
Loic Coenen 150b01d3be tests/integration.c
```c
<<<<<<< SEARCH
static volatile int passthrough_done = 0;
static volatile int beep_remaining = 0;
static volatile int bursts = 0;
static volatile int prev_above = 0;
=======
static volatile int passthrough_done = 0;
static volatile int beep_remaining = 0;
static volatile int bursts = 0;
static volatile int prev_above = 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 int midi_inject_process(jack_nframes_t nframes, void *arg) {
    (void)arg;
    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;
}
>>>>>>> REPLACE
```

tests/integration.c
```c
<<<<<<< SEARCH
/* Helper: open a transient JACK client, send a MIDI note‑on, close */
static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) {
    /* The correct JACK API requires writing events inside a process callback.
       For now we stub this function; the test will skip the MIDI transition
       tests and the pass‑through test suffices. */
    (void)target_port;
    (void)note;
    (void)velocity;
    return 0;
}
=======
/* Helper: open a transient JACK client, send a MIDI note‑on, close */
static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) {
    midi_inject_note = note;
    midi_inject_velocity = velocity;

    jack_status_t st;
    midi_inject_client = jack_client_open("test_midi_inject", 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) {
        jack_client_close(midi_inject_client);
        midi_inject_client = NULL;
        return -1;
    }
    char src[64];
    snprintf(src, sizeof(src), "test_midi_inject:out");
    if (jack_connect(midi_inject_client, src, target_port) != 0) {
        jack_client_close(midi_inject_client);
        midi_inject_client = NULL;
        midi_inject_port = NULL;
        return -1;
    }
    jack_set_process_callback(midi_inject_client, midi_inject_process, NULL);
    if (jack_activate(midi_inject_client) != 0) {
        jack_client_close(midi_inject_client);
        midi_inject_client = NULL;
        midi_inject_port = NULL;
        return -1;
    }
    midi_inject_pending = 1;
    /* wait for one process cycle to deliver the event */
    usleep(200000);
    jack_deactivate(midi_inject_client);
    jack_client_close(midi_inject_client);
    midi_inject_client = NULL;
    midi_inject_port = NULL;
    return 0;
}
>>>>>>> REPLACE
```

tests/integration.c
```c
<<<<<<< SEARCH
/*
 * Full loop recording test (stub – the MIDI API is non‑trivial without external tools,
 * so we skip the actual instrumentation and just verify the looper doesn't crash).
 */
static int test_looper_looping(void) {
    printf("Test: loop recording and playback (skip – no external MIDI tool)\n");
    printf("  SUCCESS: nothing was measured (stub)\n");
    return 0;
}
=======
/*
 * 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;

    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;
    }
    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;
    }
    usleep(200000);               /* allow state to change */

    int sr = jack_get_sample_rate(client);
    beep_remaining = (int)(0.1f * sr);   /* 0.1 second beep */
    bursts = 0;
    prev_above = 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(150000);               /* let beep start */

    /* after beep finishes, give it a moment then send note‑on to stop recording */
    usleep(500000);
    beep_remaining = 0;

    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 (3 seconds) */
    usleep(3000000);

    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;
}
>>>>>>> REPLACE
```

Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
2026-05-07 21:56:12 +00:00

376 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
#include <stdarg.h>
#include <fcntl.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
/* 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;
/* 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 int midi_inject_process(jack_nframes_t nframes, void *arg) {
(void)arg;
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;
}
/* 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);
jack_default_audio_sample_t *in =
(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;
for (jack_nframes_t i = 0; i < nframes; i++) {
/* generate beep while beep_remaining > 0 */
float out_val;
if (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;
beep_remaining--;
} else {
out_val = 0.0f;
}
outf[i] = out_val;
/* detect bursts on the input (looper output) */
float sample = inf[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_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 noteon 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 passthrough (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;
}
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;
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;
}
usleep(2200000); /* 2.2 seconds */
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 noteon, close */
static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) {
midi_inject_note = note;
midi_inject_velocity = velocity;
jack_status_t st;
midi_inject_client = jack_client_open("test_midi_inject", 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) {
jack_client_close(midi_inject_client);
midi_inject_client = NULL;
return -1;
}
char src[64];
snprintf(src, sizeof(src), "test_midi_inject:out");
if (jack_connect(midi_inject_client, src, target_port) != 0) {
jack_client_close(midi_inject_client);
midi_inject_client = NULL;
midi_inject_port = NULL;
return -1;
}
jack_set_process_callback(midi_inject_client, midi_inject_process, NULL);
if (jack_activate(midi_inject_client) != 0) {
jack_client_close(midi_inject_client);
midi_inject_client = NULL;
midi_inject_port = NULL;
return -1;
}
midi_inject_pending = 1;
/* wait for one process cycle to deliver the event */
usleep(200000);
jack_deactivate(midi_inject_client);
jack_client_close(midi_inject_client);
midi_inject_client = NULL;
midi_inject_port = NULL;
return 0;
}
/*
* Full loop recording test:
* 1. start looper
* 2. open JACK test client (audio)
* 3. send noteon to move IDLE->RECORD
* 4. generate a short 440 Hz beep (~0.1 s) while recording
* 5. send noteon 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;
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;
}
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 noteon: 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;
}
usleep(200000); /* allow state to change */
int sr = jack_get_sample_rate(client);
beep_remaining = (int)(0.1f * sr); /* 0.1 second beep */
bursts = 0;
prev_above = 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(150000); /* let beep start */
/* after beep finishes, give it a moment then send noteon to stop recording */
usleep(500000);
beep_remaining = 0;
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 (3 seconds) */
usleep(3000000);
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;
}
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 passthrough test must work for basic connectivity */
test_audio_pass_through();
/* 4. Test that looping feature is now implemented */
if (test_looper_looping() != 0) {
fprintf(stderr, " FAILED\n");
return 1;
}
printf("All tests completed successfully.\n");
return 0;
}