/* * test_audio_routing.c * * External test program for the looper application. * Connects to JACK, sends a test tone to the application's audio_in ports, * captures the output from audio_out ports, and verifies that audio is flowing. * * Compile with: * gcc -o test_audio_routing test_audio_routing.c -ljack -lm * * Run after the looper application is running and JACK is active. */ #include #include #include #include #include #include #define TEST_DURATION_SECONDS 2 #define SAMPLE_RATE 48000 #define NUM_CHANNELS 2 // test first two channels static jack_client_t *client = NULL; static jack_port_t *test_out_ports[NUM_CHANNELS]; static jack_port_t *test_in_ports[NUM_CHANNELS]; static float *captured_output[NUM_CHANNELS]; static jack_nframes_t captured_frames = 0; static volatile int running = 1; /* Generate a sine wave sample */ static float sine_sample(jack_nframes_t frame, float freq, float sample_rate) { return sinf(2.0f * M_PI * freq * frame / sample_rate); } /* Process callback: generate test tone on output ports, capture input ports */ static int process(jack_nframes_t nframes, void *arg) { (void)arg; /* Generate test tone on each output port */ for (int ch = 0; ch < NUM_CHANNELS; ch++) { jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)jack_port_get_buffer(test_out_ports[ch], nframes); for (jack_nframes_t i = 0; i < nframes; i++) { /* 440 Hz sine wave, amplitude 0.5 */ out[i] = 0.5f * sine_sample(captured_frames + i, 440.0f, SAMPLE_RATE); } } /* Capture input ports */ for (int ch = 0; ch < NUM_CHANNELS; ch++) { jack_default_audio_sample_t *in = (jack_default_audio_sample_t *)jack_port_get_buffer(test_in_ports[ch], nframes); /* Append to captured buffer */ if (captured_output[ch]) { memcpy(captured_output[ch] + captured_frames, in, nframes * sizeof(float)); } } captured_frames += nframes; return 0; } /* Shutdown callback */ static void shutdown(void *arg) { (void)arg; running = 0; } int main(void) { jack_status_t status; const char *client_name = "test_audio_routing"; client = jack_client_open(client_name, JackNullOption, &status, NULL); if (!client) { fprintf(stderr, "Failed to open JACK client, status = 0x%2.0x\n", status); return 1; } /* Register ports */ char port_name[64]; for (int ch = 0; ch < NUM_CHANNELS; ch++) { snprintf(port_name, sizeof(port_name), "test_out_%d", ch); test_out_ports[ch] = jack_port_register(client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if (!test_out_ports[ch]) { fprintf(stderr, "Failed to register output port %d\n", ch); jack_client_close(client); return 1; } snprintf(port_name, sizeof(port_name), "test_in_%d", ch); test_in_ports[ch] = jack_port_register(client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (!test_in_ports[ch]) { fprintf(stderr, "Failed to register input port %d\n", ch); jack_client_close(client); return 1; } } /* Set process callback */ jack_set_process_callback(client, process, NULL); jack_on_shutdown(client, shutdown, NULL); /* Allocate capture buffers */ jack_nframes_t sample_rate = jack_get_sample_rate(client); jack_nframes_t total_frames = sample_rate * TEST_DURATION_SECONDS; for (int ch = 0; ch < NUM_CHANNELS; ch++) { captured_output[ch] = (float *)calloc(total_frames, sizeof(float)); if (!captured_output[ch]) { fprintf(stderr, "Memory allocation failed\n"); jack_client_close(client); return 1; } } /* Activate client */ if (jack_activate(client) != 0) { fprintf(stderr, "Failed to activate JACK client\n"); jack_client_close(client); return 1; } /* Connect to the looper application's ports */ const char *app_client_name = "looper"; /* adjust if your app uses a different name */ for (int ch = 0; ch < NUM_CHANNELS; ch++) { char src_port[128], dst_port[128]; snprintf(src_port, sizeof(src_port), "%s:test_out_%d", client_name, ch); snprintf(dst_port, sizeof(dst_port), "%s:audio_in_%d", app_client_name, ch); if (jack_connect(client, src_port, dst_port) != 0) { fprintf(stderr, "Warning: could not connect %s -> %s\n", src_port, dst_port); } snprintf(src_port, sizeof(src_port), "%s:audio_out_%d", app_client_name, ch); snprintf(dst_port, sizeof(dst_port), "%s:test_in_%d", client_name, ch); if (jack_connect(client, src_port, dst_port) != 0) { fprintf(stderr, "Warning: could not connect %s -> %s\n", src_port, dst_port); } } /* Run for the test duration */ printf("Running test for %d seconds...\n", TEST_DURATION_SECONDS); sleep(TEST_DURATION_SECONDS); /* Deactivate and close */ jack_deactivate(client); jack_client_close(client); /* Analyze captured output */ int pass = 1; for (int ch = 0; ch < NUM_CHANNELS; ch++) { float max_val = 0.0f; for (jack_nframes_t i = 0; i < total_frames; i++) { float abs_val = fabsf(captured_output[ch][i]); if (abs_val > max_val) max_val = abs_val; } printf("Channel %d: max output amplitude = %f\n", ch, max_val); if (max_val < 0.001f) { printf("FAIL: Channel %d output is near zero (no audio flowing)\n", ch); pass = 0; } } /* Cleanup */ for (int ch = 0; ch < NUM_CHANNELS; ch++) { free(captured_output[ch]); } if (pass) { printf("PASS: Audio routing is working correctly.\n"); return 0; } else { printf("FAIL: Audio routing test failed.\n"); 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; }