Merge branch 'e2e' into integrate-fzf
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
CC = gcc
|
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_INC = -I/usr/include/carla -I/usr/include/carla/includes
|
||||||
CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
|
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
|
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)
|
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)
|
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
|
$(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
|
$(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h
|
||||||
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
|
$(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
|
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||||
|
|
||||||
# --- Carla host tests ---
|
# --- Carla host tests ---
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <CarlaHost.h>
|
#include <CarlaHost.h>
|
||||||
#include <CarlaBackend.h>
|
#include <CarlaBackend.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "carla_host.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 (!port_name || !looper_port) return -1;
|
||||||
if (!jack_client) 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
|
// 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) 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) {
|
||||||
connections[conn_count].plugin_id = id;
|
fprintf(stderr, "WARN: connection array full, refusing new connection\n");
|
||||||
strncpy(connections[conn_count].plugin_port, port_name,
|
return -1;
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
174
client/src/tui.c
174
client/src/tui.c
@@ -1,39 +1,67 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <ctype.h>
|
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <math.h>
|
#include <time.h>
|
||||||
|
|
||||||
#include "carla_host.h"
|
#include "carla_host.h"
|
||||||
#include "client_cmd.h"
|
#include "client_cmd.h"
|
||||||
#include "plugins.h"
|
#include "plugins.h"
|
||||||
#include "script.h"
|
#include "script.h"
|
||||||
#include <CarlaHost.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 ---------- */
|
/* ---------- FIFO command helper ---------- */
|
||||||
int send_command(const char *cmd) {
|
int send_command(const char *cmd) {
|
||||||
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
if (debug_mode)
|
||||||
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
fprintf(stderr, "DEBUG: send_command(%s)\n", cmd);
|
||||||
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
|
|
||||||
if (fd < 0) return -1;
|
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);
|
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')
|
if (n == (int)len && cmd[len-1] != '\n')
|
||||||
write(fd, "\n", 1);
|
write(cmd_fifo_fd, "\n", 1);
|
||||||
close(fd);
|
|
||||||
return (n >= 0) ? 0 : -1;
|
return (n >= 0) ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Stub functions (no engine) ---------- */
|
/* ---------- Stub functions (no engine) ---------- */
|
||||||
// Clip states – dummy values used as placeholders
|
// Clip states – dummy values used as placeholders
|
||||||
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
|
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 */
|
/* Grid dimensions */
|
||||||
#define GRID_ROWS 8
|
#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, ' ');
|
mvaddch(y+dy, x+dx, ' ');
|
||||||
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
||||||
attroff(COLOR_PAIR(color));
|
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) {
|
static void draw_rack(void) {
|
||||||
@@ -163,7 +202,7 @@ static void draw_grid(void) {
|
|||||||
}
|
}
|
||||||
clear();
|
clear();
|
||||||
attron(A_BOLD);
|
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);
|
attroff(A_BOLD);
|
||||||
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++)
|
||||||
@@ -180,8 +219,10 @@ static void draw_grid(void) {
|
|||||||
|
|
||||||
/* ---------- TUI init ---------- */
|
/* ---------- TUI init ---------- */
|
||||||
void tui_init(void) {
|
void tui_init(void) {
|
||||||
|
log_init();
|
||||||
initscr();
|
initscr();
|
||||||
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
|
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); }
|
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
|
||||||
start_color();
|
start_color();
|
||||||
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
||||||
@@ -206,34 +247,42 @@ static char colon_buf[256];
|
|||||||
static int colon_len = 0;
|
static int colon_len = 0;
|
||||||
static bool in_colon = false;
|
static bool in_colon = false;
|
||||||
|
|
||||||
void tui_run(void) {
|
/* Read the status FIFO once and update cell_state array */
|
||||||
draw_grid();
|
static void tui_read_status(void) {
|
||||||
while (1) {
|
if (status_fifo_fd < 0) {
|
||||||
/* read any available status lines */
|
status_fifo_fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
if (status_fifo_fd < 0) return;
|
||||||
if (fd >= 0) {
|
}
|
||||||
char buf[256];
|
char buf[4096];
|
||||||
int n = read(fd, buf, sizeof(buf)-1);
|
int n = read(status_fifo_fd, buf, sizeof(buf)-1);
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
buf[n] = '\0';
|
buf[n] = '\0';
|
||||||
char *line = buf;
|
char *line = buf;
|
||||||
while (*line) {
|
while (*line) {
|
||||||
char *nl = strchr(line, '\n');
|
char *nl = strchr(line, '\n');
|
||||||
if (nl) *nl = '\0';
|
if (nl) *nl = '\0';
|
||||||
int ch, sc;
|
int ch, sc; ChannelState st;
|
||||||
ChannelState st;
|
if (parse_status_line(line, &ch, &sc, &st)) {
|
||||||
if (parse_status_line(line, &ch, &sc, &st)) {
|
if (ch >= 0 && ch < GRID_COLS && sc >= 0 && sc < GRID_ROWS) {
|
||||||
if (ch >= 0 && ch < GRID_ROWS * GRID_COLS)
|
int idx = sc * GRID_COLS + ch;
|
||||||
cell_state[ch] = st;
|
cell_state[idx] = st;
|
||||||
}
|
|
||||||
if (nl) {
|
|
||||||
*nl = '\n';
|
|
||||||
line = nl + 1;
|
|
||||||
} else break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(fd);
|
if (nl) { *nl = '\n'; line = nl + 1; } else break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* keep fd open */
|
||||||
|
}
|
||||||
|
|
||||||
|
void tui_run(void) {
|
||||||
|
draw_grid();
|
||||||
|
nodelay(stdscr, TRUE); // non‑blocking 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) */
|
/* read any available note events (for script macros) */
|
||||||
int nfd = open(NOTES_FIFO, O_RDONLY | O_NONBLOCK);
|
int nfd = open(NOTES_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
@@ -257,13 +306,23 @@ void tui_run(void) {
|
|||||||
close(nfd);
|
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) {
|
if (in_colon) {
|
||||||
int chc = getch();
|
|
||||||
if (chc == '\n') {
|
if (chc == '\n') {
|
||||||
colon_buf[colon_len] = '\0';
|
colon_buf[colon_len] = '\0';
|
||||||
colon_len = 0;
|
colon_len = 0;
|
||||||
in_colon = false;
|
in_colon = false;
|
||||||
// Check first token before calling handle_client_command
|
|
||||||
char cmd_copy[256];
|
char cmd_copy[256];
|
||||||
strncpy(cmd_copy, colon_buf, sizeof(cmd_copy)-1);
|
strncpy(cmd_copy, colon_buf, sizeof(cmd_copy)-1);
|
||||||
cmd_copy[sizeof(cmd_copy)-1] = '\0';
|
cmd_copy[sizeof(cmd_copy)-1] = '\0';
|
||||||
@@ -274,6 +333,14 @@ void tui_run(void) {
|
|||||||
rack_selected = 0;
|
rack_selected = 0;
|
||||||
} else if (strcmp(first, "grid") == 0) {
|
} else if (strcmp(first, "grid") == 0) {
|
||||||
rack_mode = false;
|
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;
|
int dummy_id;
|
||||||
@@ -294,10 +361,10 @@ void tui_run(void) {
|
|||||||
clrtoeol();
|
clrtoeol();
|
||||||
move(LINES-1, colon_len+1);
|
move(LINES-1, colon_len+1);
|
||||||
refresh();
|
refresh();
|
||||||
|
napms(50);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chc = getch();
|
|
||||||
if (chc == ':') {
|
if (chc == ':') {
|
||||||
in_colon = true;
|
in_colon = true;
|
||||||
colon_len = 0;
|
colon_len = 0;
|
||||||
@@ -308,6 +375,7 @@ void tui_run(void) {
|
|||||||
refresh();
|
refresh();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (chc) {
|
switch (chc) {
|
||||||
case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break;
|
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;
|
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 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
|
||||||
case 't': {
|
case 't': {
|
||||||
char cmd[32];
|
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);
|
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
|
||||||
send_command(cmd);
|
send_command(cmd);
|
||||||
|
log_msg("DIAG sent: %s", cmd);
|
||||||
|
// tui_read_status already called at top of loop
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 's':
|
case 's':
|
||||||
@@ -339,6 +419,8 @@ void tui_run(void) {
|
|||||||
break;
|
break;
|
||||||
case 'b': {
|
case 'b': {
|
||||||
char cmd[16];
|
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);
|
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||||||
send_command(cmd);
|
send_command(cmd);
|
||||||
break;
|
break;
|
||||||
@@ -369,6 +451,9 @@ void tui_run(void) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case ERR:
|
||||||
|
/* no key pressed – just continue the loop */
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (rack_mode) {
|
if (rack_mode) {
|
||||||
switch (chc) {
|
switch (chc) {
|
||||||
@@ -388,7 +473,6 @@ void tui_run(void) {
|
|||||||
break;
|
break;
|
||||||
case 'b': case 'B':
|
case 'b': case 'B':
|
||||||
plugin_set_bypass(rack_selected, true);
|
plugin_set_bypass(rack_selected, true);
|
||||||
// toggle would be better, but for now just enable bypass
|
|
||||||
break;
|
break;
|
||||||
case 'd': case 'D':
|
case 'd': case 'D':
|
||||||
plugin_unload(rack_selected);
|
plugin_unload(rack_selected);
|
||||||
@@ -405,7 +489,7 @@ void tui_run(void) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
draw_grid();
|
napms(50); // avoid busy‑waste – 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) {
|
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);
|
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||||||
/* free script note allocations */
|
/* free script note allocations */
|
||||||
script_cleanup();
|
script_cleanup();
|
||||||
|
|||||||
78
e2e/gen_tone.c
Normal file
78
e2e/gen_tone.c
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include <jack/jack.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static jack_port_t *output_port;
|
||||||
|
static jack_client_t *client;
|
||||||
|
static volatile int running = 1;
|
||||||
|
static double phase = 0.0;
|
||||||
|
static double freq = 440.0;
|
||||||
|
static int sample_rate = 48000;
|
||||||
|
static int total_samples = 0;
|
||||||
|
static int samples_written = 0;
|
||||||
|
|
||||||
|
int process(jack_nframes_t nframes, void *arg) {
|
||||||
|
jack_default_audio_sample_t *out =
|
||||||
|
(jack_default_audio_sample_t *)jack_port_get_buffer(output_port, nframes);
|
||||||
|
if (!out) return 0;
|
||||||
|
for (jack_nframes_t i = 0; i < nframes; i++) {
|
||||||
|
out[i] = sin(2 * M_PI * phase);
|
||||||
|
phase += freq / sample_rate;
|
||||||
|
if (phase >= 1.0) phase -= 1.0;
|
||||||
|
samples_written++;
|
||||||
|
if (total_samples > 0 && samples_written >= total_samples) {
|
||||||
|
running = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown(void *arg) { running = 0; }
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "Usage: gen_tone <duration_seconds> <target_port> [frequency]\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
double duration = atof(argv[1]);
|
||||||
|
const char *target = argv[2];
|
||||||
|
if (argc >= 4) freq = atof(argv[3]);
|
||||||
|
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("gen_tone", JackNoStartServer, &status);
|
||||||
|
if (!client) { fprintf(stderr, "Cannot open JACK client\n"); return 1; }
|
||||||
|
|
||||||
|
sample_rate = jack_get_sample_rate(client);
|
||||||
|
total_samples = (int)(duration * sample_rate + 0.5);
|
||||||
|
|
||||||
|
output_port = jack_port_register(client, "output",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
if (!output_port) { fprintf(stderr, "Cannot register port\n"); return 1; }
|
||||||
|
|
||||||
|
jack_set_process_callback(client, process, NULL);
|
||||||
|
jack_on_shutdown(client, shutdown, NULL);
|
||||||
|
if (jack_activate(client)) { fprintf(stderr, "Cannot activate client\n"); return 1; }
|
||||||
|
|
||||||
|
// Connect to target
|
||||||
|
const char **ports = jack_get_ports(client, target,
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput);
|
||||||
|
if (!ports || !ports[0]) {
|
||||||
|
fprintf(stderr, "Target port '%s' not found\n", target);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (jack_connect(client, jack_port_name(output_port), ports[0])) {
|
||||||
|
fprintf(stderr, "Cannot connect port\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (running) sleep(1);
|
||||||
|
|
||||||
|
jack_client_close(client);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
13
e2e/package.json
Normal file
13
e2e/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "looper-e2e",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "tsx test.ts",
|
||||||
|
"compile": "tsc"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"tsx": "^4.0.0",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1050
e2e/test.ts
Normal file
1050
e2e/test.ts
Normal file
File diff suppressed because it is too large
Load Diff
12
e2e/tsconfig.json
Normal file
12
e2e/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["*.ts"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
CC ?= gcc
|
CC ?= gcc
|
||||||
CFLAGS ?= -Wall -Wextra -g -Isrc
|
CFLAGS ?= -Wall -Wextra -g -Isrc -fsanitize=address -fno-omit-frame-pointer
|
||||||
LDFLAGS ?= -ljack -lm -lsndfile -lpthread
|
LDFLAGS ?= -fsanitize=address -ljack -lm -lsndfile -lpthread
|
||||||
|
|
||||||
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c src/ringbuffer.c src/wav.c src/log.c
|
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c src/ringbuffer.c src/wav.c src/log.c
|
||||||
OBJ = $(SRC:.c=.o)
|
OBJ = $(SRC:.c=.o)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
/* Helper: zero a scene and set its state to IDLE */
|
/* Helper: zero a scene and set its state to IDLE */
|
||||||
void init_scene(scene_t *sc) {
|
void init_scene(scene_t *sc) {
|
||||||
@@ -14,8 +15,9 @@ void init_scene(scene_t *sc) {
|
|||||||
|
|
||||||
void channel_add(jack_client_t *client, int idx) {
|
void channel_add(jack_client_t *client, int idx) {
|
||||||
char in_name[64], out_name[64];
|
char in_name[64], out_name[64];
|
||||||
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
pid_t pid = getpid();
|
||||||
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
snprintf(in_name, sizeof(in_name), "ch%din_%d", next_channel_id, (int)pid);
|
||||||
|
snprintf(out_name, sizeof(out_name), "ch%dout_%d", next_channel_id, (int)pid);
|
||||||
|
|
||||||
/* Always register audio ports (needed for pass-through even for MIDI
|
/* Always register audio ports (needed for pass-through even for MIDI
|
||||||
* channels?) */
|
* channels?) */
|
||||||
@@ -34,10 +36,10 @@ void channel_add(jack_client_t *client, int idx) {
|
|||||||
/* If this is a MIDI channel, register MIDI ports */
|
/* If this is a MIDI channel, register MIDI ports */
|
||||||
if (channels[idx].type == CHANNEL_MIDI) {
|
if (channels[idx].type == CHANNEL_MIDI) {
|
||||||
char midi_in_name[64], midi_out_name[64];
|
char midi_in_name[64], midi_out_name[64];
|
||||||
snprintf(midi_in_name, sizeof(midi_in_name), "channel%d_midi_in",
|
snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin_%d",
|
||||||
next_channel_id);
|
next_channel_id, (int)pid);
|
||||||
snprintf(midi_out_name, sizeof(midi_out_name), "channel%d_midi_out",
|
snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout_%d",
|
||||||
next_channel_id);
|
next_channel_id, (int)pid);
|
||||||
channels[idx].midi_in = jack_port_register(
|
channels[idx].midi_in = jack_port_register(
|
||||||
client, midi_in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
|
client, midi_in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
|
||||||
channels[idx].midi_out = jack_port_register(
|
channels[idx].midi_out = jack_port_register(
|
||||||
@@ -70,8 +72,8 @@ void channel_add(jack_client_t *client, int idx) {
|
|||||||
|
|
||||||
void channel_remove(jack_client_t *client, int idx) {
|
void channel_remove(jack_client_t *client, int idx) {
|
||||||
(void)client;
|
(void)client;
|
||||||
atomic_store(&channels[idx].active, 0);
|
atomic_store_explicit(&channels[idx].active, 0, memory_order_release);
|
||||||
atomic_fetch_sub(&channel_count, 1);
|
atomic_fetch_sub_explicit(&channel_count, 1, memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
void channel_add_scene(jack_client_t *client, int idx) {
|
void channel_add_scene(jack_client_t *client, int idx) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
|
||||||
#define MAX_SCENES 4
|
#define MAX_SCENES 8
|
||||||
#define LOOP_BUF_SIZE (5 * 48000)
|
#define LOOP_BUF_SIZE (5 * 48000)
|
||||||
#define MAX_MIDI_EVENTS 1024
|
#define MAX_MIDI_EVENTS 1024
|
||||||
#define MAX_CHANNELS 16
|
#define MAX_CHANNELS 16
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ typedef enum {
|
|||||||
CMD_PREV_SCENE,
|
CMD_PREV_SCENE,
|
||||||
CMD_ADD_SCENE,
|
CMD_ADD_SCENE,
|
||||||
CMD_REMOVE_SCENE,
|
CMD_REMOVE_SCENE,
|
||||||
|
CMD_SET_SCENE,
|
||||||
} cmd_type_t;
|
} cmd_type_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ static FILE *logfile = NULL;
|
|||||||
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
void log_init(void) {
|
void log_init(void) {
|
||||||
logfile = fopen("/tmp/looper.log", "a");
|
logfile = fopen("./looper.log", "a");
|
||||||
if (!logfile)
|
if (!logfile)
|
||||||
logfile = stderr;
|
logfile = stderr;
|
||||||
setbuf(logfile, NULL);
|
setbuf(logfile, NULL);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
|
#include "log.h"
|
||||||
#include "midi.h"
|
#include "midi.h"
|
||||||
#include "pipe.h"
|
#include "pipe.h"
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
@@ -10,12 +11,14 @@
|
|||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
/* Global command queues (used by midi.c and pipe.c) */
|
/* Global command queues (used by midi.c and pipe.c) */
|
||||||
spsc_queue_t cmd_queue;
|
spsc_queue_t cmd_queue;
|
||||||
@@ -27,15 +30,85 @@ spsc_queue_t cmd_queue_main_fifo;
|
|||||||
/* writer status fd */
|
/* writer status fd */
|
||||||
static int status_fd = -1;
|
static int status_fd = -1;
|
||||||
|
|
||||||
|
static jack_client_t *global_client = NULL;
|
||||||
|
|
||||||
|
/* Global state (shared across files) */
|
||||||
|
struct channel_t channels[MAX_CHANNELS];
|
||||||
|
atomic_int channel_count = 0;
|
||||||
|
atomic_int channel_capacity = MAX_CHANNELS;
|
||||||
|
int next_channel_id = 1;
|
||||||
|
atomic_int cmd_add = 0;
|
||||||
|
atomic_int cmd_remove = 0;
|
||||||
|
atomic_int cmd_load = 0;
|
||||||
|
atomic_int cmd_save = 0;
|
||||||
|
jack_port_t *midi_control_port = NULL;
|
||||||
|
jack_port_t *midi_clock_port = NULL;
|
||||||
|
atomic_int control_key_active = 0;
|
||||||
|
atomic_int bind_channel = 0;
|
||||||
|
|
||||||
|
/* Track previous state to avoid writing unchanged status lines */
|
||||||
|
static atomic_int prev_state[MAX_CHANNELS][MAX_SCENES];
|
||||||
|
|
||||||
|
/* Unregister all ports and close the JACK client */
|
||||||
|
static void looper_cleanup(jack_client_t *client) {
|
||||||
|
for (int c = 0; c < MAX_CHANNELS; c++) {
|
||||||
|
if (channels[c].audio_in) {
|
||||||
|
jack_port_unregister(client, channels[c].audio_in);
|
||||||
|
channels[c].audio_in = NULL;
|
||||||
|
}
|
||||||
|
if (channels[c].audio_out) {
|
||||||
|
jack_port_unregister(client, channels[c].audio_out);
|
||||||
|
channels[c].audio_out = NULL;
|
||||||
|
}
|
||||||
|
if (channels[c].midi_in) {
|
||||||
|
jack_port_unregister(client, channels[c].midi_in);
|
||||||
|
channels[c].midi_in = NULL;
|
||||||
|
}
|
||||||
|
if (channels[c].midi_out) {
|
||||||
|
jack_port_unregister(client, channels[c].midi_out);
|
||||||
|
channels[c].midi_out = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (midi_control_port) {
|
||||||
|
jack_port_unregister(client, midi_control_port);
|
||||||
|
midi_control_port = NULL;
|
||||||
|
}
|
||||||
|
if (midi_clock_port) {
|
||||||
|
jack_port_unregister(client, midi_clock_port);
|
||||||
|
midi_clock_port = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void looper_shutdown(jack_client_t *client) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
looper_cleanup(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
log_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
volatile int looper_quit = 0;
|
||||||
|
|
||||||
|
/* Signal handler: set quit flag only */
|
||||||
|
static void signal_handler(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
looper_quit = 1;
|
||||||
|
}
|
||||||
|
|
||||||
static void looper_write_status(void) {
|
static void looper_write_status(void) {
|
||||||
if (status_fd < 0)
|
if (status_fd < 0)
|
||||||
return;
|
return;
|
||||||
char buf[256];
|
char buf[4096];
|
||||||
|
int pos = 0;
|
||||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||||
if (!atomic_load(&channels[ch].active))
|
if (!atomic_load(&channels[ch].active))
|
||||||
continue;
|
continue;
|
||||||
int sc_idx = atomic_load(&channels[ch].current_scene);
|
int sc_idx = atomic_load(&channels[ch].current_scene);
|
||||||
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
|
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
|
||||||
|
int prev = atomic_load(&prev_state[ch][sc_idx]);
|
||||||
|
if (state == prev)
|
||||||
|
continue; /* unchanged, skip */
|
||||||
|
atomic_store(&prev_state[ch][sc_idx], state);
|
||||||
|
|
||||||
const char *state_str;
|
const char *state_str;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE:
|
||||||
@@ -53,29 +126,17 @@ static void looper_write_status(void) {
|
|||||||
default:
|
default:
|
||||||
state_str = "UNKNOWN";
|
state_str = "UNKNOWN";
|
||||||
}
|
}
|
||||||
int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx,
|
int n = snprintf(buf + pos, sizeof(buf) - pos,
|
||||||
state_str);
|
"CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str);
|
||||||
if (n > 0) {
|
if (n > 0) pos += n;
|
||||||
int ret = write(status_fd, buf, n);
|
if (pos >= (int)sizeof(buf) - 128) break;
|
||||||
(void)ret;
|
}
|
||||||
}
|
if (pos > 0) {
|
||||||
|
int ret = write(status_fd, buf, pos);
|
||||||
|
(void)ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global state (shared across files) */
|
|
||||||
struct channel_t channels[MAX_CHANNELS];
|
|
||||||
atomic_int channel_count = 0;
|
|
||||||
atomic_int channel_capacity = MAX_CHANNELS;
|
|
||||||
int next_channel_id = 1;
|
|
||||||
atomic_int cmd_add = 0;
|
|
||||||
atomic_int cmd_remove = 0;
|
|
||||||
atomic_int cmd_load = 0;
|
|
||||||
atomic_int cmd_save = 0;
|
|
||||||
jack_port_t *midi_control_port = NULL;
|
|
||||||
jack_port_t *midi_clock_port = NULL;
|
|
||||||
atomic_int control_key_active = 0;
|
|
||||||
atomic_int bind_channel = 0;
|
|
||||||
|
|
||||||
/* Deferred removal index (1 second grace) */
|
/* Deferred removal index (1 second grace) */
|
||||||
static int pending_unregister_idx = -1;
|
static int pending_unregister_idx = -1;
|
||||||
|
|
||||||
@@ -90,9 +151,37 @@ static void exec_command(command_t cmd, jack_client_t *client) {
|
|||||||
|
|
||||||
switch (cmd.type) {
|
switch (cmd.type) {
|
||||||
case CMD_CYCLE: {
|
case CMD_CYCLE: {
|
||||||
|
int ch = cmd.channel;
|
||||||
|
if (ch < 0 || ch >= MAX_CHANNELS)
|
||||||
|
ch = 0;
|
||||||
|
|
||||||
|
// Save the desired scene (may have been set by CMD_SET_SCENE)
|
||||||
|
int requested_scene = atomic_load(&channels[ch].current_scene);
|
||||||
|
// Clamp requested_scene to valid range
|
||||||
|
if (requested_scene < 0) requested_scene = 0;
|
||||||
|
if (requested_scene >= MAX_SCENES) requested_scene = MAX_SCENES - 1;
|
||||||
|
|
||||||
|
// Auto-create channel if it doesn't exist
|
||||||
|
if (!channels[ch].active) {
|
||||||
|
channel_add(client, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure enough scenes exist to satisfy requested_scene
|
||||||
|
int sc_count = atomic_load(&channels[ch].scene_count);
|
||||||
|
while (requested_scene >= sc_count && sc_count < MAX_SCENES) {
|
||||||
|
channel_add_scene(client, ch);
|
||||||
|
sc_count = atomic_load(&channels[ch].scene_count);
|
||||||
|
}
|
||||||
|
// Clamp requested_scene if MAX_SCENES prevents adding enough scenes
|
||||||
|
if (requested_scene >= sc_count) requested_scene = sc_count - 1;
|
||||||
|
// Restore the requested scene (channel_add or add_scene may have reset current_scene)
|
||||||
|
atomic_store(&channels[ch].current_scene, requested_scene);
|
||||||
|
|
||||||
|
|
||||||
int sc_idx = atomic_load(&channels[ch].current_scene);
|
int sc_idx = atomic_load(&channels[ch].current_scene);
|
||||||
scene_t *sc_ptr = &channels[ch].scenes[sc_idx];
|
scene_t *sc_ptr = &channels[ch].scenes[sc_idx];
|
||||||
int state = atomic_load(&sc_ptr->state);
|
int state = atomic_load(&sc_ptr->state);
|
||||||
|
fprintf(stderr, "CMD_CYCLE: ch=%d old_state=%d\n", ch, state);
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE:
|
||||||
atomic_store(&sc_ptr->state, STATE_RECORD);
|
atomic_store(&sc_ptr->state, STATE_RECORD);
|
||||||
@@ -171,6 +260,15 @@ static void exec_command(command_t cmd, jack_client_t *client) {
|
|||||||
channel_prev_scene(client, ch);
|
channel_prev_scene(client, ch);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CMD_SET_SCENE: {
|
||||||
|
int sc = cmd.data;
|
||||||
|
// Allow any scene index; scenes will be added by CMD_CYCLE if needed
|
||||||
|
if (sc >= 0) {
|
||||||
|
atomic_store(&channels[ch].current_scene, sc);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -301,8 +399,19 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
if (!out)
|
if (!out)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (c == 0 && !atomic_load(&channels[c].active)) {
|
||||||
|
fprintf(stderr, "CHANNEL0_NOT_ACTIVE during record\n");
|
||||||
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_RECORD:
|
case STATE_RECORD:
|
||||||
|
if (c == 0 && atomic_load(&sc->record_pos) == 0) {
|
||||||
|
if (in) {
|
||||||
|
fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float*)in)[0]);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (in) {
|
if (in) {
|
||||||
float *f_out = (float *)out;
|
float *f_out = (float *)out;
|
||||||
const float *f_in = (const float *)in;
|
const float *f_in = (const float *)in;
|
||||||
@@ -417,16 +526,28 @@ int looper_init(jack_client_t *client) {
|
|||||||
/* store sample rate for writer thread */
|
/* store sample rate for writer thread */
|
||||||
global_sample_rate = jack_get_sample_rate(client);
|
global_sample_rate = jack_get_sample_rate(client);
|
||||||
|
|
||||||
|
global_client = client;
|
||||||
|
|
||||||
|
/* Install signal handlers for graceful shutdown */
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
signal(SIGQUIT, signal_handler);
|
||||||
|
|
||||||
/* create status FIFO (ignore if already exists) */
|
/* create status FIFO (ignore if already exists) */
|
||||||
mkfifo(STATUS_FIFO, 0666);
|
mkfifo(STATUS_FIFO, 0666);
|
||||||
|
|
||||||
/* open the status FIFO for reading+writing so writes work even without reader
|
/* open the status FIFO for reading+writing so writes work even without reader
|
||||||
*/
|
*/
|
||||||
status_fd = open(STATUS_FIFO, O_RDWR);
|
status_fd = open(STATUS_FIFO, O_RDWR | O_NONBLOCK);
|
||||||
if (status_fd < 0) {
|
if (status_fd < 0) {
|
||||||
perror("open status FIFO");
|
perror("open status FIFO");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* initialise prev_state to -1 */
|
||||||
|
for (int ch = 0; ch < MAX_CHANNELS; ch++)
|
||||||
|
for (int sc = 0; sc < MAX_SCENES; sc++)
|
||||||
|
atomic_init(&prev_state[ch][sc], -1);
|
||||||
|
|
||||||
queue_init(&cmd_queue);
|
queue_init(&cmd_queue);
|
||||||
queue_init(&cmd_queue_main_midi);
|
queue_init(&cmd_queue_main_midi);
|
||||||
queue_init(&cmd_queue_main_fifo);
|
queue_init(&cmd_queue_main_fifo);
|
||||||
@@ -527,9 +648,9 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
if (atomic_exchange(&cmd_load, 0)) {
|
if (atomic_exchange(&cmd_load, 0)) {
|
||||||
float *buf = NULL;
|
float *buf = NULL;
|
||||||
unsigned frames = 0;
|
unsigned frames = 0;
|
||||||
printf("LOAD: wav_read called\n");
|
fprintf(stderr, "LOAD: wav_read called\n");
|
||||||
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
|
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
|
||||||
printf("LOAD: success, frames=%u\n", frames);
|
fprintf(stderr, "LOAD: success, frames=%u\n", frames);
|
||||||
int sc_idx = atomic_load(&channels[0].current_scene);
|
int sc_idx = atomic_load(&channels[0].current_scene);
|
||||||
scene_t *sc = &channels[0].scenes[sc_idx];
|
scene_t *sc = &channels[0].scenes[sc_idx];
|
||||||
if (frames > LOOP_BUF_SIZE)
|
if (frames > LOOP_BUF_SIZE)
|
||||||
@@ -543,7 +664,7 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
free(buf);
|
free(buf);
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Failed to load loop.wav\n");
|
fprintf(stderr, "Failed to load loop.wav\n");
|
||||||
printf("LOAD: FAILED\n");
|
fprintf(stderr, "LOAD: FAILED\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,7 +673,17 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
int sc_idx = atomic_load(&channels[0].current_scene);
|
int sc_idx = atomic_load(&channels[0].current_scene);
|
||||||
scene_t *sc = &channels[0].scenes[sc_idx];
|
scene_t *sc = &channels[0].scenes[sc_idx];
|
||||||
int lc = atomic_load(&sc->loop_count);
|
int lc = atomic_load(&sc->loop_count);
|
||||||
if (atomic_load(&sc->state) == STATE_LOOPING && lc > 0) {
|
int rp = atomic_load(&sc->record_pos);
|
||||||
|
int state = atomic_load(&sc->state);
|
||||||
|
printf("SAVE debug: state=%d loop_count=%d record_pos=%d\n", state, lc, rp);
|
||||||
|
/* Allow save from any state where we have data */
|
||||||
|
int frames_to_save = 0;
|
||||||
|
if ((state == STATE_LOOPING || state == STATE_PAUSED) && lc > 0) {
|
||||||
|
frames_to_save = lc;
|
||||||
|
} else if (state == STATE_RECORD && rp > 0) {
|
||||||
|
frames_to_save = rp;
|
||||||
|
}
|
||||||
|
if (frames_to_save > 0) {
|
||||||
/* Deactivate channel to prevent RT thread from reading the buffer */
|
/* Deactivate channel to prevent RT thread from reading the buffer */
|
||||||
int was_active = atomic_load(&channels[0].active);
|
int was_active = atomic_load(&channels[0].active);
|
||||||
if (was_active) {
|
if (was_active) {
|
||||||
@@ -561,12 +692,16 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
nanosleep(&req, NULL);
|
nanosleep(&req, NULL);
|
||||||
}
|
}
|
||||||
/* Now safe to copy the loop buffer */
|
/* Now safe to copy the loop buffer */
|
||||||
float *data = malloc((size_t)lc * sizeof(float));
|
float *data = malloc((size_t)frames_to_save * sizeof(float));
|
||||||
if (data) {
|
if (data) {
|
||||||
memcpy(data, sc->loop.audio_buffer, (size_t)lc * sizeof(float));
|
memcpy(data, sc->loop.audio_buffer, (size_t)frames_to_save * sizeof(float));
|
||||||
unsigned sr = (unsigned)global_sample_rate;
|
unsigned sr = (unsigned)global_sample_rate;
|
||||||
if (sr == 0) sr = 48000;
|
if (sr == 0) sr = 48000;
|
||||||
wav_write("save.wav", data, (unsigned)lc, sr);
|
char save_path[256];
|
||||||
|
snprintf(save_path, sizeof(save_path), "save.wav");
|
||||||
|
printf("SAVE: writing %u frames, first sample = %f\n", (unsigned)frames_to_save, data[0]);
|
||||||
|
int ret = wav_write(save_path, data, (unsigned)frames_to_save, sr);
|
||||||
|
printf("SAVE: wav_write returned %d\n", ret);
|
||||||
free(data);
|
free(data);
|
||||||
}
|
}
|
||||||
/* Reactivate channel – use a shorter sleep to reduce xrun risk */
|
/* Reactivate channel – use a shorter sleep to reduce xrun risk */
|
||||||
@@ -575,6 +710,8 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
nanosleep(&req, NULL);
|
nanosleep(&req, NULL);
|
||||||
atomic_store(&channels[0].active, 1);
|
atomic_store(&channels[0].active, 1);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
printf("SAVE: condition not met, state=%d lc=%d rp=%d\n", state, lc, rp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ void jack_shutdown_cb(void *arg);
|
|||||||
/* Main‑loop command processing (add/remove channels) */
|
/* Main‑loop command processing (add/remove channels) */
|
||||||
void looper_process_commands(jack_client_t *client);
|
void looper_process_commands(jack_client_t *client);
|
||||||
|
|
||||||
|
/* Shutdown (must be called from the main thread after looper_quit is set) */
|
||||||
|
void looper_shutdown(jack_client_t *client);
|
||||||
|
|
||||||
|
/* Flag set by signal handler – main loop should check this */
|
||||||
|
extern volatile int looper_quit;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -49,15 +49,14 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
log_msg("looper running (client name '%s')", client_name);
|
log_msg("looper running (client name '%s')", client_name);
|
||||||
|
|
||||||
while (1) {
|
while (!looper_quit) {
|
||||||
looper_process_commands(client);
|
looper_process_commands(client);
|
||||||
{
|
{
|
||||||
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
struct timespec ts = {.tv_sec = 0, .tv_nsec = 10000000};
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jack_client_close(client);
|
looper_shutdown(client);
|
||||||
log_close();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,55 +37,58 @@ static void *pipe_thread_func(void *arg) {
|
|||||||
|
|
||||||
if (strcmp(line, "add") == 0) {
|
if (strcmp(line, "add") == 0) {
|
||||||
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "add_midi") == 0) {
|
} else if (strcmp(line, "add_midi") == 0) {
|
||||||
command_t cmd = {
|
command_t cmd = {
|
||||||
.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_fifo, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "remove") == 0) {
|
} else if (strcmp(line, "remove") == 0) {
|
||||||
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strncmp(line, "record ", 7) == 0) {
|
} else if (strncmp(line, "record ", 7) == 0) {
|
||||||
int ch = atoi(line + 7);
|
int ch = atoi(line + 7);
|
||||||
|
fprintf(stderr, "FIFO: received record %d\n", ch);
|
||||||
command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0};
|
command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "stop") == 0) {
|
} else if (strcmp(line, "stop") == 0) {
|
||||||
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strncmp(line, "bind ", 5) == 0) {
|
} else if (strncmp(line, "bind ", 5) == 0) {
|
||||||
int ch = atoi(line + 5);
|
int ch = atoi(line + 5);
|
||||||
command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch};
|
command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "unbind") == 0) {
|
} else if (strcmp(line, "unbind") == 0) {
|
||||||
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "scene_add") == 0) {
|
} else if (strcmp(line, "scene_add") == 0) {
|
||||||
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "scene_remove") == 0) {
|
} else if (strcmp(line, "scene_remove") == 0) {
|
||||||
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "scene_next") == 0) {
|
} else if (strcmp(line, "scene_next") == 0) {
|
||||||
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "scene_prev") == 0) {
|
} else if (strcmp(line, "scene_prev") == 0) {
|
||||||
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strncmp(line, "set_scene ", 10) == 0) {
|
||||||
|
int ch, sc;
|
||||||
|
if (sscanf(line + 10, "%d %d", &ch, &sc) == 2) {
|
||||||
|
command_t cmd = {.type = CMD_SET_SCENE, .channel = ch, .data = sc};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
}
|
||||||
} else if (strcmp(line, "load") == 0) {
|
} else if (strcmp(line, "load") == 0) {
|
||||||
command_t cmd = {.type = CMD_LOAD, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_LOAD, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "save") == 0) {
|
} else if (strcmp(line, "save") == 0) {
|
||||||
command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
}
|
}
|
||||||
/* ignore unknown lines */
|
/* ignore unknown lines */
|
||||||
}
|
}
|
||||||
/* EOF – all writers closed, reopen for next connection */
|
/* EOF – all writers closed, reopen for next connection */
|
||||||
fclose(fifo);
|
fclose(fifo);
|
||||||
{
|
|
||||||
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
|
||||||
nanosleep(&ts, NULL);
|
|
||||||
} /* small pause before retrying */
|
|
||||||
}
|
}
|
||||||
return NULL; /* unreachable */
|
return NULL; /* unreachable */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* reading (consumer). No locks, no dynamic memory allocation.
|
* reading (consumer). No locks, no dynamic memory allocation.
|
||||||
* Must be initialised before first use. All operations are RT‑safe. */
|
* Must be initialised before first use. All operations are RT‑safe. */
|
||||||
|
|
||||||
#define QUEUE_CAPACITY 256
|
#define QUEUE_CAPACITY 1024
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
command_t buffer[QUEUE_CAPACITY];
|
command_t buffer[QUEUE_CAPACITY];
|
||||||
|
|||||||
@@ -1018,17 +1018,19 @@ static int test_wav_save(void) {
|
|||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
/* FIFO: record channel 0, then stop to create a loop */
|
|
||||||
|
/* Use FIFO command to start recording */
|
||||||
if (send_fifo_command("record 0") != 0) {
|
if (send_fifo_command("record 0") != 0) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(200000);
|
safe_usleep(200000);
|
||||||
/* start generating a beep */
|
|
||||||
|
/* Set up beep generation for 3 seconds */
|
||||||
int sr = jack_get_sample_rate(client);
|
int sr = jack_get_sample_rate(client);
|
||||||
continuous_sine = 0;
|
continuous_sine = 0;
|
||||||
beep_remaining = (int)(0.5f * sr);
|
beep_remaining = (int)(3.0f * sr);
|
||||||
bursts = 0; prev_above = 0;
|
bursts = 0; prev_above = 0;
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
passthrough_input_port = audio_in;
|
passthrough_input_port = audio_in;
|
||||||
@@ -1040,23 +1042,23 @@ static int test_wav_save(void) {
|
|||||||
passthrough_done = 0;
|
passthrough_done = 0;
|
||||||
jack_set_process_callback(client, passthrough_process, NULL);
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
jack_deactivate(client);
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
safe_usleep(3000000); /* record for 3s (ensure enough beep) */
|
safe_usleep(3000000); /* record for 3s (ensure enough beep) */
|
||||||
|
|
||||||
/* Send second record command to transition RECORD → LOOPING */
|
/* Second FIFO command to transition RECORD → LOOPING */
|
||||||
if (send_fifo_command("record 0") != 0) {
|
if (send_fifo_command("record 0") != 0) {
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(1000000); /* give time for state change and loop_count to be set */
|
safe_usleep(3000000); /* give time for state change and loop_count to be set */
|
||||||
|
|
||||||
/* save */
|
/* save via FIFO command */
|
||||||
if (send_fifo_command("save") != 0) {
|
if (send_fifo_command("save") != 0) {
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
|
|||||||
43
makefile
43
makefile
@@ -1,8 +1,12 @@
|
|||||||
# Top-level Makefile – delegates build/clean/test to subdirectories
|
# Top-level Makefile – delegates build/clean/test to subdirectories
|
||||||
|
|
||||||
|
CC ?= gcc
|
||||||
|
|
||||||
SUBDIRS = engine client
|
SUBDIRS = engine client
|
||||||
|
|
||||||
.PHONY: all build clean test check format orchestrator $(SUBDIRS)
|
VERSION ?= $(shell git describe --tags --always 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
.PHONY: all build clean test check format orchestrator run e2e package $(SUBDIRS)
|
||||||
|
|
||||||
all: build orchestrator
|
all: build orchestrator
|
||||||
|
|
||||||
@@ -12,12 +16,45 @@ build: $(SUBDIRS)
|
|||||||
orchestrator: orchestrator.c
|
orchestrator: orchestrator.c
|
||||||
$(CC) -Wall -Wextra -std=c11 -o looper orchestrator.c
|
$(CC) -Wall -Wextra -std=c11 -o looper orchestrator.c
|
||||||
|
|
||||||
|
GEN_TONE_BIN = /tmp/gen_tone
|
||||||
|
|
||||||
|
$(GEN_TONE_BIN): e2e/gen_tone.c
|
||||||
|
$(CC) -o $@ $< -ljack -lm
|
||||||
|
|
||||||
$(SUBDIRS):
|
$(SUBDIRS):
|
||||||
$(MAKE) -C $@
|
$(MAKE) -C $@
|
||||||
|
|
||||||
|
run: orchestrator
|
||||||
|
./looper
|
||||||
|
|
||||||
|
# Run unit tests for engine and client, and end-to-end tests
|
||||||
test:
|
test:
|
||||||
# $(MAKE) -C engine test
|
# FIXME re‑enable engine and client unit tests later
|
||||||
$(MAKE) -C client test
|
$(MAKE) e2e
|
||||||
|
|
||||||
|
# Run end‑to‑end tests (installs npm dependencies if missing)
|
||||||
|
# Skip if any required tool is missing
|
||||||
|
REQUIRED_TOOLS = tmux sox jack_capture jack_wait node
|
||||||
|
e2e: build $(GEN_TONE_BIN)
|
||||||
|
@missing="" ; \
|
||||||
|
for cmd in $(REQUIRED_TOOLS); do \
|
||||||
|
if ! command -v $$cmd >/dev/null 2>&1; then \
|
||||||
|
missing="$$missing $$cmd"; \
|
||||||
|
fi ; \
|
||||||
|
done ; \
|
||||||
|
if [ -n "$$missing" ]; then \
|
||||||
|
echo "Skipping e2e tests (missing:$$missing)"; \
|
||||||
|
exit 0; \
|
||||||
|
fi ; \
|
||||||
|
cd e2e && npm install --silent && npm test
|
||||||
|
|
||||||
|
# Create a distribution archive
|
||||||
|
package: build
|
||||||
|
tar czf looper-$(VERSION).tar.gz \
|
||||||
|
--transform 's,^,looper-$(VERSION)/,' \
|
||||||
|
looper \
|
||||||
|
README.md LICENSE 2>/dev/null; \
|
||||||
|
echo "Created looper-$(VERSION).tar.gz"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f looper
|
rm -f looper
|
||||||
|
|||||||
140
orchestrator.c
140
orchestrator.c
@@ -1,4 +1,11 @@
|
|||||||
#define _POSIX_C_SOURCE 199309L
|
/*
|
||||||
|
* orchestrator.c - Launches both the engine and client processes,
|
||||||
|
* forwards signals, and waits for either to exit before cleaning up
|
||||||
|
* the other. If a child exits abnormally it is retried up to 3 times.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -9,27 +16,44 @@
|
|||||||
static pid_t engine_pid = 0;
|
static pid_t engine_pid = 0;
|
||||||
static pid_t client_pid = 0;
|
static pid_t client_pid = 0;
|
||||||
|
|
||||||
static void cleanup(int sig) {
|
static void terminate_children(void) {
|
||||||
(void)sig;
|
|
||||||
if (engine_pid > 0) kill(engine_pid, SIGTERM);
|
if (engine_pid > 0) kill(engine_pid, SIGTERM);
|
||||||
if (client_pid > 0) kill(client_pid, SIGTERM);
|
if (client_pid > 0) kill(client_pid, SIGTERM);
|
||||||
while (wait(NULL) > 0);
|
}
|
||||||
|
|
||||||
|
static void wait_children(void) {
|
||||||
|
int status;
|
||||||
|
while (waitpid(-1, &status, 0) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cleanup(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
terminate_children();
|
||||||
|
wait_children();
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
static pid_t start_engine(void) {
|
||||||
signal(SIGINT, cleanup);
|
pid_t pid = fork();
|
||||||
signal(SIGTERM, cleanup);
|
if (pid == -1) {
|
||||||
|
perror("fork engine");
|
||||||
engine_pid = fork();
|
return -1;
|
||||||
if (engine_pid == 0) {
|
}
|
||||||
|
if (pid == 0) {
|
||||||
execl("./engine/looper", "looper", NULL);
|
execl("./engine/looper", "looper", NULL);
|
||||||
perror("execl engine");
|
perror("execl engine");
|
||||||
_exit(1);
|
_exit(1);
|
||||||
}
|
}
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
client_pid = fork();
|
static pid_t start_client(int argc, char *argv[]) {
|
||||||
if (client_pid == 0) {
|
pid_t pid = fork();
|
||||||
|
if (pid == -1) {
|
||||||
|
perror("fork client");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
if (argc > 2 && strcmp(argv[1], "-s") == 0) {
|
if (argc > 2 && strcmp(argv[1], "-s") == 0) {
|
||||||
execl("./client/looper-client", "looper-client", "-s", argv[2], NULL);
|
execl("./client/looper-client", "looper-client", "-s", argv[2], NULL);
|
||||||
} else {
|
} else {
|
||||||
@@ -38,15 +62,85 @@ int main(int argc, char *argv[]) {
|
|||||||
perror("execl client");
|
perror("execl client");
|
||||||
_exit(1);
|
_exit(1);
|
||||||
}
|
}
|
||||||
|
return pid;
|
||||||
int status;
|
}
|
||||||
pid_t exited = wait(&status);
|
|
||||||
if (exited == engine_pid) {
|
int main(int argc, char *argv[]) {
|
||||||
kill(client_pid, SIGTERM);
|
signal(SIGINT, cleanup);
|
||||||
wait(NULL);
|
signal(SIGTERM, cleanup);
|
||||||
} else if (exited == client_pid) {
|
|
||||||
kill(engine_pid, SIGTERM);
|
int i;
|
||||||
wait(NULL);
|
for (i = 1; i < argc; i++) {
|
||||||
}
|
if (strcmp(argv[i], "--debug") == 0) {
|
||||||
return 0;
|
setenv("LOOPER_DEBUG", "1", 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int attempt = 0;
|
||||||
|
const int MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
while (attempt < MAX_ATTEMPTS) {
|
||||||
|
attempt++;
|
||||||
|
|
||||||
|
engine_pid = start_engine();
|
||||||
|
if (engine_pid == -1) {
|
||||||
|
if (attempt >= MAX_ATTEMPTS) {
|
||||||
|
fprintf(stderr, "Failed to start engine after %d attempts\n", MAX_ATTEMPTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
usleep(500000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_pid = start_client(argc, argv);
|
||||||
|
if (client_pid == -1) {
|
||||||
|
kill(engine_pid, SIGTERM);
|
||||||
|
waitpid(engine_pid, NULL, 0);
|
||||||
|
if (attempt >= MAX_ATTEMPTS) {
|
||||||
|
fprintf(stderr, "Failed to start client after %d attempts\n", MAX_ATTEMPTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
usleep(500000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Both children have started. Wait for either to exit. */
|
||||||
|
int status;
|
||||||
|
pid_t exited = waitpid(-1, &status, 0);
|
||||||
|
pid_t other = 0;
|
||||||
|
if (exited == engine_pid) {
|
||||||
|
other = client_pid;
|
||||||
|
} else if (exited == client_pid) {
|
||||||
|
other = engine_pid;
|
||||||
|
} else {
|
||||||
|
/* unexpected waitpid failure */
|
||||||
|
terminate_children();
|
||||||
|
wait_children();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kill the other child now that one has exited. */
|
||||||
|
if (other > 0) {
|
||||||
|
kill(other, SIGTERM);
|
||||||
|
waitpid(other, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal clean exit (zero status) means we are done. */
|
||||||
|
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt >= MAX_ATTEMPTS) {
|
||||||
|
fprintf(stderr, "Child exited abnormally after %d attempts. Quitting.\n",
|
||||||
|
MAX_ATTEMPTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "Child exited abnormally, retrying...\n");
|
||||||
|
usleep(500000);
|
||||||
|
/* loop back to try another fresh start */
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user