Merge branch 'e2e' into integrate-fzf

This commit is contained in:
Loic Coenen
2026-05-24 14:15:45 +00:00
20 changed files with 1680 additions and 148 deletions

View File

@@ -1,5 +1,6 @@
CC = gcc
CFLAGS = -Wall -Wextra -Wpedantic -std=gnu11 -Isrc -I../engine/src
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc -I../engine/src -fsanitize=address -fno-omit-frame-pointer
CARLA_INC = -I/usr/include/carla -I/usr/include/carla/includes
CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
@@ -20,7 +21,7 @@ TEST_INTEGRATION_BIN = test_integration
all: looper-client test_status_parse
looper-client: src/main.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
$(CC) $(CFLAGS) $(CARLA_INC) -fsanitize=address -o $@ $^ $(CARLA_LIB) -ljack -lncurses
test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o test_status_parse tests/test_status_parse.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ) $(CARLA_LIB) -ljack -lncurses
@@ -93,7 +94,7 @@ TEST_CLIENT_OBJ = tests/test_client.o
$(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ)
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
# --- Carla host tests ---

View File

@@ -2,6 +2,7 @@
#include <stdlib.h>
#include <CarlaHost.h>
#include <CarlaBackend.h>
#include <stdio.h>
#include <string.h>
#include "carla_host.h"
@@ -139,21 +140,25 @@ int carla_connect(int id, const char *port_name, const char *looper_port) {
if (!port_name || !looper_port) return -1;
if (!jack_client) return -1;
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);
if (ret != 0) return -1;
// Store the connection so we can disconnect it later
if (conn_count < MAX_CONNECTIONS) {
connections[conn_count].plugin_id = id;
strncpy(connections[conn_count].plugin_port, port_name,
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, looper_port,
sizeof(connections[conn_count].looper_port) - 1);
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
conn_count++;
if (conn_count >= MAX_CONNECTIONS) {
fprintf(stderr, "WARN: connection array full, refusing new connection\n");
return -1;
}
connections[conn_count].plugin_id = id;
strncpy(connections[conn_count].plugin_port, port_name,
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, looper_port,
sizeof(connections[conn_count].looper_port) - 1);
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
conn_count++;
return 0;
}

View File

