feat: add direct JACK port connection and VU meter support

This commit is contained in:
Loic Coenen
2026-05-27 20:28:09 +00:00
committed by Loic Coenen (aider)
parent 1e62ec9310
commit 316320c294
12 changed files with 455 additions and 138 deletions

View File

@@ -58,6 +58,11 @@ int carla_init_jack(void) {
jack_status_t status;
jack_client = jack_client_open("looper-connector", JackNoStartServer, &status);
// It's okay if jack_client is NULL; we still try Carla
if (jack_client) {
if (jack_activate(jack_client) != 0) {
fprintf(stderr, "WARN: could not activate looper-connector JACK client\n");
}
}
#endif
// 2) Create the Carla host handle
@@ -83,6 +88,28 @@ int carla_init_jack(void) {
return 0;
}
int carla_connect_direct(const char *source, const char *target) {
if (!source || !target) return -1;
if (!jack_client) return -1;
int ret = jack_connect(jack_client, source, target);
if (ret != 0) {
fprintf(stderr, "JACK connect failed %s -> %s (ret=%d)\n", source, target, ret);
return ret;
}
// Store the connection so get_connected_port can find it
if (conn_count < MAX_CONNECTIONS) {
strncpy(connections[conn_count].plugin_port, source,
sizeof(connections[conn_count].plugin_port)-1);
connections[conn_count].plugin_port[sizeof(connections[conn_count].plugin_port)-1] = '\0';
strncpy(connections[conn_count].looper_port, target,
sizeof(connections[conn_count].looper_port)-1);
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port)-1] = '\0';
connections[conn_count].plugin_id = -1; // direct connection
conn_count++;
}
return 0;
}
void carla_cleanup_jack(void) {
if (handle != NULL) {
carla_engine_close(handle);
@@ -145,7 +172,7 @@ int carla_connect(int id, const char *port_name, const char *looper_port) {
fprintf(stderr, "CARLA_CONNECT: plugin_id=%d conn_count=%d port=%s looper=%s\n",
id, conn_count, port_name, looper_port);
// Real JACK port connection
int ret = jack_connect(jack_client, looper_port, port_name);
int ret = jack_connect(jack_client, port_name, looper_port);
if (ret != 0) {
fprintf(stderr, "CARLA_CONNECT: jack_connect(%s, %s) failed with %d\n", looper_port, port_name, ret);
return -1;
@@ -295,6 +322,15 @@ bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufs
return true;
}
}
// Also look for direct connections to looper:input / looper:output (channel 0)
const char *direct_needle = is_input ? "looper:input" : "looper:output";
for (int i = 0; i < conn_count; i++) {
if (strcmp(connections[i].looper_port, direct_needle) == 0) {
strncpy(buf, connections[i].plugin_port, bufsize - 1);
buf[bufsize - 1] = '\0';
return true;
}
}
buf[0] = '\0';
return false;
}

View File

@@ -16,8 +16,9 @@ int carla_unload(int id);
int carla_connect(int id, const char *port_name, const char *looper_port);
int carla_disconnect(const char *from, const char *to);
void carla_set_bypass(int id, bool bypass);
int carla_connect_direct(const char *source, const char *target);
int carla_get_ports(const char *type, char ***ports, int *count);
/* Get internal Carla host handle, may be NULL */
int carla_disconnect_plugin(int id);
CarlaHostHandle carla_get_handle(void);

View File

@@ -1,11 +1,13 @@
#include "client_cmd.h"
#include "plugins.h"
#include "carla_host.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static char from_port[256] = "";
static char to_port[256] = "";
char g_connect_error[512] = "";
const char* get_stored_from(void) { return from_port; }
const char* get_stored_to(void) { return to_port; }
@@ -34,18 +36,32 @@ int handle_client_command(const char *input, int *out_id) {
if (strcmp(token, "from") == 0) {
const char *port = strtok(NULL, " ");
if (!port) return -1;
strncpy(from_port, port, sizeof(from_port)-1);
from_port[sizeof(from_port)-1] = '\0';
return 0;
int ret = carla_connect_direct(port, "looper:input");
if (ret == 0) {
strncpy(from_port, port, sizeof(from_port)-1);
from_port[sizeof(from_port)-1] = '\0';
g_connect_error[0] = '\0';
} else {
snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: %s -> looper:input (ret=%d)", port, ret);
}
return ret;
}
// --- to <port> ---
if (strcmp(token, "to") == 0) {
const char *port = strtok(NULL, " ");
if (!port) return -1;
strncpy(to_port, port, sizeof(to_port)-1);
to_port[sizeof(to_port)-1] = '\0';
return 0;
int ret = carla_connect_direct("looper:output", port);
if (ret == 0) {
strncpy(to_port, port, sizeof(to_port)-1);
to_port[sizeof(to_port)-1] = '\0';
g_connect_error[0] = '\0';
} else {
snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: looper:output -> %s (ret=%d)", port, ret);
}
return ret;
}
// --- addplugin <path> ---

View File

@@ -13,4 +13,6 @@ int handle_client_command(const char *input, int *out_id);
const char* get_stored_from(void);
const char* get_stored_to(void);
extern char g_connect_error[512];
#endif

View File

@@ -1,6 +1,7 @@
#include "tui.h"
#include "script.h"
#include "log.h"
#include "carla_host.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
@@ -8,6 +9,10 @@
int main(int argc, char *argv[]) {
log_init();
if (carla_init_jack() != 0) {
log_msg("Warning: could not initialise JACK connector client");
}
const char *script_path = NULL;
if (argc > 2 && strcmp(argv[1], "-s") == 0) {

View File

@@ -56,6 +56,32 @@ int send_command(const char *cmd) {
return (n >= 0) ? 0 : -1;
}
/* ---------- Helper to resolve channel port ---------- */
static bool carla_resolve_channel_port(int channel, bool is_to, char *buf, size_t bufsize) {
char **ports = NULL;
int count = 0;
if (carla_get_ports(NULL, &ports, &count) != 0) {
return false;
}
char pattern[64];
if (is_to) {
snprintf(pattern, sizeof(pattern), "ch%dout", channel);
} else {
snprintf(pattern, sizeof(pattern), "ch%din", channel);
}
bool found = false;
for (int i = 0; i < count && !found; i++) {
if (strstr(ports[i], pattern)) {
strncpy(buf, ports[i], bufsize - 1);
buf[bufsize - 1] = '\0';
found = true;
}
free(ports[i]);
}
free(ports);
return found;
}
/* ---------- Stub functions (no engine) ---------- */
// Clip states dummy values used as placeholders
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
@@ -115,6 +141,12 @@ typedef struct {
static FuzzySearch fuzzy_search = {0};
/* ---------- Parse status line from engine status FIFO ---------- */
static float vu_level[16] = {0.0f}; /* perchannel RMS level (index = channel number) */
static bool parse_level_line(const char *line, int *ch, float *level) {
return sscanf(line, "CH=%d LEVEL=%f", ch, level) == 2;
}
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state) {
int sta;
if (sscanf(line, "CH=%d SC=%d STATE=%d", ch, scene, &sta) == 3) {
@@ -162,6 +194,7 @@ static void draw_cell(int grid, int row, int col, bool selected) {
(s == STATE_PAUSED) ? 'P' : '.';
mvprintw(y, x, "ch %2d", ch);
mvaddch(y, x+5, state_char);
attroff(COLOR_PAIR(color));
}
@@ -230,15 +263,39 @@ static void draw_grid(void) {
}
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, x, "i:%-20.20s", has_input ? input_buf : fallback);
mvprintw(footer_y+1, x, "o:%-20.20s", has_output ? output_buf : fallback);
}
mvprintw(footer_y+2, 0, "Selected: Grid %d, Row %d, Col %d",
/* VU meter line per channel */
int vu_y = footer_y + 2;
for (int c = 0; c < GRID_COLS; c++) {
int x = c * CELL_WIDTH + 1;
float level = vu_level[c];
int bar_width = CELL_WIDTH - 2;
int filled = (int)(level * bar_width);
if (filled > bar_width) filled = bar_width;
mvprintw(vu_y, x, "%*s", CELL_WIDTH, "");
for (int i = 0; i < filled; i++) {
char ch = (i < bar_width * 0.3f) ? '.' :
(i < bar_width * 0.6f) ? 'x' : '#';
mvaddch(vu_y, x + 1 + i, ch);
}
}
/* Display connection error if any */
if (g_connect_error[0]) {
attron(COLOR_PAIR(COLOR_RECORDING));
mvprintw(vu_y + 1, 0, "ERROR: %-60s", g_connect_error);
attroff(COLOR_PAIR(COLOR_RECORDING));
g_connect_error[0] = '\0';
}
mvprintw(vu_y + 2, 0, "Selected: Grid %d, Row %d, Col %d",
selected_grid, selected_row, selected_col);
if (show_help) {
attron(COLOR_PAIR(COLOR_HELP));
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");
mvprintw(vu_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));
}
refresh();
@@ -288,8 +345,11 @@ static void tui_read_status(void) {
while (*line) {
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
int ch, sc; ChannelState st;
if (parse_status_line(line, &ch, &sc, &st)) {
int ch, sc; ChannelState st; float level_val;
if (parse_level_line(line, &ch, &level_val)) {
if (ch >= 0 && ch < 16)
vu_level[ch] = level_val;
} else if (parse_status_line(line, &ch, &sc, &st)) {
if (ch >= 0 && ch < GRID_COLS && sc >= 0 && sc < GRID_ROWS) {
int idx = sc * GRID_COLS + ch;
cell_state[idx] = st;
@@ -365,32 +425,42 @@ void tui_run(void) {
const char *port_name = NULL;
if (potential_arg != NULL) {
port_name = potential_arg;
const char *colon = strchr(port_name, ':');
if (colon) port_name = colon + 1;
// Do NOT strip client prefix keep full JACK port name
} else {
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);
}
/* port assignment happens only after successful connection below */
if (port_name) {
const char *looper_port = (strcmp(first, "from") == 0) ? "looper:input" : "looper:output";
int ret = carla_connect(0, port_name, looper_port);
/* Resolve the looper port for the currently selected channel */
char looper_port[256] = "";
const bool is_to = (strcmp(first, "to") == 0);
int channel = selected_col; // selected column = channel number
bool found = carla_resolve_channel_port(channel, is_to, looper_port, sizeof(looper_port));
if (!found) {
/* Fallback to generic name (may not exist) */
if (is_to)
snprintf(looper_port, sizeof(looper_port), "ch%dout", channel);
else
snprintf(looper_port, sizeof(looper_port), "ch%din", channel);
/* The actual port name includes a PID suffix, but we try anyway */
}
int ret = carla_connect_direct(port_name, looper_port);
if (ret == 0) {
if (is_to) {
strncpy(g_to_port, port_name, sizeof(g_to_port)-1);
g_to_port[sizeof(g_to_port)-1] = '\0';
} else {
strncpy(g_from_port, port_name, sizeof(g_from_port)-1);
g_from_port[sizeof(g_from_port)-1] = '\0';
}
g_connect_error[0] = '\0';
log_msg("Connected %s -> %s", port_name, looper_port);
} else {
snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: %s -> %s (ret=%d)", port_name, looper_port, ret);
log_msg("Failed to connect %s -> %s (ret=%d)", port_name, looper_port, ret);
}
}