feat: add address sanitizer, persistent FIFO fds, and latency test
This commit is contained in:
committed by
Loic Coenen (aider)
parent
0537263a7a
commit
dd67576c45
@@ -1,5 +1,5 @@
|
|||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -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,10 +20,10 @@ 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)
|
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) $(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
|
||||||
|
|
||||||
# --- Plugin stubs (now real) ---
|
# --- Plugin stubs (now real) ---
|
||||||
$(PLUGINS_OBJ): src/plugins.c src/plugins.h
|
$(PLUGINS_OBJ): src/plugins.c src/plugins.h
|
||||||
@@ -84,7 +84,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 ---
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#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"
|
||||||
|
|
||||||
@@ -137,12 +138,17 @@ 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) {
|
||||||
|
fprintf(stderr, "WARN: connection array full, refusing new connection\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
connections[conn_count].plugin_id = id;
|
connections[conn_count].plugin_id = id;
|
||||||
strncpy(connections[conn_count].plugin_port, port_name,
|
strncpy(connections[conn_count].plugin_port, port_name,
|
||||||
sizeof(connections[conn_count].plugin_port) - 1);
|
sizeof(connections[conn_count].plugin_port) - 1);
|
||||||
@@ -151,7 +157,6 @@ int carla_connect(int id, const char *port_name, const char *looper_port) {
|
|||||||
sizeof(connections[conn_count].looper_port) - 1);
|
sizeof(connections[conn_count].looper_port) - 1);
|
||||||
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
|
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
|
||||||
conn_count++;
|
conn_count++;
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,34 +24,29 @@
|
|||||||
static bool engine_running = false;
|
static bool engine_running = false;
|
||||||
static bool debug_mode = 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) {
|
||||||
if (debug_mode)
|
if (debug_mode)
|
||||||
fprintf(stderr, "DEBUG: send_command(%s)\n", cmd);
|
fprintf(stderr, "DEBUG: send_command(%s)\n", cmd);
|
||||||
|
|
||||||
|
if (cmd_fifo_fd < 0) {
|
||||||
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
||||||
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
||||||
|
cmd_fifo_fd = open(fifo_path, O_WRONLY);
|
||||||
// Retry open up to 5 times with a short sleep, blocking mode
|
if (cmd_fifo_fd < 0) {
|
||||||
int fd = -1;
|
perror("open cmd FIFO");
|
||||||
for (int attempt = 0; attempt < 5 && fd < 0; attempt++) {
|
return -1;
|
||||||
fd = open(fifo_path, O_WRONLY); // blocking – waits for reader
|
|
||||||
if (fd < 0) {
|
|
||||||
if (errno == ENXIO && attempt < 4)
|
|
||||||
{
|
|
||||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 };
|
|
||||||
nanosleep(&ts, NULL);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fd < 0) 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +249,12 @@ static bool in_colon = false;
|
|||||||
|
|
||||||
/* Read the status FIFO once and update cell_state array */
|
/* Read the status FIFO once and update cell_state array */
|
||||||
static void tui_read_status(void) {
|
static void tui_read_status(void) {
|
||||||
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
if (status_fifo_fd < 0) {
|
||||||
if (fd < 0) return;
|
status_fifo_fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
char buf[256];
|
if (status_fifo_fd < 0) return;
|
||||||
int n = read(fd, buf, sizeof(buf)-1);
|
}
|
||||||
|
char buf[4096];
|
||||||
|
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;
|
||||||
@@ -274,7 +271,7 @@ static void tui_read_status(void) {
|
|||||||
if (nl) { *nl = '\n'; line = nl + 1; } else break;
|
if (nl) { *nl = '\n'; line = nl + 1; } else break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(fd);
|
/* keep fd open */
|
||||||
}
|
}
|
||||||
|
|
||||||
void tui_run(void) {
|
void tui_run(void) {
|
||||||
@@ -310,7 +307,14 @@ void tui_run(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* redraw grid (status may have changed – no extra key needed) */
|
/* redraw grid (status may have changed – no extra key needed) */
|
||||||
|
{
|
||||||
|
struct timespec t1, t2;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||||
draw_grid();
|
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();
|
int chc = getch();
|
||||||
|
|
||||||
@@ -470,6 +474,14 @@ void tui_run(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
76
e2e/test.ts
76
e2e/test.ts
@@ -71,8 +71,8 @@ function writeFifoCommand(cmd: string): void {
|
|||||||
|
|
||||||
function setupTest() {
|
function setupTest() {
|
||||||
process.stdout.write(" Killing stale processes...\n");
|
process.stdout.write(" Killing stale processes...\n");
|
||||||
runNoThrow("pkill -9 -x looper");
|
runNoThrow("pkill -15 -x looper");
|
||||||
runNoThrow("pkill -9 -x looper-client");
|
runNoThrow("pkill -15 -x looper-client");
|
||||||
runNoThrow("pkill -9 -x jack_capture");
|
runNoThrow("pkill -9 -x jack_capture");
|
||||||
runNoThrow("tmux kill-session -t looper 2>/dev/null || true");
|
runNoThrow("tmux kill-session -t looper 2>/dev/null || true");
|
||||||
process.stdout.write(" Checking JACK...\n");
|
process.stdout.write(" Checking JACK...\n");
|
||||||
@@ -90,8 +90,8 @@ function teardownTest() {
|
|||||||
fs.closeSync(cmdFifoFd);
|
fs.closeSync(cmdFifoFd);
|
||||||
cmdFifoFd = null;
|
cmdFifoFd = null;
|
||||||
}
|
}
|
||||||
runNoThrow("pkill -9 -x looper");
|
runNoThrow("pkill -15 -x looper");
|
||||||
runNoThrow("pkill -9 -x looper-client");
|
runNoThrow("pkill -15 -x looper-client");
|
||||||
runNoThrow("pkill -9 -x jack_capture");
|
runNoThrow("pkill -9 -x jack_capture");
|
||||||
runNoThrow("tmux kill-session -t looper");
|
runNoThrow("tmux kill-session -t looper");
|
||||||
}
|
}
|
||||||
@@ -911,13 +911,13 @@ 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, has selection line)
|
// Check TUI pane integrity (non‑empty, at least has header and a cell)
|
||||||
let pane = tmuxCapturePane("looper", "0");
|
let pane = tmuxCapturePane("looper", "0");
|
||||||
if (!pane || pane.trim() === "") {
|
if (!pane || pane.trim() === "") {
|
||||||
await wait(200);
|
await wait(200);
|
||||||
pane = tmuxCapturePane("looper", "0");
|
pane = tmuxCapturePane("looper", "0");
|
||||||
}
|
}
|
||||||
if (!pane || !pane.includes("Selected:")) {
|
if (!pane || !pane.includes("JACK Looper") || !pane.includes(" 0")) {
|
||||||
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();
|
||||||
@@ -940,6 +940,67 @@ async function testStressRandomUsage(): Promise<void> {
|
|||||||
teardownTest();
|
teardownTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testKeyPressLatency(): Promise<void> {
|
||||||
|
console.log("\nTest: KEY PRESS LATENCY (50 toggles, check for exponential slowdown)");
|
||||||
|
setupTest();
|
||||||
|
const engine = await startEngine();
|
||||||
|
await startClientInTmux();
|
||||||
|
openCmdFifo();
|
||||||
|
await wait(500);
|
||||||
|
|
||||||
|
const ITERATIONS = 50;
|
||||||
|
const LATENCY_WARN = 500; // warn if >500ms
|
||||||
|
const LATENCY_FAIL = 5000; // fail if >5s
|
||||||
|
|
||||||
|
let latencies: number[] = [];
|
||||||
|
let prevState = "IDLE";
|
||||||
|
|
||||||
|
for (let i = 0; i < ITERATIONS; i++) {
|
||||||
|
// Determine which state we expect after toggle
|
||||||
|
const expectNext = (prevState === "IDLE") ? "R" : "L";
|
||||||
|
const startTime = Date.now();
|
||||||
|
tmuxSendKeys("looper", "0", "t");
|
||||||
|
const pane = await waitForPaneText(expectNext, 10000);
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
latencies.push(elapsed);
|
||||||
|
|
||||||
|
// Log periodic summary
|
||||||
|
if (i % 10 === 9) {
|
||||||
|
const avg = latencies.slice(i-9, i+1).reduce((a,b)=>a+b,0) / 10;
|
||||||
|
console.log(` Iteration ${i+1}: avg last 10 = ${avg.toFixed(0)} ms, last = ${elapsed} ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed > LATENCY_FAIL) {
|
||||||
|
console.log(` FAIL: Iteration ${i+1} latency ${elapsed} ms exceeds ${LATENCY_FAIL} ms`);
|
||||||
|
engine.kill(); teardownTest();
|
||||||
|
throw new Error(`Latency exceeded fail threshold at iteration ${i+1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed > LATENCY_WARN) {
|
||||||
|
console.log(` WARN: Iteration ${i+1} latency ${elapsed} ms > ${LATENCY_WARN} ms (possible slowdown)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle state for next expectation
|
||||||
|
prevState = (prevState === "IDLE") ? "LOOPING" : "IDLE";
|
||||||
|
await wait(200); // brief cooldown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for trend: if last 10 avg > 3x first 10 avg → exponential
|
||||||
|
const first10Avg = latencies.slice(0,10).reduce((a,b)=>a+b,0) / 10;
|
||||||
|
const last10Avg = latencies.slice(-10).reduce((a,b)=>a+b,0) / 10;
|
||||||
|
console.log(` First 10 avg: ${first10Avg.toFixed(0)} ms, Last 10 avg: ${last10Avg.toFixed(0)} ms`);
|
||||||
|
|
||||||
|
if (last10Avg > 3 * first10Avg && last10Avg > 500) {
|
||||||
|
console.log(` FAIL: Latency grew from ${first10Avg.toFixed(0)} ms to ${last10Avg.toFixed(0)} ms (exponential pattern)`);
|
||||||
|
engine.kill(); teardownTest();
|
||||||
|
throw new Error("Exponential latency increase");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(" PASS: No exponential latency growth");
|
||||||
|
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");
|
||||||
|
|
||||||
@@ -956,7 +1017,8 @@ async function main(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
testRecordOnHighRow,
|
testRecordOnHighRow,
|
||||||
testRecordMoveRecord,
|
testRecordMoveRecord,
|
||||||
testStressRandomUsage
|
testStressRandomUsage,
|
||||||
|
testKeyPressLatency
|
||||||
];
|
];
|
||||||
let passCount = 0;
|
let passCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,81 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signal handler: deactivate and cleanup before exit */
|
||||||
|
static void signal_handler(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
if (global_client) {
|
||||||
|
looper_cleanup(global_client);
|
||||||
|
jack_client_close(global_client);
|
||||||
|
}
|
||||||
|
log_close();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
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,28 +122,16 @@ 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;
|
||||||
|
}
|
||||||
|
if (pos > 0) {
|
||||||
|
int ret = write(status_fd, buf, pos);
|
||||||
(void)ret;
|
(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;
|
||||||
@@ -465,16 +522,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user