feat: add script module for note-to-command mapping with FIFO support
This commit is contained in:
committed by
Loic Coenen (aider)
parent
16a800209f
commit
f776b8a361
@@ -7,6 +7,7 @@ CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
|
||||
CARLA_OBJ = src/carla_host.o
|
||||
PLUGINS_OBJ = src/plugins.o
|
||||
CLIENT_CMD_OBJ = src/client_cmd.o
|
||||
SCRIPT_OBJ = src/script.o
|
||||
|
||||
# Test binaries
|
||||
TEST_PLUGINS_BIN = test_plugins
|
||||
@@ -17,7 +18,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)
|
||||
looper-client: src/main.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ)
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||
|
||||
test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
|
||||
@@ -38,6 +39,20 @@ $(CARLA_TEST_OBJ): src/carla_host.c src/carla_host.h
|
||||
$(CLIENT_CMD_OBJ): src/client_cmd.c src/client_cmd.h
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
|
||||
|
||||
|
||||
$(SCRIPT_OBJ): src/script.c src/script.h
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# --- Script test ---
|
||||
TEST_SCRIPT_BIN = test_script
|
||||
TEST_SCRIPT_OBJ = tests/test_script.o
|
||||
|
||||
$(TEST_SCRIPT_OBJ): tests/test_script.c src/script.h
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(TEST_SCRIPT_BIN): $(TEST_SCRIPT_OBJ) $(SCRIPT_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
|
||||
# --- Plugin tests ---
|
||||
TEST_PLUGINS_OBJ = tests/test_plugins.o
|
||||
|
||||
@@ -65,7 +80,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)
|
||||
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ)
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||
|
||||
# --- Carla host tests ---
|
||||
@@ -91,7 +106,7 @@ $(TEST_CARLA_MOCK_BIN): tests/test_carla_host_mock.c $(CARLA_MOCK_OBJ)
|
||||
$(TEST_INTEGRATION_BIN): tests/test_integration.c $(CARLA_TEST_OBJ)
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -DTESTING -o $@ $^ $(CARLA_LIB) -ljack
|
||||
|
||||
test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN) $(TEST_INTEGRATION_BIN) $(TEST_CARLA_MOCK_BIN)
|
||||
test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN) $(TEST_INTEGRATION_BIN) $(TEST_CARLA_MOCK_BIN) $(TEST_SCRIPT_BIN)
|
||||
./test_status_parse
|
||||
./$(TEST_PLUGINS_BIN)
|
||||
./$(TEST_CLIENT_BIN)
|
||||
@@ -99,8 +114,9 @@ test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(T
|
||||
./$(TEST_CLIENT_CMD_BIN)
|
||||
./$(TEST_INTEGRATION_BIN)
|
||||
./$(TEST_CARLA_MOCK_BIN)
|
||||
./$(TEST_SCRIPT_BIN)
|
||||
|
||||
.PHONY: all test clean
|
||||
|
||||
clean:
|
||||
rm -f looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN) $(TEST_INTEGRATION_BIN) $(TEST_CARLA_MOCK_BIN) *.o tests/*.o src/*.o
|
||||
rm -f looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN) $(TEST_INTEGRATION_BIN) $(TEST_CARLA_MOCK_BIN) $(TEST_SCRIPT_BIN) *.o tests/*.o src/*.o
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
#include "tui.h"
|
||||
#include "script.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *script_path = NULL;
|
||||
|
||||
if (argc > 2 && strcmp(argv[1], "-s") == 0) {
|
||||
script_path = argv[2];
|
||||
} else {
|
||||
const char *home = getenv("HOME");
|
||||
if (home) {
|
||||
static char default_path[1024];
|
||||
snprintf(default_path, sizeof(default_path),
|
||||
"%s/.config/looper/scripts/launchpad.rc", home);
|
||||
script_path = default_path;
|
||||
}
|
||||
}
|
||||
|
||||
if (script_path && script_load(script_path) != 0) {
|
||||
fprintf(stderr, "Warning: could not load script '%s'\n", script_path);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
tui_init();
|
||||
tui_run();
|
||||
tui_cleanup();
|
||||
|
||||
71
client/src/script.c
Normal file
71
client/src/script.c
Normal file
@@ -0,0 +1,71 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "script.h"
|
||||
#include "tui.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define MAX_NOTES 128
|
||||
|
||||
static char *note_actions[MAX_NOTES] = {0};
|
||||
|
||||
int script_load(const char *path) {
|
||||
FILE *fp = fopen(path, "r");
|
||||
if (!fp) return -1;
|
||||
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char *s = line;
|
||||
while (*s == ' ' || *s == '\t') s++;
|
||||
if (*s == '#' || *s == '\n') continue;
|
||||
|
||||
int note;
|
||||
char macro[256];
|
||||
int matched = sscanf(s, "%d %255[^\n]", ¬e, macro);
|
||||
if (matched >= 1 && note >= 0 && note < MAX_NOTES) {
|
||||
free(note_actions[note]);
|
||||
if (matched == 2) {
|
||||
// Trim leading and trailing whitespace from macro
|
||||
char *start = macro;
|
||||
while (*start == ' ' || *start == '\t') start++;
|
||||
char *end = start + strlen(start) - 1;
|
||||
while (end > start && (*end == ' ' || *end == '\t')) end--;
|
||||
*(end + 1) = '\0';
|
||||
if (*start == '\0') {
|
||||
note_actions[note] = NULL;
|
||||
} else {
|
||||
note_actions[note] = strdup(start);
|
||||
}
|
||||
} else {
|
||||
note_actions[note] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void script_handle_note(int note) {
|
||||
if (note < 0 || note >= MAX_NOTES) return;
|
||||
char *macro = note_actions[note];
|
||||
if (!macro) return;
|
||||
|
||||
char macro_copy[512];
|
||||
strncpy(macro_copy, macro, sizeof(macro_copy) - 1);
|
||||
macro_copy[sizeof(macro_copy) - 1] = '\0';
|
||||
|
||||
char *token = strtok(macro_copy, ";");
|
||||
while (token) {
|
||||
while (*token == ' ') token++;
|
||||
if (*token == '\0') {
|
||||
token = strtok(NULL, ";");
|
||||
continue;
|
||||
}
|
||||
char *end = token + strlen(token) - 1;
|
||||
while (end > token && *end == ' ') end--;
|
||||
*(end + 1) = '\0';
|
||||
|
||||
send_command(token);
|
||||
token = strtok(NULL, ";");
|
||||
}
|
||||
}
|
||||
7
client/src/script.h
Normal file
7
client/src/script.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef SCRIPT_H
|
||||
#define SCRIPT_H
|
||||
|
||||
int script_load(const char *path);
|
||||
void script_handle_note(int note);
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "carla_host.h"
|
||||
#include "client_cmd.h"
|
||||
#include "plugins.h"
|
||||
#include "script.h"
|
||||
#include <CarlaHost.h>
|
||||
|
||||
/* ---------- FIFO command helper ---------- */
|
||||
@@ -43,6 +44,7 @@ static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
|
||||
/* status FIFO path */
|
||||
#define STATUS_FIFO "/tmp/looper_status"
|
||||
#define CMD_FIFO "/tmp/looper_cmd"
|
||||
#define NOTES_FIFO "/tmp/looper_notes"
|
||||
|
||||
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
||||
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||
@@ -193,6 +195,9 @@ void tui_init(void) {
|
||||
cell_state[i] = STATE_IDLE;
|
||||
/* open the JACK client used for Carla plugins */
|
||||
carla_init_jack();
|
||||
/* create note FIFO (for scripted controller input) */
|
||||
unlink(NOTES_FIFO);
|
||||
mkfifo(NOTES_FIFO, 0666);
|
||||
}
|
||||
|
||||
/* ---------- TUI run ---------- */
|
||||
@@ -229,6 +234,28 @@ void tui_run(void) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* read any available note events (for script macros) */
|
||||
int nfd = open(NOTES_FIFO, O_RDONLY | O_NONBLOCK);
|
||||
if (nfd >= 0) {
|
||||
char nbuf[256];
|
||||
int m = read(nfd, nbuf, sizeof(nbuf)-1);
|
||||
if (m > 0) {
|
||||
nbuf[m] = '\0';
|
||||
char *p = nbuf;
|
||||
while (*p) {
|
||||
char *nl = strchr(p, '\n');
|
||||
if (nl) *nl = '\0';
|
||||
int note = atoi(p);
|
||||
script_handle_note(note);
|
||||
if (nl) {
|
||||
*nl = '\n';
|
||||
p = nl + 1;
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
close(nfd);
|
||||
}
|
||||
|
||||
if (in_colon) {
|
||||
int chc = getch();
|
||||
if (chc == '\n') {
|
||||
@@ -374,6 +401,7 @@ void tui_cleanup(void) {
|
||||
/* delete FIFOs */
|
||||
unlink(STATUS_FIFO);
|
||||
unlink(CMD_FIFO);
|
||||
unlink(NOTES_FIFO);
|
||||
/* close the Carla JACK client */
|
||||
carla_cleanup_jack();
|
||||
curs_set(1); endwin();
|
||||
|
||||
188
client/tests/test_script.c
Normal file
188
client/tests/test_script.c
Normal file
@@ -0,0 +1,188 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* mock send_command – records last command */
|
||||
static char last_cmd[4096] = "";
|
||||
int send_command(const char *cmd) {
|
||||
strncpy(last_cmd, cmd, sizeof(last_cmd)-1);
|
||||
last_cmd[sizeof(last_cmd)-1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "../src/script.h"
|
||||
|
||||
static int tests_passed = 0;
|
||||
static int tests_failed = 0;
|
||||
|
||||
static void test_load_valid(void) {
|
||||
const char *path = "/tmp/test_script_1.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "# comment\n11 record 0\n12 stop\n\n13 add\n");
|
||||
fclose(f);
|
||||
int r = script_load(path);
|
||||
if (r != 0) {
|
||||
printf("FAIL: script_load returned %d\n", r);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_single_command(void) {
|
||||
const char *path = "/tmp/test_script_2.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "11 record 0\n");
|
||||
fclose(f);
|
||||
script_load(path);
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(11);
|
||||
if (strcmp(last_cmd, "record 0") != 0) {
|
||||
printf("FAIL: expected 'record 0' got '%s'\n", last_cmd);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_multiple_commands(void) {
|
||||
const char *path = "/tmp/test_script_3.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "21 record 0 ; stop\n");
|
||||
fclose(f);
|
||||
script_load(path);
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(21);
|
||||
if (strcmp(last_cmd, "stop") != 0) {
|
||||
printf("FAIL: expected 'stop' got '%s'\n", last_cmd);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_unmapped_note(void) {
|
||||
const char *path = "/tmp/test_script_4.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "30 add\n");
|
||||
fclose(f);
|
||||
script_load(path);
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(31);
|
||||
if (last_cmd[0] != '\0') {
|
||||
printf("FAIL: expected empty last_cmd\n");
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_ignores_comments_and_blanks(void) {
|
||||
const char *path = "/tmp/test_script_5.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "\n# this is a comment\n \n40 bind 2\n\n");
|
||||
fclose(f);
|
||||
int r = script_load(path);
|
||||
if (r != 0) {
|
||||
printf("FAIL: script_load returned %d\n", r);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(40);
|
||||
if (strcmp(last_cmd, "bind 2") != 0) {
|
||||
printf("FAIL: expected 'bind 2' got '%s'\n", last_cmd);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_note_out_of_range(void) {
|
||||
const char *path = "/tmp/test_script_6.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "11 load\n");
|
||||
fclose(f);
|
||||
script_load(path);
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(200);
|
||||
if (last_cmd[0] != '\0') {
|
||||
printf("FAIL: expected empty last_cmd\n");
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
static void test_empty_macro(void) {
|
||||
const char *path = "/tmp/test_script_7.rc";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { tests_failed++; return; }
|
||||
fprintf(f, "11 \n12 record 0\n");
|
||||
fclose(f);
|
||||
int r = script_load(path);
|
||||
if (r != 0) {
|
||||
printf("FAIL: script_load returned %d\n", r);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
last_cmd[0] = '\0';
|
||||
script_handle_note(11);
|
||||
if (last_cmd[0] != '\0') {
|
||||
printf("FAIL: empty macro produced command\n");
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
script_handle_note(12);
|
||||
if (strcmp(last_cmd, "record 0") != 0) {
|
||||
printf("FAIL: expected 'record 0' got '%s'\n", last_cmd);
|
||||
tests_failed++;
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
tests_passed++;
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Script module tests:\n");
|
||||
test_load_valid();
|
||||
test_single_command();
|
||||
test_multiple_commands();
|
||||
test_unmapped_note();
|
||||
test_ignores_comments_and_blanks();
|
||||
test_note_out_of_range();
|
||||
test_empty_macro();
|
||||
|
||||
printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed);
|
||||
return tests_failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user