feat: add port connection display and audio pass-through test
This commit is contained in:
committed by
Loic Coenen (aider)
parent
c9c8afc602
commit
1e62ec9310
@@ -135,8 +135,10 @@ int carla_unload(int id) {
|
|||||||
|
|
||||||
int carla_connect(int id, const char *port_name, const char *looper_port) {
|
int carla_connect(int id, const char *port_name, const char *looper_port) {
|
||||||
// Check that the plugin id is valid
|
// 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;
|
return -1;
|
||||||
|
}
|
||||||
if (!port_name || !looper_port) return -1;
|
if (!port_name || !looper_port) return -1;
|
||||||
if (!jack_client) 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);
|
id, conn_count, port_name, looper_port);
|
||||||
// Real JACK port connection
|
// Real JACK port connection
|
||||||
int ret = jack_connect(jack_client, looper_port, port_name);
|
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
|
// Store the connection so we can disconnect it later
|
||||||
if (conn_count >= MAX_CONNECTIONS) {
|
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) {
|
CarlaHostHandle carla_get_handle(void) {
|
||||||
return handle;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) */
|
/* 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);
|
int carla_init_jack(void);
|
||||||
void carla_cleanup_jack(void);
|
void carla_cleanup_jack(void);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
#include <CarlaHost.h>
|
#include <CarlaHost.h>
|
||||||
#include "log.h"
|
#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 ---------- */
|
/* ---------- engine alive indicator ---------- */
|
||||||
static bool engine_running = false;
|
static bool engine_running = false;
|
||||||
static bool debug_mode = false;
|
static bool debug_mode = false;
|
||||||
@@ -67,7 +73,7 @@ static const char *clip_state_string(ClipState s) {
|
|||||||
#define GRID_ROWS 8
|
#define GRID_ROWS 8
|
||||||
#define GRID_COLS 8
|
#define GRID_COLS 8
|
||||||
#define NUM_GRIDS 8
|
#define NUM_GRIDS 8
|
||||||
#define CELL_WIDTH 6
|
#define CELL_WIDTH 20
|
||||||
#define CELL_HEIGHT 3
|
#define CELL_HEIGHT 3
|
||||||
|
|
||||||
/* status FIFO path */
|
/* status FIFO path */
|
||||||
@@ -150,19 +156,13 @@ static void draw_cell(int grid, int row, int col, bool selected) {
|
|||||||
for (int dy=0; dy<CELL_HEIGHT; dy++)
|
for (int dy=0; dy<CELL_HEIGHT; dy++)
|
||||||
for (int dx=0; dx<CELL_WIDTH; dx++)
|
for (int dx=0; dx<CELL_WIDTH; dx++)
|
||||||
mvaddch(y+dy, x+dx, ' ');
|
mvaddch(y+dy, x+dx, ' ');
|
||||||
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
int ch = grid * GRID_ROWS * GRID_COLS + row * GRID_COLS + col;
|
||||||
attroff(COLOR_PAIR(color));
|
|
||||||
|
|
||||||
/* Draw state indicator character below the number, centered */
|
|
||||||
const char state_char = (s == STATE_RECORD) ? 'R' :
|
const char state_char = (s == STATE_RECORD) ? 'R' :
|
||||||
(s == STATE_LOOPING) ? 'L' :
|
(s == STATE_LOOPING) ? 'L' :
|
||||||
(s == STATE_PAUSED) ? 'P' : '.';
|
(s == STATE_PAUSED) ? 'P' : '.';
|
||||||
int state_color = (s == STATE_RECORD) ? COLOR_RECORDING :
|
mvprintw(y, x, "ch %2d", ch);
|
||||||
(s == STATE_LOOPING) ? COLOR_LOOPING :
|
mvaddch(y, x+5, state_char);
|
||||||
(s == STATE_PAUSED) ? COLOR_STOPPED : COLOR_EMPTY;
|
attroff(COLOR_PAIR(color));
|
||||||
attron(COLOR_PAIR(state_color));
|
|
||||||
mvaddch(y+2, x + CELL_WIDTH / 2, state_char);
|
|
||||||
attroff(COLOR_PAIR(state_color));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_rack(void) {
|
static void draw_rack(void) {
|
||||||
@@ -207,11 +207,38 @@ static void draw_grid(void) {
|
|||||||
for (int r=0; r<GRID_ROWS; r++)
|
for (int r=0; r<GRID_ROWS; r++)
|
||||||
for (int c=0; c<GRID_COLS; c++)
|
for (int c=0; c<GRID_COLS; c++)
|
||||||
draw_cell(selected_grid, r, c, r==selected_row && c==selected_col);
|
draw_cell(selected_grid, r, c, r==selected_row && c==selected_col);
|
||||||
mvprintw(GRID_ROWS*CELL_HEIGHT+3, 0, "Selected: Grid %d, Row %d, Col %d",
|
|
||||||
|
/* ---------- Footer: per‑column input / output ---------- */
|
||||||
|
int footer_y = GRID_ROWS * CELL_HEIGHT + 3;
|
||||||
|
for (int c=0; c<GRID_COLS; c++) {
|
||||||
|
int x = c * CELL_WIDTH + 1;
|
||||||
|
int global_ch = selected_grid * GRID_ROWS * GRID_COLS + c;
|
||||||
|
char input_buf[80], output_buf[80];
|
||||||
|
bool has_input = carla_get_connected_port(global_ch, true, input_buf, sizeof(input_buf));
|
||||||
|
bool has_output = carla_get_connected_port(global_ch, false, output_buf, sizeof(output_buf));
|
||||||
|
if (global_ch == 0) {
|
||||||
|
if (!has_input && g_from_port[0]) {
|
||||||
|
strncpy(input_buf, g_from_port, sizeof(input_buf)-1);
|
||||||
|
input_buf[sizeof(input_buf)-1] = '\0';
|
||||||
|
has_input = true;
|
||||||
|
}
|
||||||
|
if (!has_output && g_to_port[0]) {
|
||||||
|
strncpy(output_buf, g_to_port, sizeof(output_buf)-1);
|
||||||
|
output_buf[sizeof(output_buf)-1] = '\0';
|
||||||
|
has_output = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char fallback[16];
|
||||||
|
snprintf(fallback, sizeof(fallback), "ch%d", global_ch);
|
||||||
|
mvprintw(footer_y, x, "i:%-8.8s", has_input ? input_buf : fallback);
|
||||||
|
mvprintw(footer_y+1, x, "o:%-8.8s", has_output ? output_buf : fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
mvprintw(footer_y+2, 0, "Selected: Grid %d, Row %d, Col %d",
|
||||||
selected_grid, selected_row, selected_col);
|
selected_grid, selected_row, selected_col);
|
||||||
if (show_help) {
|
if (show_help) {
|
||||||
attron(COLOR_PAIR(COLOR_HELP));
|
attron(COLOR_PAIR(COLOR_HELP));
|
||||||
mvprintw(GRID_ROWS*CELL_HEIGHT+4, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, R rack, ? help, Esc/Q quit");
|
mvprintw(footer_y+3, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, R rack, ? help, Esc/Q quit");
|
||||||
attroff(COLOR_PAIR(COLOR_HELP));
|
attroff(COLOR_PAIR(COLOR_HELP));
|
||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
@@ -335,14 +362,43 @@ void tui_run(void) {
|
|||||||
rack_mode = false;
|
rack_mode = false;
|
||||||
} else if ((strcmp(first, "from") == 0 || strcmp(first, "to") == 0)) {
|
} else if ((strcmp(first, "from") == 0 || strcmp(first, "to") == 0)) {
|
||||||
char *potential_arg = strtok(NULL, " ");
|
char *potential_arg = strtok(NULL, " ");
|
||||||
if (potential_arg == NULL) {
|
const char *port_name = NULL;
|
||||||
// no argument: launch fzf
|
if (potential_arg != NULL) {
|
||||||
|
port_name = potential_arg;
|
||||||
|
const char *colon = strchr(port_name, ':');
|
||||||
|
if (colon) port_name = colon + 1;
|
||||||
|
} else {
|
||||||
script_handle_fzf_command(first);
|
script_handle_fzf_command(first);
|
||||||
|
if (g_selected_port[0] != '\0') {
|
||||||
|
port_name = g_selected_port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Store the full port name (before stripping colon) for footer fallback */
|
||||||
|
if (potential_arg) {
|
||||||
|
if (strcmp(first, "from") == 0)
|
||||||
|
strncpy(g_from_port, potential_arg, sizeof(g_from_port)-1);
|
||||||
|
else
|
||||||
|
strncpy(g_to_port, potential_arg, sizeof(g_to_port)-1);
|
||||||
|
} else if (g_selected_port[0]) {
|
||||||
|
if (strcmp(first, "from") == 0)
|
||||||
|
strncpy(g_from_port, g_selected_port, sizeof(g_from_port)-1);
|
||||||
|
else
|
||||||
|
strncpy(g_to_port, g_selected_port, sizeof(g_to_port)-1);
|
||||||
|
}
|
||||||
|
if (port_name) {
|
||||||
|
const char *looper_port = (strcmp(first, "from") == 0) ? "looper:input" : "looper:output";
|
||||||
|
int ret = carla_connect(0, port_name, looper_port);
|
||||||
|
if (ret == 0) {
|
||||||
|
log_msg("Connected %s -> %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();
|
draw_grid();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
int dummy_id;
|
int dummy_id;
|
||||||
handle_client_command(colon_buf, &dummy_id);
|
handle_client_command(colon_buf, &dummy_id);
|
||||||
draw_grid();
|
draw_grid();
|
||||||
|
|||||||
114
e2e/test.ts
114
e2e/test.ts
@@ -898,7 +898,7 @@ async function testStressRandomUsage(): Promise<void> {
|
|||||||
|
|
||||||
if (keysSent % CHECK_INTERVAL === 0) {
|
if (keysSent % CHECK_INTERVAL === 0) {
|
||||||
// Wait a little for TUI to settle
|
// Wait a little for TUI to settle
|
||||||
await wait(300);
|
await wait(500);
|
||||||
|
|
||||||
// Check engine alive
|
// Check engine alive
|
||||||
if (engine.pid && !isProcessAlive(engine.pid)) {
|
if (engine.pid && !isProcessAlive(engine.pid)) {
|
||||||
@@ -911,13 +911,19 @@ async function testStressRandomUsage(): Promise<void> {
|
|||||||
throw new Error("Engine crash during stress test");
|
throw new Error("Engine crash during stress test");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check TUI pane integrity (non‑empty, at least has header and a cell)
|
// Wait a little more for TUI to settle and pane to be captured fully
|
||||||
let pane = tmuxCapturePane("looper", "0");
|
await wait(1000);
|
||||||
if (!pane || pane.trim() === "") {
|
|
||||||
|
// 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);
|
await wait(200);
|
||||||
pane = tmuxCapturePane("looper", "0");
|
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(` FAIL: TUI pane appears corrupted at key ${keysSent}`);
|
||||||
console.log(" Pane:\n" + (pane ? pane.slice(0, 1000) : "(empty)"));
|
console.log(" Pane:\n" + (pane ? pane.slice(0, 1000) : "(empty)"));
|
||||||
teardownTest();
|
teardownTest();
|
||||||
@@ -1001,6 +1007,103 @@ async function testKeyPressLatency(): Promise<void> {
|
|||||||
teardownTest();
|
teardownTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testFromToAudioPass(): Promise<void> {
|
||||||
|
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<void> {
|
async function main(): Promise<void> {
|
||||||
console.log("=== Looper E2E Tests ===\n");
|
console.log("=== Looper E2E Tests ===\n");
|
||||||
|
|
||||||
@@ -1016,6 +1119,7 @@ async function main(): Promise<void> {
|
|||||||
testRapidKeyMashConsistency,
|
testRapidKeyMashConsistency,
|
||||||
*/
|
*/
|
||||||
testRecordOnHighRow,
|
testRecordOnHighRow,
|
||||||
|
testFromToAudioPass,
|
||||||
testRecordMoveRecord,
|
testRecordMoveRecord,
|
||||||
testStressRandomUsage,
|
testStressRandomUsage,
|
||||||
testKeyPressLatency
|
testKeyPressLatency
|
||||||
|
|||||||
Reference in New Issue
Block a user