@@ -1,39 +1,67 @@
#define _POSIX_C_SOURCE 200809L
#include "tui.h"
#include <ncurses.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include <math.h>
#include <time.h>
#include "carla_host.h"
#include "client_cmd.h"
#include "plugins.h"
#include "script.h"
#include <CarlaHost.h>
#include "log.h"
/* ---------- engine alive indicator ---------- */
static bool engine_running = false;
static bool debug_mode = false;
/* Persistent FIFO fds open once and reuse */
static int cmd_fifo_fd = -1;
static int status_fifo_fd = -1;
/* ---------- FIFO command helper ---------- */
int send_command(const char *cmd) {
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
if (fd < 0) return -1;
if (debug_mode)
fprintf(stderr, "DEBUG: send_command(%s)\n", cmd);
if (cmd_fifo_fd < 0) {
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
cmd_fifo_fd = open(fifo_path, O_WRONLY);
if (cmd_fifo_fd < 0) {
perror("open cmd FIFO");
return -1;
}
}
size_t len = strlen(cmd);
int n = write(fd, cmd, len);
int n = write(cmd_fifo_fd, cmd, len);
if (n == (int)len && cmd[len-1] != '\n')
write(fd, "\n", 1);
close(fd);
write(cmd_fifo_fd, "\n", 1);
return (n >= 0) ? 0 : -1;
}
/* ---------- Stub functions (no engine) ---------- */
// Clip states dummy values used as placeholders
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
static const char *clip_state_string(ClipState s) {
switch (s) {
case CLIP_EMPTY: return " ";
case CLIP_RECORDING: return "R";
case CLIP_LOOPING: return "L";
case CLIP_STOPPED: return "P";
default: return "?";
}
}
/* Grid dimensions */
#define GRID_ROWS 8
@@ -124,6 +152,17 @@ static void draw_cell(int grid, int row, int col, bool selected) {
mvaddch(y+dy, x+dx, ' ');
mvprintw(y+1, x+1, "%2d", 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' :
(s == STATE_LOOPING) ? 'L' :
(s == STATE_PAUSED) ? 'P' : '.';
int state_color = (s == STATE_RECORD) ? COLOR_RECORDING :
(s == STATE_LOOPING) ? COLOR_LOOPING :
(s == STATE_PAUSED) ? COLOR_STOPPED : COLOR_EMPTY;
attron(COLOR_PAIR(state_color));
mvaddch(y+2, x + CELL_WIDTH / 2, state_char);
attroff(COLOR_PAIR(state_color));
}
static void draw_rack(void) {
@@ -163,7 +202,7 @@ static void draw_grid(void) {
}
clear();
attron(A_BOLD);
mvprintw(0,0,"JACK Looper - Client (FIFO only)");
mvprintw(0,0,"JACK Looper - Client (FIFO only) %s", engine_running ? "[online]" : "[offline]");
attroff(A_BOLD);
for (int r=0; r<GRID_ROWS; r++)
for (int c=0; c<GRID_COLS; c++)
@@ -180,8 +219,10 @@ static void draw_grid(void) {
/* ---------- TUI init ---------- */
void tui_init(void) {
log_init();
initscr();
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
debug_mode = (getenv("LOOPER_DEBUG") != NULL);
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
start_color();
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
@@ -206,34 +247,42 @@ static char colon_buf[256];
static int colon_len = 0;
static bool in_colon = false;
void tui_run(void) {
draw_grid();
while (1) {
/* read any available status lines */
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
if (fd >= 0) {
char buf[256];
int n = read(fd, buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
char *line = buf;
while (*line) {
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
int ch, sc;
ChannelState st;
if (parse_status_line(line, &ch, &sc, &st)) {
if (ch >= 0 && ch < GRID_ROWS * GRID_COLS)
cell_state[ch] = st;
}
if (nl) {
*nl = '\n';
line = nl + 1;
} else break;
/* Read the status FIFO once and update cell_state array */
static void tui_read_status(void) {
if (status_fifo_fd < 0) {
status_fifo_fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
if (status_fifo_fd < 0) return;
}
char buf[4096];
int n = read(status_fifo_fd, buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
char *line = buf;
while (*line) {
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
int ch, sc; ChannelState st;
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;
}
}
close(fd);
if (nl) { *nl = '\n'; line = nl + 1; } else break;
}
}
/* keep fd open */
}
void tui_run(void) {
draw_grid();
nodelay(stdscr, TRUE); // nonblocking input getch returns ERR when no key is pressed
while (1) {
/* read status FIFO once per iteration always */
tui_read_status();
/* Check if engine is alive */
engine_running = (access(STATUS_FIFO, F_OK) == 0);
/* read any available note events (for script macros) */
int nfd = open(NOTES_FIFO, O_RDONLY | O_NONBLOCK);
@@ -257,13 +306,23 @@ void tui_run(void) {
close(nfd);
}
/* redraw grid (status may have changed no extra key needed) */
{
struct timespec t1, t2;
clock_gettime(CLOCK_MONOTONIC, &t1);
draw_grid();
clock_gettime(CLOCK_MONOTONIC, &t2);
double ms = (t2.tv_sec - t1.tv_sec)*1000.0 + (t2.tv_nsec - t1.tv_nsec)/1000000.0;
if (ms > 200) log_msg("SLOW draw_grid: %f ms", ms);
}
int chc = getch();
if (in_colon) {
int chc = getch();
if (chc == '\n') {
colon_buf[colon_len] = '\0';
colon_len = 0;
in_colon = false;
// Check first token before calling handle_client_command
char cmd_copy[256];
strncpy(cmd_copy, colon_buf, sizeof(cmd_copy)-1);
cmd_copy[sizeof(cmd_copy)-1] = '\0';
@@ -274,6 +333,14 @@ void tui_run(void) {
rack_selected = 0;
} else if (strcmp(first, "grid") == 0) {
rack_mode = false;
} else if ((strcmp(first, "from") == 0 || strcmp(first, "to") == 0)) {
char *potential_arg = strtok(NULL, " ");
if (potential_arg == NULL) {
// no argument: launch fzf
script_handle_fzf_command(first);
draw_grid();
continue;
}
}
}
int dummy_id;
@@ -294,10 +361,10 @@ void tui_run(void) {
clrtoeol();
move(LINES-1, colon_len+1);
refresh();
napms(50);
continue;
}
int chc = getch();
if (chc == ':') {
in_colon = true;
colon_len = 0;
@@ -308,6 +375,7 @@ void tui_run(void) {
refresh();
continue;
}
switch (chc) {
case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break;
case 'j': case KEY_DOWN: selected_row = (selected_row+1)%GRID_ROWS; break;
@@ -315,8 +383,20 @@ void tui_run(void) {
case 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
case 't': {
char cmd[32];
log_msg("DIAG t pressed: selected_row=%d selected_col=%d", selected_row, selected_col);
// First bind to the selected channel so engine knows which channel to operate on
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
send_command(cmd);
log_msg("DIAG sent: %s", cmd);
// Then set the scene for that channel
snprintf(cmd, sizeof(cmd), "set_scene %d %d\n", selected_col, selected_row);
send_command(cmd);
log_msg("DIAG sent: %s", cmd);
// Finally trigger record
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
send_command(cmd);
log_msg("DIAG sent: %s", cmd);
// tui_read_status already called at top of loop
break;
}
case 's':
@@ -339,6 +419,8 @@ void tui_run(void) {
break;
case 'b': {
char cmd[16];
snprintf(cmd, sizeof(cmd), "set_scene %d %d\n", selected_col, selected_row);
send_command(cmd);
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
send_command(cmd);
break;
@@ -369,6 +451,9 @@ void tui_run(void) {
break;
}
return;
case ERR:
/* no key pressed just continue the loop */
break;
default:
if (rack_mode) {
switch (chc) {
@@ -388,7 +473,6 @@ void tui_run(void) {
break;
case 'b': case 'B':
plugin_set_bypass(rack_selected, true);
// toggle would be better, but for now just enable bypass
break;
case 'd': case 'D':
plugin_unload(rack_selected);
@@ -405,7 +489,7 @@ void tui_run(void) {
}
break;
}
draw_grid();
napms(50); // avoid busywaste grid redraws frequently enough
}
}
@@ -473,6 +557,14 @@ char* tui_fzf_select(const char *const items[], size_t count, const char *prompt
}
void tui_cleanup(void) {
if (cmd_fifo_fd >= 0) {
close(cmd_fifo_fd);
cmd_fifo_fd = -1;
}
if (status_fifo_fd >= 0) {
close(status_fifo_fd);
status_fifo_fd = -1;
}
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
/* free script note allocations */
script_cleanup();