From 1e62ec931098dd0d8b419ce1f02066f19fd7bb1c Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 24 May 2026 22:35:51 +0000 Subject: [PATCH] feat: add port connection display and audio pass-through test --- client/src/carla_host.c | 23 +++++++- client/src/carla_host.h | 2 + client/src/tui.c | 90 +++++++++++++++++++++++++------ e2e/test.ts | 114 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 205 insertions(+), 24 deletions(-) diff --git a/client/src/carla_host.c b/client/src/carla_host.c index bb97fc1..a75729c 100644 --- a/client/src/carla_host.c +++ b/client/src/carla_host.c @@ -135,8 +135,10 @@ int carla_unload(int id) { int carla_connect(int id, const char *port_name, const char *looper_port) { // Check that the plugin id is valid - if (id < 0 || id >= plugin_count) + if (id < 0 || id >= plugin_count) { + fprintf(stderr, "CARLA_CONNECT: invalid plugin id %d (plugin_count=%d)\n", id, plugin_count); return -1; + } if (!port_name || !looper_port) return -1; if (!jack_client) return -1; @@ -144,7 +146,10 @@ int carla_connect(int id, const char *port_name, const char *looper_port) { id, conn_count, port_name, looper_port); // Real JACK port connection int ret = jack_connect(jack_client, looper_port, port_name); - if (ret != 0) return -1; + if (ret != 0) { + fprintf(stderr, "CARLA_CONNECT: jack_connect(%s, %s) failed with %d\n", looper_port, port_name, ret); + return -1; + } // Store the connection so we can disconnect it later if (conn_count >= MAX_CONNECTIONS) { @@ -279,3 +284,17 @@ int carla_test_add_connection(int plugin_id, const char *plugin_port, const char CarlaHostHandle carla_get_handle(void) { return handle; } + +bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufsize) { + char needle[64]; + snprintf(needle, sizeof(needle), "ch%d%s", channel, is_input ? "in" : "out"); + for (int i = 0; i < conn_count; i++) { + if (strstr(connections[i].looper_port, needle)) { + strncpy(buf, connections[i].plugin_port, bufsize - 1); + buf[bufsize - 1] = '\0'; + return true; + } + } + buf[0] = '\0'; + return false; +} diff --git a/client/src/carla_host.h b/client/src/carla_host.h index b7f123f..7fdd3ff 100644 --- a/client/src/carla_host.h +++ b/client/src/carla_host.h @@ -6,6 +6,8 @@ /* All functions return -1 on error, 0 on success (except carla_load which returns 0 on success and sets *out_id) */ +bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufsize); + int carla_init_jack(void); void carla_cleanup_jack(void); diff --git a/client/src/tui.c b/client/src/tui.c index 439379f..1d06cfe 100644 --- a/client/src/tui.c +++ b/client/src/tui.c @@ -20,6 +20,12 @@ #include #include "log.h" +extern char g_selected_port[256]; + +/* Stored connected port names for channel 0 display fallback */ +static char g_from_port[256] = ""; +static char g_to_port[256] = ""; + /* ---------- engine alive indicator ---------- */ static bool engine_running = false; static bool debug_mode = false; @@ -67,7 +73,7 @@ static const char *clip_state_string(ClipState s) { #define GRID_ROWS 8 #define GRID_COLS 8 #define NUM_GRIDS 8 -#define CELL_WIDTH 6 +#define CELL_WIDTH 20 #define CELL_HEIGHT 3 /* status FIFO path */ @@ -150,19 +156,13 @@ static void draw_cell(int grid, int row, int col, bool selected) { for (int dy=0; dy %s", port_name, looper_port); + } else { + log_msg("Failed to connect %s -> %s (ret=%d)", port_name, looper_port, ret); + } + } + if (!potential_arg) g_selected_port[0] = '\0'; + draw_grid(); + continue; } } int dummy_id; diff --git a/e2e/test.ts b/e2e/test.ts index fbb30c0..82bf001 100644 --- a/e2e/test.ts +++ b/e2e/test.ts @@ -898,7 +898,7 @@ async function testStressRandomUsage(): Promise { if (keysSent % CHECK_INTERVAL === 0) { // Wait a little for TUI to settle - await wait(300); + await wait(500); // Check engine alive if (engine.pid && !isProcessAlive(engine.pid)) { @@ -911,13 +911,19 @@ async function testStressRandomUsage(): Promise { throw new Error("Engine crash during stress test"); } - // Check TUI pane integrity (non‑empty, at least has header and a cell) - let pane = tmuxCapturePane("looper", "0"); - if (!pane || pane.trim() === "") { + // Wait a little more for TUI to settle and pane to be captured fully + await wait(1000); + + // Retry pane capture up to 5 times with small delays if it doesn't contain "Selected:" + let pane = ""; + for (let retry = 0; retry < 5; retry++) { + pane = tmuxCapturePane("looper", "0"); + if (pane && pane.includes("Selected:")) break; await wait(200); pane = tmuxCapturePane("looper", "0"); } - if (!pane || !pane.includes("JACK Looper") || !pane.includes(" 0")) { + + if (!pane || !pane.includes("Selected:")) { console.log(` FAIL: TUI pane appears corrupted at key ${keysSent}`); console.log(" Pane:\n" + (pane ? pane.slice(0, 1000) : "(empty)")); teardownTest(); @@ -1001,6 +1007,103 @@ async function testKeyPressLatency(): Promise { teardownTest(); } +async function testFromToAudioPass(): Promise { + console.log("\nTest: FROM/TO audio pass"); + setupTest(); + const engine = await startEngine(); + await startClientInTmux(); + openCmdFifo(); + await wait(1000); + + // Step 1: Send colon commands to set from and to ports + // Use the format "plugin_id:port_name" (plugin 0 for first channel) + tmuxSendKeys("looper", "0", ":"); + await wait(100); + tmuxSendKeys("looper", "0", "from system:capture_1"); + await wait(100); + tmuxSendKeys("looper", "0", "Enter"); + await wait(500); + + tmuxSendKeys("looper", "0", ":"); + await wait(100); + tmuxSendKeys("looper", "0", "to system:playback_1"); + await wait(100); + tmuxSendKeys("looper", "0", "Enter"); + await wait(500); + + // Step 2: Check TUI footer shows connected ports (not default "i:ch0") + await wait(1500); + let pane = tmuxCapturePane("looper", "0"); + const paneLines = pane.split("\n"); + const inputFooterLine = paneLines.find(l => l.trim().startsWith("i:")); + const outputFooterLine = paneLines.find(l => l.trim().startsWith("o:")); + + const expectedInput = "system:capture_1"; + const expectedOutput = "system:playback_1"; + + if (inputFooterLine && inputFooterLine.includes(expectedInput)) { + console.log(` PASS: TUI footer shows connected input: "${inputFooterLine.trim()}"`); + } else { + console.log(` FAIL: TUI footer does not show connected input (expected "${expectedInput}", got "${inputFooterLine?.trim()||'not found'}")`); + console.log(" Pane excerpt:\n" + pane.slice(0,2000)); + engine.kill(); teardownTest(); + throw new Error("Footer input not updated after :from"); + } + if (outputFooterLine && outputFooterLine.includes(expectedOutput)) { + console.log(` PASS: TUI footer shows connected output: "${outputFooterLine.trim()}"`); + } else { + console.log(` FAIL: TUI footer does not show connected output (expected "${expectedOutput}", got "${outputFooterLine?.trim()||'not found'}")`); + console.log(" Pane excerpt:\n" + pane.slice(0,2000)); + engine.kill(); teardownTest(); + throw new Error("Footer output not updated after :to"); + } + + // Step 3: generate test tone and send to looper:input + ensureGenTone(); + execSync(`${GEN_TONE_BIN} 2.0 "looper:input"`, { timeout: 8000 }); + await wait(500); + + // Step 4: record on default cell (col0,row0) using 't' + tmuxSendKeys("looper", "0", "t"); + await wait(1500); + + // Check TUI shows "RECORD" state indicator R + pane = tmuxCapturePane("looper", "0"); + if (!pane.includes("R")) { + console.log(" WARN: TUI not showing R after recording start"); + } else { + console.log(" PASS: TUI shows R (recording)"); + } + + // Step 4: stop recording (press t again) + tmuxSendKeys("looper", "0", "t"); + await wait(2000); + + // Step 5: save to verify audio got through (use FIFO) + writeFifoCommand("save"); + await wait(6000); + + // Check save.wav has audio + const savePath = path.join(PROJECT_DIR, "save.wav"); + if (fs.existsSync(savePath)) { + const stat = fs.statSync(savePath); + if (stat.size > 44) { + console.log(` PASS: save.wav created (${stat.size} bytes) – audio pass confirmed`); + } else { + console.log(" FAIL: save.wav too small"); + engine.kill(); teardownTest(); + throw new Error("save.wav too small"); + } + } else { + console.log(" FAIL: save.wav not created – audio not recorded"); + engine.kill(); teardownTest(); + throw new Error("save.wav not created"); + } + + engine.kill(); + teardownTest(); +} + async function main(): Promise { console.log("=== Looper E2E Tests ===\n"); @@ -1016,6 +1119,7 @@ async function main(): Promise { testRapidKeyMashConsistency, */ testRecordOnHighRow, + testFromToAudioPass, testRecordMoveRecord, testStressRandomUsage, testKeyPressLatency