From 2a0667b372c72f5abbcce2795c1e2878fcb7b017 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Mon, 4 May 2026 17:45:33 +0000 Subject: [PATCH] test: add integration tests for audio and MIDI routing via JACK Co-authored-by: aider (deepseek/deepseek-coder) --- makefile | 11 +- test_audio_routing.c | 413 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 2 deletions(-) diff --git a/makefile b/makefile index ff0cd82..2a34602 100644 --- a/makefile +++ b/makefile @@ -2,7 +2,7 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c99 -D_POSIX_C_SOURCE=200809L -I. -Ilib -fsanitize=thread LDFLAGS = -ljack -lm -lncurses -lpthread -all: jack-looper test_engine test_tui test_gui test_cli test_stress test_wav_io +all: jack-looper test_engine test_tui test_gui test_cli test_stress test_wav_io test_audio_routing jack-looper: main.o engine.o tui.o gui.o cli.o transport.o dispatcher.o lib/microui.o wav_io.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) @@ -25,6 +25,9 @@ test_stress: test_stress.o engine.o wav_io.o dispatcher.o carla.o fs.o test_wav_io: test_wav_io.o wav_io.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +test_audio_routing: test_audio_routing.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + test_stress.o: test_stress.c dispatcher.h engine.h wav_io.h $(CC) $(CFLAGS) -c -o $@ $< @@ -75,6 +78,9 @@ test_cli.o: test_cli.c engine.h transport.h dispatcher.h test_wav_io.o: test_wav_io.c wav_io.h $(CC) $(CFLAGS) -c -o $@ $< +test_audio_routing.o: test_audio_routing.c + $(CC) $(CFLAGS) -c -o $@ $< + cli.o: cli.c cli.h engine.h transport.h $(CC) $(CFLAGS) -c -o $@ $< @@ -84,8 +90,9 @@ cli.o: cli.c cli.h engine.h transport.h clean: rm -f *.o lib/microui.o wav_io.o jack-looper test_engine test_tui test_gui test_cli test_stress test_wav_io -test: test_engine test_tui test_cli test_wav_io +test: test_engine test_tui test_cli test_wav_io test_audio_routing ./test_engine ./test_tui ./test_cli ./test_wav_io + ./test_audio_routing diff --git a/test_audio_routing.c b/test_audio_routing.c index 9e6c315..24622be 100644 --- a/test_audio_routing.c +++ b/test_audio_routing.c @@ -178,3 +178,416 @@ int main(void) { return 1; } } +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Test configuration +#define TEST_SAMPLE_RATE 48000 +#define TEST_NFRAMES 1024 +#define TEST_DURATION_SECONDS 2 +#define MAX_CHANNELS 8 +#define MAX_SCENES 8 +#define MAX_CLIPS 512 + +// Global test state +static pid_t looper_pid = -1; +static jack_client_t *test_client = NULL; +static jack_port_t *test_audio_out[MAX_CHANNELS]; +static jack_port_t *test_audio_in[MAX_CHANNELS]; +static jack_port_t *test_midi_out; +static jack_port_t *test_midi_in; +static atomic_bool test_running; +static atomic_int tests_passed; +static atomic_int tests_failed; + +// Test audio buffers +static float test_output_buffer[MAX_CHANNELS][TEST_NFRAMES * TEST_DURATION_SECONDS]; +static atomic_size_t test_output_count[MAX_CHANNELS]; + +// ============================================================ +// Test framework +// ============================================================ + +#define TEST(name, expr) do { \ + if (expr) { \ + atomic_fetch_add(&tests_passed, 1); \ + printf(" PASS: %s\n", name); \ + } else { \ + atomic_fetch_add(&tests_failed, 1); \ + printf(" FAIL: %s\n", name); \ + } \ +} while(0) + +// ============================================================ +// JACK test client callbacks +// ============================================================ + +static int test_process_callback(jack_nframes_t nframes, void *arg) { + (void)arg; + if (!atomic_load(&test_running)) return 0; + + // Read audio from looper outputs + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) + jack_port_get_buffer(test_audio_in[ch], nframes); + + size_t count = atomic_load(&test_output_count[ch]); + if (count + nframes <= TEST_NFRAMES * TEST_DURATION_SECONDS) { + for (jack_nframes_t i = 0; i < nframes; i++) { + test_output_buffer[ch][count + i] = in[i]; + } + atomic_store(&test_output_count[ch], count + nframes); + } + } + + // Read MIDI from looper + void *midi_buf = jack_port_get_buffer(test_midi_in, nframes); + jack_midi_event_t event; + int event_count = jack_midi_get_event_count(midi_buf); + for (int i = 0; i < event_count; i++) { + jack_midi_event_get(&event, midi_buf, i); + // Process MIDI events if needed + } + + return 0; +} + +// ============================================================ +// Helper functions +// ============================================================ + +static int start_looper(const char *client_name) { + looper_pid = fork(); + if (looper_pid == 0) { + // Child process - start jack-looper + execl("./jack-looper", "jack-looper", "-n", client_name, "-i", NULL); + perror("execl failed"); + exit(1); + } + if (looper_pid < 0) { + perror("fork failed"); + return -1; + } + + // Wait for looper to start + usleep(500000); // 500ms + return 0; +} + +static int stop_looper(void) { + if (looper_pid > 0) { + kill(looper_pid, SIGTERM); + int status; + waitpid(looper_pid, &status, 0); + looper_pid = -1; + } + return 0; +} + +static int connect_test_client(const char *looper_name) { + jack_status_t status; + test_client = jack_client_open("test_routing", JackNullOption, &status, NULL); + if (!test_client) return -1; + + char port_name[64]; + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + snprintf(port_name, sizeof(port_name), "test_out_%d", ch); + test_audio_out[ch] = jack_port_register(test_client, port_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + + snprintf(port_name, sizeof(port_name), "test_in_%d", ch); + test_audio_in[ch] = jack_port_register(test_client, port_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + + if (!test_audio_out[ch] || !test_audio_in[ch]) return -1; + } + + test_midi_out = jack_port_register(test_client, "test_midi_out", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsOutput, 0); + test_midi_in = jack_port_register(test_client, "test_midi_in", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + + if (!test_midi_out || !test_midi_in) return -1; + + jack_set_process_callback(test_client, test_process_callback, NULL); + + if (jack_activate(test_client) != 0) return -1; + + // Connect test outputs to looper inputs + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + char looper_port[64]; + snprintf(looper_port, sizeof(looper_port), "%s:audio_in_%d", looper_name, ch); + snprintf(port_name, sizeof(port_name), "test_routing:test_out_%d", ch); + jack_connect(test_client, port_name, looper_port); + + // Connect looper outputs to test inputs + snprintf(looper_port, sizeof(looper_port), "%s:audio_out_%d", looper_name, ch); + snprintf(port_name, sizeof(port_name), "test_routing:test_in_%d", ch); + jack_connect(test_client, looper_port, port_name); + } + + // Connect MIDI + char looper_port[64]; + snprintf(looper_port, sizeof(looper_port), "%s:midi_control_in", looper_name); + jack_connect(test_client, "test_routing:test_midi_out", looper_port); + + snprintf(looper_port, sizeof(looper_port), "%s:midi_out", looper_name); + jack_connect(test_client, looper_port, "test_routing:test_midi_in"); + + return 0; +} + +static void disconnect_test_client(void) { + if (test_client) { + jack_client_close(test_client); + test_client = NULL; + } +} + +static void send_sine_wave(int channel, float frequency, float amplitude, jack_nframes_t duration) { + if (!test_client) return; + + jack_nframes_t sample_rate = jack_get_sample_rate(test_client); + jack_nframes_t total_samples = (sample_rate * duration) / 1000; + jack_nframes_t processed = 0; + + while (processed < total_samples && atomic_load(&test_running)) { + jack_nframes_t nframes = (total_samples - processed < TEST_NFRAMES) + ? (total_samples - processed) : TEST_NFRAMES; + + jack_default_audio_sample_t *buf = (jack_default_audio_sample_t *) + jack_port_get_buffer(test_audio_out[channel], nframes); + + for (jack_nframes_t i = 0; i < nframes; i++) { + buf[i] = amplitude * sinf(2.0 * M_PI * frequency * (processed + i) / sample_rate); + } + + processed += nframes; + usleep(10000); // 10ms delay to let JACK process + } +} + +static void send_midi_note(int note, int velocity) { + if (!test_client) return; + + jack_nframes_t nframes = TEST_NFRAMES; + void *buf = jack_port_get_buffer(test_midi_out, nframes); + jack_midi_clear_buffer(buf); + + uint8_t msg[3] = {0x90, note & 0x7F, velocity & 0x7F}; + jack_midi_event_write(buf, 0, msg, 3); + + usleep(100000); // 100ms to let JACK process +} + +static float get_channel_rms(int channel) { + size_t count = atomic_load(&test_output_count[channel]); + if (count == 0) return 0.0f; + + double sum_sq = 0.0; + for (size_t i = 0; i < count; i++) { + sum_sq += test_output_buffer[channel][i] * test_output_buffer[channel][i]; + } + return sqrtf(sum_sq / count); +} + +static void clear_output_buffers(void) { + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + atomic_store(&test_output_count[ch], 0); + memset(test_output_buffer[ch], 0, sizeof(test_output_buffer[ch])); + } +} + +// ============================================================ +// Test cases +// ============================================================ + +static void test_basic_audio_routing(void) { + printf("\nTest: Basic Audio Routing\n"); + + clear_output_buffers(); + + // Send sine wave to channel 0 + send_sine_wave(0, 440.0, 0.5, 500); + usleep(200000); // Wait for processing + + // Check that audio appears on channel 0 output + float rms_0 = get_channel_rms(0); + float rms_1 = get_channel_rms(1); + + TEST("Channel 0 has audio output", rms_0 > 0.01f); + TEST("Channel 1 has no audio (no input)", rms_1 < 0.01f); +} + +static void test_multi_channel_routing(void) { + printf("\nTest: Multi-Channel Audio Routing\n"); + + clear_output_buffers(); + + // Send different frequencies to different channels + send_sine_wave(0, 440.0, 0.5, 300); + send_sine_wave(1, 880.0, 0.5, 300); + send_sine_wave(2, 1760.0, 0.5, 300); + usleep(200000); + + float rms[3]; + for (int ch = 0; ch < 3; ch++) { + rms[ch] = get_channel_rms(ch); + } + + TEST("Channel 0 has audio", rms[0] > 0.01f); + TEST("Channel 1 has audio", rms[1] > 0.01f); + TEST("Channel 2 has audio", rms[2] > 0.01f); + TEST("Channel 3 has no audio (no input)", get_channel_rms(3) < 0.01f); +} + +static void test_midi_clip_trigger(void) { + printf("\nTest: MIDI Clip Trigger\n"); + + clear_output_buffers(); + + // Send MIDI note to trigger clip recording + send_midi_note(60, 100); // Middle C, velocity 100 + usleep(100000); + + // Send audio to channel 0 while clip is recording + send_sine_wave(0, 440.0, 0.5, 500); + usleep(200000); + + // Send another MIDI note to stop recording (toggle) + send_midi_note(60, 100); + usleep(100000); + + // Now the clip should be looping - send no audio input + clear_output_buffers(); + usleep(200000); + + // Check that audio is still coming out (from the looped clip) + float rms = get_channel_rms(0); + TEST("Looped clip produces audio", rms > 0.01f); +} + +static void test_midi_scene_launch(void) { + printf("\nTest: MIDI Scene Launch\n"); + + clear_output_buffers(); + + // Record clips on multiple channels for scene 0 + for (int ch = 0; ch < 4; ch++) { + send_midi_note(60 + ch, 100); // Start recording + usleep(50000); + send_sine_wave(ch, 440.0 * (ch + 1), 0.3, 200); + usleep(50000); + send_midi_note(60 + ch, 100); // Stop recording + usleep(50000); + } + + clear_output_buffers(); + usleep(200000); + + // All 4 channels should have audio from their looped clips + for (int ch = 0; ch < 4; ch++) { + float rms = get_channel_rms(ch); + char test_name[64]; + snprintf(test_name, sizeof(test_name), "Channel %d has looped audio", ch); + TEST(test_name, rms > 0.01f); + } +} + +static void test_channel_independence(void) { + printf("\nTest: Channel Independence\n"); + + clear_output_buffers(); + + // Send audio only to channel 0 + send_sine_wave(0, 440.0, 0.8, 500); + usleep(200000); + + float rms_0 = get_channel_rms(0); + float rms_1 = get_channel_rms(1); + float rms_2 = get_channel_rms(2); + + TEST("Channel 0 has strong signal", rms_0 > 0.1f); + TEST("Channel 1 has no crosstalk", rms_1 < 0.01f); + TEST("Channel 2 has no crosstalk", rms_2 < 0.01f); +} + +static void test_volume_control(void) { + printf("\nTest: Volume Control\n"); + + // Note: Volume control would need to be set via CLI commands + // For now, test that audio passes through at default volume + clear_output_buffers(); + + send_sine_wave(0, 440.0, 0.5, 500); + usleep(200000); + + float rms = get_channel_rms(0); + TEST("Audio passes through at default volume", rms > 0.01f); +} + +// ============================================================ +// Main test runner +// ============================================================ + +int main(void) { + printf("=== Audio and MIDI Routing Integration Tests ===\n"); + printf("Note: These tests require JACK to be running\n\n"); + + atomic_store(&tests_passed, 0); + atomic_store(&tests_failed, 0); + atomic_store(&test_running, true); + + // Start jack-looper + printf("Starting jack-looper...\n"); + if (start_looper("test_looper") != 0) { + fprintf(stderr, "Failed to start jack-looper\n"); + return 1; + } + printf("jack-looper started (PID: %d)\n", looper_pid); + + // Connect test client + printf("Connecting test client...\n"); + if (connect_test_client("test_looper") != 0) { + fprintf(stderr, "Failed to connect test client\n"); + stop_looper(); + return 1; + } + printf("Test client connected\n\n"); + + // Run tests + test_basic_audio_routing(); + test_multi_channel_routing(); + test_midi_clip_trigger(); + test_midi_scene_launch(); + test_channel_independence(); + test_volume_control(); + + // Cleanup + atomic_store(&test_running, false); + disconnect_test_client(); + stop_looper(); + + // Report results + int passed = atomic_load(&tests_passed); + int failed = atomic_load(&tests_failed); + + printf("\n=== Results ===\n"); + printf("Passed: %d\n", passed); + printf("Failed: %d\n", failed); + + return failed > 0 ? 1 : 0; +}