#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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]; // 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; 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]); for (jack_nframes_t i = 0; i < nframes; i++) { if (read_pos + i < count) { out[i] = test_audio_input[ch][read_pos + i]; } else { out[i] = 0.0f; } } atomic_store(&test_audio_input_read[ch], read_pos + nframes); } // 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, "-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; 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 + 100000); // duration ms + 100ms extra } 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); pthread_mutex_unlock(&test_buffer_mutex); usleep(100000); // 100ms to let JACK process } 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) send_midi_note(0, 100); usleep(100000); // Step 2: Send audio to channel 0 while recording send_sine_wave(0, 440.0, 0.5, 500); usleep(200000); // Step 3: Stop recording (toggle to looping) send_midi_note(0, 100); usleep(100000); // Step 4: Clear output and check that looped audio appears clear_output_buffers(); usleep(300000); 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(); // Record clips on channels 0, 1, 2 for (int ch = 0; ch < 3; ch++) { // Note ch triggers clip ch (since note % MAX_CLIPS = ch for ch < 512) send_midi_note(ch, 100); usleep(50000); send_sine_wave(ch, 440.0 * (ch + 1), 0.5, 300); usleep(50000); send_midi_note(ch, 100); // Toggle to looping usleep(50000); } clear_output_buffers(); usleep(300000); 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 (note 0 triggers clip 0) send_midi_note(0, 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 to looping) send_midi_note(0, 100); usleep(100000); // Now the clip should be looping - clear output and check clear_output_buffers(); usleep(300000); // 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 channels 0-3 using notes 0-3 for (int ch = 0; ch < 4; ch++) { send_midi_note(ch, 100); // Start recording usleep(50000); send_sine_wave(ch, 440.0 * (ch + 1), 0.3, 200); usleep(50000); send_midi_note(ch, 100); // Toggle to looping usleep(50000); } clear_output_buffers(); usleep(300000); // 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(); // Record on channel 0 only send_midi_note(0, 100); usleep(50000); send_sine_wave(0, 440.0, 0.8, 500); usleep(50000); send_midi_note(0, 100); // Toggle to looping usleep(50000); clear_output_buffers(); usleep(300000); 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"); clear_output_buffers(); // Record on channel 0 send_midi_note(0, 100); usleep(50000); send_sine_wave(0, 440.0, 0.5, 500); usleep(50000); send_midi_note(0, 100); // Toggle to looping usleep(50000); clear_output_buffers(); usleep(300000); float rms = get_channel_rms(0); 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); // 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_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; }