Files
jack-looper/test_audio_routing.c
Loic Coenen 3c373370f5 fix: resolve duplicate main and missing includes in test_audio_routing.c
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-04 17:47:24 +00:00

420 lines
13 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <math.h>
#include <stdbool.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdatomic.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// 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;
}