Files
jack-looper/test_audio_routing.c
Loic Coenen 86cc2652c8 fix: add log check after connecting test client to verify process callback
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-05 16:40:26 +00:00

679 lines
22 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 <sys/time.h>
#include <sys/resource.h>
#include <stdatomic.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Test configuration
#define TEST_SAMPLE_RATE 48000
#define TEST_NFRAMES 256
#define TEST_DURATION_SECONDS 1
#define MAX_CHANNELS 8
#define MAX_SCENES 8
#define MAX_CLIPS 512
// Hard timeout (seconds)
#define TEST_TIMEOUT_SECONDS 30
// 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_bool test_timeout;
static atomic_int tests_passed;
static atomic_int tests_failed;
// Shared buffers for test audio input (written by test thread, read by JACK callback)
// Max samples = sample_rate * max_duration_seconds (sample_rate is typically 48000)
#define MAX_SAMPLES (48000 * TEST_DURATION_SECONDS)
static float test_audio_input[MAX_CHANNELS][MAX_SAMPLES];
static atomic_size_t test_audio_input_count[MAX_CHANNELS];
static atomic_size_t test_audio_input_read[MAX_CHANNELS];
// Debug counters
static atomic_ulong test_callback_count;
static atomic_ulong test_midi_sent_count;
static atomic_ulong test_audio_written_count;
// Shared MIDI buffer (written by test thread, read by JACK callback)
static uint8_t test_midi_buffer[3]; // status, note, velocity
static atomic_bool test_midi_pending;
// Test audio output buffers (written by JACK callback, read by test thread)
static float test_output_buffer[MAX_CHANNELS][MAX_SAMPLES];
static atomic_size_t test_output_count[MAX_CHANNELS];
// Mutex to protect shared buffers between main thread and JACK callback
static pthread_mutex_t test_buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
// ============================================================
// 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;
atomic_fetch_add(&test_callback_count, 1);
pthread_mutex_lock(&test_buffer_mutex);
// Write test audio to output ports (to be sent to looper)
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)
jack_port_get_buffer(test_audio_out[ch], nframes);
size_t read_pos = atomic_load(&test_audio_input_read[ch]);
size_t count = atomic_load(&test_audio_input_count[ch]);
bool wrote_audio = false;
for (jack_nframes_t i = 0; i < nframes; i++) {
if (read_pos + i < count) {
out[i] = test_audio_input[ch][read_pos + i];
if (test_audio_input[ch][read_pos + i] != 0.0f) wrote_audio = true;
} else {
out[i] = 0.0f;
}
}
atomic_store(&test_audio_input_read[ch], read_pos + nframes);
if (wrote_audio) atomic_fetch_add(&test_audio_written_count, 1);
}
// 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 <= MAX_SAMPLES) {
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);
}
}
// Send pending MIDI message to looper
if (atomic_exchange(&test_midi_pending, false)) {
void *midi_out_buf = jack_port_get_buffer(test_midi_out, nframes);
jack_midi_clear_buffer(midi_out_buf);
uint8_t msg[3];
msg[0] = test_midi_buffer[0];
msg[1] = test_midi_buffer[1];
msg[2] = test_midi_buffer[2];
jack_midi_event_write(midi_out_buf, 0, msg, 3);
}
// 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
}
pthread_mutex_unlock(&test_buffer_mutex);
return 0;
}
// ============================================================
// Helper functions
// ============================================================
static int start_looper(const char *client_name) {
// Check if binary exists
if (access("./jack-looper", X_OK) != 0) {
fprintf(stderr, "Error: ./jack-looper binary not found or not executable\n");
return -1;
}
looper_pid = fork();
if (looper_pid == 0) {
// Child process - start jack-looper
// Set CPU limit for child
struct rlimit cpu_limit;
cpu_limit.rlim_cur = 10; // 10 seconds CPU time
cpu_limit.rlim_max = 10;
setrlimit(RLIMIT_CPU, &cpu_limit);
execl("./jack-looper", "jack-looper", "-n", client_name, "-t", NULL);
perror("execl failed");
exit(1);
}
if (looper_pid < 0) {
perror("fork failed");
return -1;
}
// Wait for looper to start
usleep(500000); // 500ms
// Check if looper is still running
if (kill(looper_pid, 0) != 0) {
fprintf(stderr, "Looper process died\n");
return -1;
}
// Check log file for debug messages
FILE *log = fopen("jack-looper.log", "r");
if (log) {
char line[256];
int has_debug = 0;
while (fgets(line, sizeof(line), log)) {
if (strstr(line, "Channel") && strstr(line, "nframes")) {
has_debug = 1;
printf(" [LOG] %s", line);
}
}
fclose(log);
if (!has_debug) {
printf(" [LOG] No process callback debug messages found\n");
}
} else {
printf(" [LOG] Could not open jack-looper.log\n");
}
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");
// Wait for JACK to process and check log file
usleep(500000);
FILE *log = fopen("jack-looper.log", "r");
if (log) {
char line[256];
int has_debug = 0;
while (fgets(line, sizeof(line), log)) {
if (strstr(line, "Channel") && strstr(line, "nframes")) {
has_debug = 1;
printf(" [LOG] %s", line);
}
}
fclose(log);
if (!has_debug) {
printf(" [LOG] No process callback debug messages found after connect\n");
}
} else {
printf(" [LOG] Could not open jack-looper.log after connect\n");
}
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;
if (atomic_load(&test_timeout)) return;
jack_nframes_t sample_rate = jack_get_sample_rate(test_client);
jack_nframes_t total_samples = (sample_rate * duration) / 1000;
pthread_mutex_lock(&test_buffer_mutex);
// Fill the shared input buffer
for (jack_nframes_t i = 0; i < total_samples; i++) {
test_audio_input[channel][i] = amplitude * sinf(2.0 * M_PI * frequency * i / sample_rate);
}
atomic_store(&test_audio_input_count[channel], total_samples);
atomic_store(&test_audio_input_read[channel], 0);
pthread_mutex_unlock(&test_buffer_mutex);
// Wait for the JACK callback to consume the audio
// (the callback runs in a separate thread)
usleep(duration * 1000 + 300000); // duration ms + 300ms extra
unsigned long cb_count = atomic_load(&test_callback_count);
unsigned long aw_count = atomic_load(&test_audio_written_count);
printf(" [DEBUG] Callback count: %lu, Audio written: %lu\n", cb_count, aw_count);
}
static void send_midi_note(int note, int velocity) {
if (!test_client) return;
if (atomic_load(&test_timeout)) return;
pthread_mutex_lock(&test_buffer_mutex);
// Store MIDI message for the JACK callback to send
test_midi_buffer[0] = 0x90; // Note On, channel 0
test_midi_buffer[1] = note & 0x7F;
test_midi_buffer[2] = velocity & 0x7F;
atomic_store(&test_midi_pending, true);
atomic_fetch_add(&test_midi_sent_count, 1);
pthread_mutex_unlock(&test_buffer_mutex);
// Wait longer for JACK to process the MIDI message
usleep(300000); // 300ms to let JACK process
unsigned long cb_count = atomic_load(&test_callback_count);
unsigned long ms_count = atomic_load(&test_midi_sent_count);
printf(" [DEBUG] Callback count: %lu, MIDI sent: %lu\n", cb_count, ms_count);
}
static float get_channel_rms(int channel) {
pthread_mutex_lock(&test_buffer_mutex);
size_t count = atomic_load(&test_output_count[channel]);
if (count == 0) {
pthread_mutex_unlock(&test_buffer_mutex);
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];
}
pthread_mutex_unlock(&test_buffer_mutex);
return sqrtf(sum_sq / count);
}
static void clear_output_buffers(void) {
pthread_mutex_lock(&test_buffer_mutex);
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]));
atomic_store(&test_audio_input_count[ch], 0);
atomic_store(&test_audio_input_read[ch], 0);
memset(test_audio_input[ch], 0, sizeof(test_audio_input[ch]));
}
atomic_store(&test_midi_pending, false);
pthread_mutex_unlock(&test_buffer_mutex);
}
// ============================================================
// Test cases
// ============================================================
static void test_basic_audio_routing(void) {
printf("\nTest: Basic Audio Routing\n");
clear_output_buffers();
// Step 1: Start recording on clip 0 (note 0 triggers clip 0)
printf(" Sending MIDI note 0 to start recording...\n");
send_midi_note(0, 100);
usleep(200000);
// Step 2: Send audio to channel 0 while recording
printf(" Sending sine wave to channel 0...\n");
send_sine_wave(0, 440.0, 0.5, 500);
usleep(300000);
// Step 3: Stop recording (toggle to looping)
printf(" Sending MIDI note 0 to toggle to looping...\n");
send_midi_note(0, 100);
usleep(200000);
// Step 4: Clear output and check that looped audio appears
printf(" Clearing output buffers and waiting for looped audio...\n");
clear_output_buffers();
usleep(500000);
float rms_0 = get_channel_rms(0);
float rms_1 = get_channel_rms(1);
printf(" RMS channel 0: %f, channel 1: %f\n", rms_0, 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();
// Record clips on channels 0, 1, 2
for (int ch = 0; ch < 3; ch++) {
printf(" Recording clip on channel %d...\n", ch);
// Note ch triggers clip ch (since note % MAX_CLIPS = ch for ch < 512)
send_midi_note(ch, 100);
usleep(200000);
send_sine_wave(ch, 440.0 * (ch + 1), 0.5, 300);
usleep(200000);
send_midi_note(ch, 100); // Toggle to looping
usleep(200000);
}
clear_output_buffers();
usleep(500000);
float rms[3];
for (int ch = 0; ch < 3; ch++) {
rms[ch] = get_channel_rms(ch);
printf(" RMS channel %d: %f\n", ch, 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 (note 0 triggers clip 0)
printf(" Sending MIDI note 0 to start recording...\n");
send_midi_note(0, 100);
usleep(200000);
// Send audio to channel 0 while clip is recording
printf(" Sending sine wave to channel 0...\n");
send_sine_wave(0, 440.0, 0.5, 500);
usleep(300000);
// Send another MIDI note to stop recording (toggle to looping)
printf(" Sending MIDI note 0 to toggle to looping...\n");
send_midi_note(0, 100);
usleep(200000);
// Now the clip should be looping - clear output and check
printf(" Clearing output buffers and waiting for looped audio...\n");
clear_output_buffers();
usleep(500000);
// Check that audio is still coming out (from the looped clip)
float rms = get_channel_rms(0);
printf(" RMS channel 0: %f\n", rms);
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 channels 0-3 using notes 0-3
for (int ch = 0; ch < 4; ch++) {
printf(" Recording clip on channel %d...\n", ch);
send_midi_note(ch, 100); // Start recording
usleep(200000);
send_sine_wave(ch, 440.0 * (ch + 1), 0.3, 200);
usleep(200000);
send_midi_note(ch, 100); // Toggle to looping
usleep(200000);
}
clear_output_buffers();
usleep(500000);
// All 4 channels should have audio from their looped clips
for (int ch = 0; ch < 4; ch++) {
float rms = get_channel_rms(ch);
printf(" RMS channel %d: %f\n", ch, rms);
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();
// Record on channel 0 only
printf(" Recording on channel 0 only...\n");
send_midi_note(0, 100);
usleep(200000);
send_sine_wave(0, 440.0, 0.8, 500);
usleep(200000);
send_midi_note(0, 100); // Toggle to looping
usleep(200000);
clear_output_buffers();
usleep(500000);
float rms_0 = get_channel_rms(0);
float rms_1 = get_channel_rms(1);
float rms_2 = get_channel_rms(2);
printf(" RMS channel 0: %f, channel 1: %f, channel 2: %f\n", rms_0, rms_1, 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_direct_passthrough(void) {
printf("\nTest: Direct Audio Passthrough\n");
clear_output_buffers();
// Send audio directly to channel 0 without any clip recording
printf(" Sending sine wave to channel 0 (no clip)...\n");
send_sine_wave(0, 440.0, 0.5, 500);
usleep(500000);
float rms_0 = get_channel_rms(0);
printf(" RMS channel 0: %f\n", rms_0);
// If the looper is working, audio should pass through even without clips
// because the rack processes input directly
TEST("Audio passes through looper", rms_0 > 0.01f);
}
static void test_volume_control(void) {
printf("\nTest: Volume Control\n");
clear_output_buffers();
// Record on channel 0
printf(" Recording on channel 0...\n");
send_midi_note(0, 100);
usleep(200000);
send_sine_wave(0, 440.0, 0.5, 500);
usleep(200000);
send_midi_note(0, 100); // Toggle to looping
usleep(200000);
clear_output_buffers();
usleep(500000);
float rms = get_channel_rms(0);
printf(" RMS channel 0: %f\n", rms);
TEST("Audio passes through at default volume", rms > 0.01f);
}
// ============================================================
// Main test runner
// ============================================================
// Watchdog thread - hard kill if test hangs
static void *watchdog_thread(void *arg) {
(void)arg;
sleep(TEST_TIMEOUT_SECONDS + 5); // 5 seconds grace period
if (atomic_load(&test_running)) {
fprintf(stderr, "WATCHDOG: Test exceeded %d seconds, killing process\n",
TEST_TIMEOUT_SECONDS + 5);
kill(getpid(), SIGKILL);
}
return NULL;
}
int main(void) {
printf("=== Audio and MIDI Routing Integration Tests ===\n");
printf("Note: These tests require JACK to be running\n\n");
// Check available memory
long pages = sysconf(_SC_PHYS_PAGES);
long page_size = sysconf(_SC_PAGE_SIZE);
long phys_mem = pages * page_size;
long needed = sizeof(test_output_buffer);
if (needed > phys_mem / 2) {
fprintf(stderr, "Not enough memory for test buffers (need %ld, have %ld)\n",
needed, phys_mem / 2);
return 1;
}
// Set CPU limit for this process
struct rlimit cpu_limit;
cpu_limit.rlim_cur = TEST_TIMEOUT_SECONDS;
cpu_limit.rlim_max = TEST_TIMEOUT_SECONDS;
setrlimit(RLIMIT_CPU, &cpu_limit);
atomic_store(&tests_passed, 0);
atomic_store(&tests_failed, 0);
atomic_store(&test_running, true);
atomic_store(&test_timeout, false);
atomic_store(&test_callback_count, 0);
atomic_store(&test_midi_sent_count, 0);
atomic_store(&test_audio_written_count, 0);
// Start watchdog thread
pthread_t watchdog;
pthread_create(&watchdog, NULL, watchdog_thread, NULL);
pthread_detach(watchdog);
// 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_direct_passthrough();
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;
}