679 lines
22 KiB
C
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;
|
|
}
|