Files
looper/tests/integration.c
Loic Coenen 3d9f2af9b3 test: add audio pass-through test with RMS validation
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
2026-05-07 20:55:21 +00:00

277 lines
7.7 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>
/*
* 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
static int run_cmd(const char *fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
return system(buf);
}
/*
* 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;
}
/*
* Send a hex MIDI message to the given port.
*/
static int send_midi(const char *port, const char *msg) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"jack_midi_send -c looper:%s -m '%s' 2>/dev/null",
port, msg);
int ret = system(cmd);
if (ret != 0) {
fprintf(stderr, "jack_midi_send failed: %s\n", cmd);
}
return ret;
}
/*
* Ask the looper to report its current state and exit.
* Returns the state (0..3) or -1 on failure.
*/
static int request_state_and_exit(pid_t pid) {
kill(pid, SIGUSR1);
int status;
if (waitpid(pid, &status, 0) != pid) {
perror("waitpid");
return -1;
}
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
/* looper returns state+1, so state = code-1 */
int state = code - 1;
if (state >= STATE_IDLE && state <= STATE_PAUSED) {
return state;
}
fprintf(stderr, "Unexpected exit code %d (expected 1..4)\n", code);
return -1;
}
fprintf(stderr, "Looper terminated by signal %d\n", WTERMSIG(status));
return -1;
}
/*
* Perform a single transition test: start looper, send @p midi_msg
* (may be NULL for idleonly test), then verify state equals @p expected_state.
* Exits the whole program on failure.
*/
static void test_transition(const char *label,
const char *midi_port,
const char *midi_msg,
int expected_state)
{
printf("Test: %s (expect state %d)\n", label, expected_state);
pid_t pid = start_looper();
if (pid < 0) {
fprintf(stderr, "FAIL: could not start looper\n");
exit(1);
}
if (midi_msg) {
send_midi(midi_port, midi_msg);
sleep(WAIT_SECONDS);
}
int got = request_state_and_exit(pid);
if (got == expected_state) {
printf(" PASS\n");
} else {
fprintf(stderr, " FAIL: got %d, expected %d\n", got, expected_state);
exit(1);
}
}
/*
* Test MIDI clock Start (0xFA) while idle.
*/
static void test_clock_start(void) {
test_transition("clock start -> record", "clock", "FA", STATE_RECORD);
}
/*
* Test MIDI clock Stop (0xFC) after first entering RECORD via control note.
*/
static void test_clock_stop(void) {
printf("Test: clock stop after record (expect idle)\n");
pid_t pid = start_looper();
if (pid < 0) exit(1);
/* IDLE -> RECORD */
send_midi("control", "90 01 7f");
sleep(WAIT_SECONDS);
/* clock stop -> IDLE */
send_midi("clock", "FC");
sleep(WAIT_SECONDS);
int got = request_state_and_exit(pid);
if (got == STATE_IDLE) {
printf(" PASS\n");
} else {
fprintf(stderr, " FAIL: got %d, expected %d\n", got, STATE_IDLE);
exit(1);
}
}
/*
* Test MIDI clock Continue (0xFB) from PAUSED -> LOOPING.
*/
static void test_clock_continue(void) {
printf("Test: clock continue from paused (expect looping)\n");
pid_t pid = start_looper();
if (pid < 0) exit(1);
/* IDLE -> RECORD */
send_midi("control", "90 01 7f");
sleep(WAIT_SECONDS);
/* RECORD -> LOOPING */
send_midi("control", "90 01 7f");
sleep(WAIT_SECONDS);
/* LOOPING -> PAUSED */
send_midi("control", "90 01 7f");
sleep(WAIT_SECONDS);
/* clock continue -> LOOPING */
send_midi("clock", "FB");
sleep(WAIT_SECONDS);
int got = request_state_and_exit(pid);
if (got == STATE_LOOPING) {
printf(" PASS\n");
} else {
fprintf(stderr, " FAIL: got %d, expected %d\n", got, STATE_LOOPING);
exit(1);
}
}
static int test_audio_pass_through(void) {
printf("Test: audio passthrough (latency & RMS)\n");
/* check required tools */
if (system("which jack_sine >/dev/null 2>&1") != 0) {
fprintf(stderr, " SKIP: jack_sine not installed\n");
return 1;
}
if (system("which jack_capture >/dev/null 2>&1") != 0) {
fprintf(stderr, " SKIP: jack_capture not installed\n");
return 1;
}
if (system("which sox >/dev/null 2>&1") != 0) {
fprintf(stderr, " SKIP: sox not installed\n");
return 1;
}
if (system("which python3 >/dev/null 2>&1") != 0) {
fprintf(stderr, " SKIP: python3 not installed\n");
return 1;
}
pid_t pid = start_looper();
if (pid < 0) {
fprintf(stderr, "FAIL: could not start looper\n");
return 1;
}
/* connect sine generator to looper input */
if (system("jack_connect sine:output looper:input 2>/dev/null") != 0) {
fprintf(stderr, "FAIL: could not connect sine -> looper:input\n");
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
return 1;
}
/* capture 2 seconds of looper output */
system("jack_capture -d 2 -f /tmp/looper_test.wav 2>/dev/null");
/* compute RMS of captured WAV using Python */
int rms_ok = system(
"python3 -c '"
"import wave, math;"
"w=wave.open(\"/tmp/looper_test.wav\");"
"f=w.readframes(w.getnframes());"
"s=[int.from_bytes(f[i:i+2],\"little\",signed=True) for i in range(0,len(f),2)];"
"r=math.sqrt(sum(x*x for x in s)/len(s));"
"print(\"RMS=%%d\"%%r);"
"exit(0 if r>1000 else 1)'"
" 2>/dev/null"
);
/* clean up */
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
if (rms_ok == 0) {
printf(" PASS (RMS > 1000)\n");
return 0;
} else {
fprintf(stderr, " FAIL: RMS too low or Python error\n");
return 1;
}
}
int main(void) {
/* 1. binary must exist */
if (system("test -x ./looper") != 0) {
fprintf(stderr, "FATAL: looper binary not found\n");
return 1;
}
/* 2. check required external tool */
if (system("which jack_midi_send >/dev/null 2>&1") != 0) {
fprintf(stderr, "FATAL: jack_midi_send not available\n");
return 1;
}
/* 3. note-on toggle sequence */
test_transition("IDLE -> RECORD", "control", "90 01 7f", STATE_RECORD);
test_transition("RECORD -> LOOPING", "control", "90 01 7f", STATE_LOOPING);
test_transition("LOOPING -> PAUSED", "control", "90 01 7f", STATE_PAUSED);
test_transition("PAUSED -> LOOPING", "control", "90 01 7f", STATE_LOOPING);
/* 4. MIDI clock messages */
test_clock_start();
test_clock_stop();
test_clock_continue();
/* 5. Audio passthrough test (optional tools) */
test_audio_pass_through();
printf("All tests passed!\n");
return 0;
}