Compare commits
1 Commits
e79c2ac116
...
master-tmp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc32d12784 |
@@ -7,8 +7,6 @@ CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
|
|||||||
CARLA_OBJ = src/carla_host.o
|
CARLA_OBJ = src/carla_host.o
|
||||||
PLUGINS_OBJ = src/plugins.o
|
PLUGINS_OBJ = src/plugins.o
|
||||||
CLIENT_CMD_OBJ = src/client_cmd.o
|
CLIENT_CMD_OBJ = src/client_cmd.o
|
||||||
SCRIPT_OBJ = src/script.o
|
|
||||||
LOG_OBJ = src/log.o
|
|
||||||
|
|
||||||
# Test binaries
|
# Test binaries
|
||||||
TEST_PLUGINS_BIN = test_plugins
|
TEST_PLUGINS_BIN = test_plugins
|
||||||
@@ -19,7 +17,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)
|
||||||
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||||
|
|
||||||
test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
|
test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
|
||||||
@@ -40,23 +38,6 @@ $(CARLA_TEST_OBJ): src/carla_host.c src/carla_host.h
|
|||||||
$(CLIENT_CMD_OBJ): src/client_cmd.c src/client_cmd.h
|
$(CLIENT_CMD_OBJ): src/client_cmd.c src/client_cmd.h
|
||||||
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
|
$(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 $@ $^
|
|
||||||
|
|
||||||
$(LOG_OBJ): src/log.c
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
|
|
||||||
# --- Plugin tests ---
|
# --- Plugin tests ---
|
||||||
TEST_PLUGINS_OBJ = tests/test_plugins.o
|
TEST_PLUGINS_OBJ = tests/test_plugins.o
|
||||||
|
|
||||||
@@ -84,7 +65,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)
|
||||||
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||||
|
|
||||||
# --- Carla host tests ---
|
# --- Carla host tests ---
|
||||||
@@ -110,7 +91,7 @@ $(TEST_CARLA_MOCK_BIN): tests/test_carla_host_mock.c $(CARLA_MOCK_OBJ)
|
|||||||
$(TEST_INTEGRATION_BIN): tests/test_integration.c $(CARLA_TEST_OBJ)
|
$(TEST_INTEGRATION_BIN): tests/test_integration.c $(CARLA_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) $(CARLA_INC) -DTESTING -o $@ $^ $(CARLA_LIB) -ljack
|
$(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_SCRIPT_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_status_parse
|
./test_status_parse
|
||||||
./$(TEST_PLUGINS_BIN)
|
./$(TEST_PLUGINS_BIN)
|
||||||
./$(TEST_CLIENT_BIN)
|
./$(TEST_CLIENT_BIN)
|
||||||
@@ -118,9 +99,8 @@ test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(T
|
|||||||
./$(TEST_CLIENT_CMD_BIN)
|
./$(TEST_CLIENT_CMD_BIN)
|
||||||
./$(TEST_INTEGRATION_BIN)
|
./$(TEST_INTEGRATION_BIN)
|
||||||
./$(TEST_CARLA_MOCK_BIN)
|
./$(TEST_CARLA_MOCK_BIN)
|
||||||
./$(TEST_SCRIPT_BIN)
|
|
||||||
|
|
||||||
.PHONY: all test clean
|
.PHONY: all test clean
|
||||||
|
|
||||||
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) $(TEST_SCRIPT_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) *.o tests/*.o src/*.o
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
#include "../engine/src/log.c"
|
|
||||||
@@ -1,34 +1,8 @@
|
|||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
#include "script.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
log_init();
|
|
||||||
|
|
||||||
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) {
|
|
||||||
log_msg("Warning: could not load script '%s'", script_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
tui_init();
|
tui_init();
|
||||||
tui_run();
|
tui_run();
|
||||||
tui_cleanup();
|
tui_cleanup();
|
||||||
log_close();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
#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_cleanup(void) {
|
|
||||||
for (int i = 0; i < MAX_NOTES; i++) {
|
|
||||||
free(note_actions[i]);
|
|
||||||
note_actions[i] = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, ";");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#ifndef SCRIPT_H
|
|
||||||
#define SCRIPT_H
|
|
||||||
|
|
||||||
int script_load(const char *path);
|
|
||||||
void script_handle_note(int note);
|
|
||||||
void script_cleanup(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
#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 <CarlaHost.h>
|
#include <CarlaHost.h>
|
||||||
|
|
||||||
/* ---------- FIFO command helper ---------- */
|
/* ---------- FIFO command helper ---------- */
|
||||||
@@ -44,7 +43,6 @@ static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
|
|||||||
/* status FIFO path */
|
/* status FIFO path */
|
||||||
#define STATUS_FIFO "/tmp/looper_status"
|
#define STATUS_FIFO "/tmp/looper_status"
|
||||||
#define CMD_FIFO "/tmp/looper_cmd"
|
#define CMD_FIFO "/tmp/looper_cmd"
|
||||||
#define NOTES_FIFO "/tmp/looper_notes"
|
|
||||||
|
|
||||||
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
||||||
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||||
@@ -195,9 +193,6 @@ void tui_init(void) {
|
|||||||
cell_state[i] = STATE_IDLE;
|
cell_state[i] = STATE_IDLE;
|
||||||
/* open the JACK client used for Carla plugins */
|
/* open the JACK client used for Carla plugins */
|
||||||
carla_init_jack();
|
carla_init_jack();
|
||||||
/* create note FIFO (for scripted controller input) */
|
|
||||||
unlink(NOTES_FIFO);
|
|
||||||
mkfifo(NOTES_FIFO, 0666);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- TUI run ---------- */
|
/* ---------- TUI run ---------- */
|
||||||
@@ -234,28 +229,6 @@ void tui_run(void) {
|
|||||||
close(fd);
|
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) {
|
if (in_colon) {
|
||||||
int chc = getch();
|
int chc = getch();
|
||||||
if (chc == '\n') {
|
if (chc == '\n') {
|
||||||
@@ -398,12 +371,9 @@ void tui_run(void) {
|
|||||||
|
|
||||||
void tui_cleanup(void) {
|
void tui_cleanup(void) {
|
||||||
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||||||
/* free script note allocations */
|
|
||||||
script_cleanup();
|
|
||||||
/* delete FIFOs */
|
/* delete FIFOs */
|
||||||
unlink(STATUS_FIFO);
|
unlink(STATUS_FIFO);
|
||||||
unlink(CMD_FIFO);
|
unlink(CMD_FIFO);
|
||||||
unlink(NOTES_FIFO);
|
|
||||||
/* close the Carla JACK client */
|
/* close the Carla JACK client */
|
||||||
carla_cleanup_jack();
|
carla_cleanup_jack();
|
||||||
curs_set(1); endwin();
|
curs_set(1); endwin();
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Evaluation of Looper Codebase
|
|
||||||
|
|
||||||
## Summary Table
|
|
||||||
|
|
||||||
| Category | Rating | Notes |
|
|
||||||
|-------------------------|--------|-------|
|
|
||||||
| **Mocked / left‑doing** | ⚠️ Moderate | No mock objects; real Carla dependency. Tests for Carla host are stubs. Integration test requires running JACK server. Script module test now passes 7/7 (empty macro bug fixed). |
|
|
||||||
| **Potential segfaults** | ✅ Low | `exec_command` validates channel bounds (`ch < MAX_CHANNELS`). `note_actions` is checked for NULL before use. `strdup` returns not checked but safe in practice. No out‑of‑bounds access identified. |
|
|
||||||
| **Memory management** | ✅ Good | `note_actions` strings freed via `script_cleanup()` called in `tui_cleanup()`. `strdup` allocations are freed on reload. All dynamically allocated audio buffers are freed. No leaks at exit. |
|
|
||||||
| **Thread safety** | ✅ Good | All shared scene/channel fields use C11 atomics. Logging mutex never held in audio thread. Save deactivation uses atomic active flag with two wait periods (500ms + 200ms) guaranteeing RT thread sees the change. FIFO writes are atomic (`PIPE_BUF`). No race conditions identified. |
|
|
||||||
| **Performance** | ✅ Fair | Audio path: `memcpy`, linear loops, no allocations – real‑time safe. Main loop sleeps 50 ms – fine. Save pauses audio for 500 ms (acceptable for a tool). Logging adds negligible mutex overhead outside RT thread. JACK callback time is deterministic. |
|
|
||||||
| **Architectural soundness** | ⚠️ Medium | Separation of engine and client via FIFO is clean. Orchestrator simple but effective. Three command queues still add unnecessary complexity (single queue would suffice). `prev_state` transition detection works. Carla integration remains tightly coupled to JACK client. Script/macro mechanism is extensible and well isolated. Logging design correctly avoids audio thread. |
|
|
||||||
|
|
||||||
## Detailed Commentary
|
|
||||||
|
|
||||||
### 1. Mocked / Left‑doing
|
|
||||||
- **Engine tests** require a live JACK server – no mocking.
|
|
||||||
- **Client tests** for `carla_host` and `plugins` use stubs when `TESTING` is defined; real Carla library is still linked. Integration test also requires JACK.
|
|
||||||
- **Script tests** pass 7/7 after the empty macro bug was fixed.
|
|
||||||
- **No mock for MIDI** – engine integration test uses actual MIDI events.
|
|
||||||
|
|
||||||
### 2. Potential Segfaults
|
|
||||||
- **Channel bounds** are now validated in `exec_command`: `if (ch < 0 || ch >= MAX_CHANNELS) ch = 0;`. Safe.
|
|
||||||
- **NULL pointer dereference**: `script_handle_note` checks for NULL before using `macro`. `strdup` failure would set `note_actions[note]` to NULL, then checked. Safe.
|
|
||||||
- **FIFO write errors** silently ignored – no crash.
|
|
||||||
|
|
||||||
### 3. Memory Management
|
|
||||||
- `note_actions[]` strings are freed on every `script_load` write and on cleanup (`script_cleanup` called in `tui_cleanup`). No leak.
|
|
||||||
- Audio loop buffer (`loop_data_t`) is embedded in `scene_t` – no heap allocation.
|
|
||||||
- Save path uses `malloc/free` correctly; freed after write.
|
|
||||||
- Log file pointer is closed at exit.
|
|
||||||
|
|
||||||
### 4. Thread Safety
|
|
||||||
- All `scene_t` fields accessed from both threads are atomic (`state`, `prev_state`, `record_pos`, `loop_count`, `playback_pos`). Correct.
|
|
||||||
- `channel_t.active`, `channel_t.save_ring` are atomic. Correct.
|
|
||||||
- **Save sequence**: set `active=0`, sleep 500 ms, copy buffer, set `active=1`. The sleep guarantees the RT thread has seen the deactivation. No race.
|
|
||||||
- **Logging mutex** acquired only outside audio thread – fine.
|
|
||||||
- **FIFO writes** from multiple threads are not serialized but `write` to a FIFO is atomic for writes ≤ `PIPE_BUF` (4096 bytes). Our messages are smaller. Safe.
|
|
||||||
|
|
||||||
### 5. Performance
|
|
||||||
- Audio buffer processing uses simple loops with no function calls. `nframes` is typically 64‑256 – fine.
|
|
||||||
- `state` transitions check `prev_state` each callback – cheap.
|
|
||||||
- Save mechanism: 500 ms pause may cause one xrun. Acceptable for a prototype.
|
|
||||||
- Main loop sleep 50 ms ensures low CPU usage.
|
|
||||||
|
|
||||||
### 6. Architectural Soundness
|
|
||||||
- **Good**: Clear separation between engine (real‑time audio) and client (UI, plugin management). Communication via FIFO files.
|
|
||||||
- **Weakness**: Three command queues (`cmd_queue`, `cmd_queue_main_midi`, `cmd_queue_main_fifo`) are redundant – all feed `exec_command`. Could consolidate.
|
|
||||||
- **Weakness**: Carla integration tied to the engine’s JACK client. A separate Carla engine instance would be cleaner.
|
|
||||||
- **Strength**: Script/macro system is simple text‑based and extensible. The notes FIFO allows any external controller to inject note numbers.
|
|
||||||
- **Strength**: Logging non‑intrusive and never used in real‑time path.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Overall, the codebase is functional and stable.* All previously identified critical issues (channel bounds, memory leak, empty macro) have been fixed. Recommendations for further improvement:
|
|
||||||
- Replace three command queues with a single queue.
|
|
||||||
- Use a double‑buffer for save to eliminate the 500 ms pause.
|
|
||||||
- Consider mock objects for engine tests to remove JACK dependency.
|
|
||||||
- Add more unit tests for edge cases.
|
|
||||||
|
|
||||||
**Evaluation date**: 18 May 2026
|
|
||||||
413
docs/manual.md
413
docs/manual.md
@@ -1,413 +0,0 @@
|
|||||||
# Looper – JACK‑based audio looper
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
`looper` is a real‑time audio and MIDI looper that runs as a JACK client. It supports multiple channels (each with multiple scenes), recording, looping, and saving/loading loops as WAV files. It can be controlled via MIDI notes or via a named FIFO (`/tmp/looper_cmd`).
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- JACK development libraries (`libjack-dev` or `libjack-jackd2-dev`)
|
|
||||||
- libsndfile development libraries (`libsndfile1-dev`)
|
|
||||||
- POSIX threads, C11 atomics
|
|
||||||
- `make`
|
|
||||||
|
|
||||||
### Compilation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd engine
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces the `looper` binary and a set of test executables.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
Start the JACK server if it is not already running:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
jackd -d alsa -r 48000 -p 256 # example parameters
|
|
||||||
```
|
|
||||||
|
|
||||||
Then launch the looper:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./looper
|
|
||||||
```
|
|
||||||
|
|
||||||
The looper will register the following JACK ports:
|
|
||||||
|
|
||||||
- `looper:input` (audio in)
|
|
||||||
- `looper:output` (audio out)
|
|
||||||
- `looper:control` (MIDI input – control messages)
|
|
||||||
- `looper:clock` (MIDI input – transport clock)
|
|
||||||
- `looper:channel1_input`, `looper:channel1_output` (first dynamic channel)
|
|
||||||
|
|
||||||
Additional ports are created for every extra channel (e.g. `looper:channel2_input`, …).
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **Channels**: Each channel is independent and contains up to `MAX_SCENES` scenes. Channel 0 always exists; additional channels can be added/removed at runtime.
|
|
||||||
- **Scenes**: Each scene can be in one of four states: `IDLE`, `RECORD`, `LOOPING`, `PAUSED`. Only the current scene of a channel is active.
|
|
||||||
- **MIDI Control**: Many operations are triggered by MIDI notes received on the `looper:control` port. A special *control key* (note 64) acts as a modifier: while held, subsequent notes select a different function.
|
|
||||||
- **FIFO Commands**: A set of human‑readable commands can be written to `/tmp/looper_cmd` to control the looper from scripts or terminals.
|
|
||||||
- **Status FIFO**: The looper writes its current state to `/tmp/looper_status` (one line per active channel).
|
|
||||||
|
|
||||||
## Control
|
|
||||||
|
|
||||||
### MIDI Control (port `looper:control`)
|
|
||||||
|
|
||||||
All MIDI notes must be on channel 0 (status byte `0x90`).
|
|
||||||
|
|
||||||
| Note | Without modifier | With modifier (hold note 64) |
|
|
||||||
|------|------------------|-----------------------------|
|
|
||||||
| 0 | – (reserved) | **Bind channel** – set the channel that will be affected by subsequent commands (note value = channel index). |
|
|
||||||
| 1 | **Cycle** – toggles the current scene of the *bound* channel (or channel 0 if unbound) through IDLE→RECORD→LOOPING→PAUSED→LOOPING→… | – |
|
|
||||||
| 62 | – | **Cycle** – same as note 1 but always acts on the bound channel. |
|
|
||||||
| 63 | – | **Unbind** – resets the bound channel to channel 0. |
|
|
||||||
| 64 | – | *Control key* – held while other notes are pressed to apply the modifier column. |
|
|
||||||
| 70 | – | **Load WAV** – loads `loop.wav` from the current directory into channel 0's current scene. |
|
|
||||||
| 71 | – | **Save WAV** – saves the current loop (if the scene is in LOOPING state) to `save.wav`. |
|
|
||||||
|
|
||||||
*Notes for developers*: The MIDI handler is implemented in `engine/src/midi.c`. The control‑key state is stored in `atomic_int control_key_active`.
|
|
||||||
|
|
||||||
### FIFO Commands (file `/tmp/looper_cmd`)
|
|
||||||
|
|
||||||
Write a line to the FIFO; each line activates one command.
|
|
||||||
|
|
||||||
| Command line | Description |
|
|
||||||
|-----------------------|-------------|
|
|
||||||
| `record <ch>` | Cycle the current scene of channel `<ch>` (same as MIDI note 1). |
|
|
||||||
| `stop` | Force all scenes of the bound channel to IDLE. |
|
|
||||||
| `add` | Add a new audio channel. |
|
|
||||||
| `add_midi` | Add a new MIDI channel. |
|
|
||||||
| `remove` | Remove the last added dynamic channel. |
|
|
||||||
| `bind <ch>` | Bind subsequent commands to channel `<ch>`. |
|
|
||||||
| `unbind` | Unbind (revert to channel 0). |
|
|
||||||
| `scene_add` | Add a new scene to the bound channel. |
|
|
||||||
| `scene_remove` | Remove the current scene (not the last one) from the bound channel. |
|
|
||||||
| `scene_next` | Switch to the next scene of the bound channel. |
|
|
||||||
| `scene_prev` | Switch to the previous scene of the bound channel. |
|
|
||||||
| `load` | Load `loop.wav` into channel 0's current scene. |
|
|
||||||
| `save` | Save the current loop (if in LOOPING state) to `save.wav`. |
|
|
||||||
|
|
||||||
#### Example session (shell)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# record something on channel 0
|
|
||||||
echo "record 0" > /tmp/looper_cmd
|
|
||||||
# after some seconds, stop recording (cycle again)
|
|
||||||
echo "record 0" > /tmp/looper_cmd
|
|
||||||
# now the loop plays back; save it
|
|
||||||
echo "save" > /tmp/looper_cmd
|
|
||||||
# add a new channel
|
|
||||||
echo "add" > /tmp/looper_cmd
|
|
||||||
# bind to that channel (assuming it is channel 1)
|
|
||||||
echo "bind 1" > /tmp/looper_cmd
|
|
||||||
# record a loop on channel 1
|
|
||||||
echo "record 1" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
|
|
||||||
### MIDI Clock (port `looper:clock`)
|
|
||||||
|
|
||||||
The looper responds to a subset of MIDI Real Time messages:
|
|
||||||
|
|
||||||
| Message | Action |
|
|
||||||
|---------|--------|
|
|
||||||
| `0xFA` (Start) | If channel 0's current scene is IDLE, switches it to RECORD. |
|
|
||||||
| `0xFC` (Stop) | Sets channel 0's current scene to IDLE. |
|
|
||||||
| `0xFB` (Continue) | If channel 0's current scene is PAUSED, switches it to LOOPING. |
|
|
||||||
|
|
||||||
These allow synchronisation with an external sequencer.
|
|
||||||
|
|
||||||
## Detailed Behaviour
|
|
||||||
|
|
||||||
### Audio Channels
|
|
||||||
|
|
||||||
- **IDLE**: Input is passed through to output (live monitoring).
|
|
||||||
- **RECORD**: Input is written to the loop buffer (up to `LOOP_BUF_SIZE` frames). The buffer is written circularly; when the buffer is full, recording overwrites from the beginning.
|
|
||||||
- **LOOPING**: The recorded loop is played back repeatedly. The loop length equals the number of frames written during the last RECORD session (stored in `loop_count`).
|
|
||||||
- **PAUSED**: Output is silent; the loop is not playing.
|
|
||||||
|
|
||||||
Transitioning from RECORD→LOOPING immediately finalises the loop length and begins playback from the start of the buffer.
|
|
||||||
|
|
||||||
### MIDI Channels
|
|
||||||
|
|
||||||
MIDI channels work similarly but record MIDI events instead of audio. Received MIDI events are stored in a fixed-size array (`MAX_MIDI_EVENTS`). During LOOPING, all recorded events are played back at the beginning of each cycle (timestamps are not used in playback) – this is suitable for drum patterns and short phrases.
|
|
||||||
|
|
||||||
### Dynamic Channels
|
|
||||||
|
|
||||||
You can add and remove channels at runtime using FIFO commands or MIDI notes. Each new channel gets its own audio input/output ports and, for MIDI channels, separate MIDI ports. Removing a channel deactivates it and eventually unregisters its ports. The removal happens with a one‑second grace delay to avoid disrupting the audio thread.
|
|
||||||
|
|
||||||
### Scenes
|
|
||||||
|
|
||||||
Each channel can have several scenes (default one). You can add/remove scenes and switch between them. Only the current scene is active; switching scenes preserves their individual state and loop data. This allows you to prepare multiple loops on the same channel and swap between them.
|
|
||||||
|
|
||||||
### Loading WAV files
|
|
||||||
|
|
||||||
The `load` command reads a file named `loop.wav` (mono, 16‑bit PCM, any sample rate) from the working directory. It loads the audio into channel 0's current scene, sets the scene state to LOOPING, and begins playback. The loop length is the number of frames read (up to `LOOP_BUF_SIZE`).
|
|
||||||
|
|
||||||
### Saving WAV files
|
|
||||||
|
|
||||||
The `save` command writes the current loop of channel 0's current scene to `save.wav` (mono, 16‑bit PCM, same sample rate as the JACK server). Saving is synchronous – the audio thread is briefly deactivated to safely copy the buffer. The file is created in the working directory.
|
|
||||||
|
|
||||||
### Status FIFO
|
|
||||||
|
|
||||||
The looper periodically writes its state to `/tmp/looper_status`. Each line has the format:
|
|
||||||
|
|
||||||
```
|
|
||||||
CH=<channel> SC=<scene_index> STATE=<IDLE|RECORD|LOOPING|PAUSED>
|
|
||||||
```
|
|
||||||
|
|
||||||
This can be used by external monitoring tools or scripts to track the looper's state.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Engine Tests
|
|
||||||
|
|
||||||
The `engine` directory contains several test executables that are built by `make test`. They require a running JACK server.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd engine
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests include:
|
|
||||||
|
|
||||||
- Audio pass‑through (connectivity)
|
|
||||||
- Loop recording and playback (counts bursts of audio)
|
|
||||||
- Dynamic channel creation and removal
|
|
||||||
- Control‑key modifier and channel binding
|
|
||||||
- WAV file loading and saving
|
|
||||||
|
|
||||||
Test results are reported to stdout. If any test fails, the exit code is non‑zero.
|
|
||||||
|
|
||||||
### Client Tests
|
|
||||||
|
|
||||||
The `client` directory also contains unit and integration tests. They can be run with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd client
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
These tests verify the client command parser, plugin stubs, Carla host interface, and status FIFO parsing. They do **not** require a running JACK server for the mock‑based tests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Client (TUI Application)
|
|
||||||
|
|
||||||
The looper project includes a terminal‑based user interface client that runs alongside the engine. It provides a grid view of channels/scenes, a rack view for managing LV2/VST plugins via Carla, and a command line (`:`‑mode) for advanced operations.
|
|
||||||
|
|
||||||
## Building the Client
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd client
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces the `looper-client` binary and a test executable (`test_status_parse`).
|
|
||||||
|
|
||||||
## Running the Client
|
|
||||||
|
|
||||||
Start the looper engine first (see [Running the Engine](#running)), then launch the client:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./looper-client
|
|
||||||
```
|
|
||||||
|
|
||||||
The client will connect to the engine via the FIFO `/tmp/looper_cmd` and read status from `/tmp/looper_status`. It opens a ncurses interface.
|
|
||||||
|
|
||||||
## TUI Keybindings
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|-----|--------|
|
|
||||||
| `h` / Left | Move selection left |
|
|
||||||
| `j` / Down | Move selection down |
|
|
||||||
| `k` / Up | Move selection up |
|
|
||||||
| `l` / Right | Move selection right |
|
|
||||||
| `t` | Toggle recording on the selected channel (sends `record N`) |
|
|
||||||
| `s` | Switch to next scene |
|
|
||||||
| `S` | Switch to previous scene |
|
|
||||||
| `d` / `D` | Stop all scenes on the bound channel |
|
|
||||||
| `a` | Add a new audio channel |
|
|
||||||
| `A` | Add a new MIDI channel |
|
|
||||||
| `r` | Remove the last dynamic channel |
|
|
||||||
| `b` | Bind to the selected channel (sends `bind N`) |
|
|
||||||
| `u` | Unbind (revert to channel 0) |
|
|
||||||
| `?` | Toggle help overlay |
|
|
||||||
| `R` | Toggle rack view (for plugin management) |
|
|
||||||
| `Esc` / `Q` | Quit (in grid mode) or return to grid (in rack mode) |
|
|
||||||
| `:` | Enter colon‑command mode (see below) |
|
|
||||||
|
|
||||||
### Rack View (plugin management)
|
|
||||||
|
|
||||||
When you press `R`, the TUI switches to a rack view showing all plugins loaded via Carla. In this mode:
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|-----|--------|
|
|
||||||
| `j` / Down | Select next plugin |
|
|
||||||
| `k` / Up | Select previous plugin |
|
|
||||||
| `b` / `B` | Bypass the selected plugin |
|
|
||||||
| `d` / `D` | Unload the selected plugin |
|
|
||||||
| `x` / `X` | Disconnect all JACK connections for the selected plugin |
|
|
||||||
| `Esc` | Return to grid view |
|
|
||||||
|
|
||||||
## Colon Commands
|
|
||||||
|
|
||||||
Press `:` to enter command‑line mode. Type a command and press `Enter`. The following commands are recognised:
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `from <port>` | Store a source port name (e.g. `looper:out_0`) |
|
|
||||||
| `to <port>` | Store a destination port name (e.g. `plugin:in`) |
|
|
||||||
| `addplugin <path>` | Load a plugin from the given binary path. If `from` and `to` are set, it also auto‑connects the plugin. |
|
|
||||||
| `connect [from] [to]` | Connect two JACK ports. If omitted, uses stored `from`/`to`. |
|
|
||||||
| `disconnect [from] [to]` | Disconnect two JACK ports. |
|
|
||||||
| `rack` | Switch to rack view (same as `R`) |
|
|
||||||
| `grid` | Switch to grid view |
|
|
||||||
|
|
||||||
### Example colon session
|
|
||||||
|
|
||||||
```
|
|
||||||
:from looper:channel1_output
|
|
||||||
:to system:playback_2
|
|
||||||
:addplugin /usr/lib/lv2/amsynth.lv2/amsynth.so
|
|
||||||
:connect looper:channel1_output amsynth:in
|
|
||||||
```
|
|
||||||
|
|
||||||
## Status Display
|
|
||||||
|
|
||||||
The TUI reads the engine’s status FIFO (`/tmp/looper_status`) and displays the state of each channel as coloured cells in a 8×8 grid:
|
|
||||||
|
|
||||||
- **White** (IDLE) – channel is monitoring
|
|
||||||
- **Red** (RECORD) – channel is recording
|
|
||||||
- **Green** (LOOPING) – loop is playing back
|
|
||||||
- **Blue** (PAUSED) – loop is paused
|
|
||||||
- **Cyan** – currently selected cell
|
|
||||||
|
|
||||||
The status is updated continuously in real time.
|
|
||||||
|
|
||||||
## Plugin Management Internals
|
|
||||||
|
|
||||||
The client uses the **Carla** host library to load and manage plugins. You must have Carla development libraries installed (`libcarla-standalone-dev` or equivalent). Plugins are loaded in a separate Carla engine instance, and their JACK ports are connected to the looper’s ports using the looper’s own JACK client.
|
|
||||||
|
|
||||||
The `carla_host.c` module wraps Carla’s API and provides `carla_load`, `carla_unload`, `carla_connect`, etc. The `plugins.c` module provides a simpler interface used by the command parser.
|
|
||||||
|
|
||||||
## Communication with the Engine
|
|
||||||
|
|
||||||
All actions in the TUI are translated into FIFO commands written to `/tmp/looper_cmd`. The engine’s pipe reader thread picks them up and executes them. The status FIFO `/tmp/looper_status` is polled by the TUI’s main loop to update the display.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The following constants can be adjusted at compile time by editing `engine/src/channel.h` and `engine/src/looper.c`:
|
|
||||||
|
|
||||||
| Constant | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `MAX_CHANNELS` | 8 | Maximum number of dynamic channels. |
|
|
||||||
| `MAX_SCENES` | 4 | Maximum scenes per channel. |
|
|
||||||
| `LOOP_BUF_SIZE` | 48000 * 8 | Maximum loop length in frames (8 seconds at 48 kHz). |
|
|
||||||
| `MAX_MIDI_EVENTS` | 1024 | Maximum MIDI events that can be recorded per scene. |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **No sound**: Ensure that the JACK server is running and that you have connected `looper:output` to your system playback ports.
|
|
||||||
- **MIDI not working**: Use `jack_connect` to connect your controller's output to `looper:control`. The note must be on MIDI channel 0.
|
|
||||||
- **save.wav not created**: The scene must be in `LOOPING` state before issuing the save command. Also verify that a loop has been recorded (loop length > 0).
|
|
||||||
- **FIFO not working**: The FIFO is created automatically. You can write to it with a simple `echo "record 0" > /tmp/looper_cmd`. If you get "no such file or directory", start the looper first.
|
|
||||||
|
|
||||||
## Source Code Organisation
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `engine/src/main.c` | Entry point; opens JACK client, calls `looper_init()` and enters the main loop. |
|
|
||||||
| `engine/src/looper.c` | Core logic: `process_callback`, `looper_process_commands`, command execution, WAV load/save. |
|
|
||||||
| `engine/src/channel.c` | Channel and scene management (`channel_add`, `channel_remove`, `init_scene`, etc.). |
|
|
||||||
| `engine/src/midi.c` | MIDI event parsing and handling for the control port. |
|
|
||||||
| `engine/src/queue.c` | Lock‑free SPSC queue used for passing commands between threads. |
|
|
||||||
| `engine/src/ringbuffer.c` | Ring buffer used for saving loop audio to disk asynchronously (deprecated, now synchronous). |
|
|
||||||
| `engine/src/pipe.c` | FIFO reader thread that translates text lines into `command_t` structs. |
|
|
||||||
| `engine/src/wav.c` | WAV file reading and writing (libsndfile wrapper). |
|
|
||||||
| `engine/src/command.h` | `command_t` type definition and `cmd_type_t` enum. |
|
|
||||||
| `engine/src/channel.h` | Data structures for `channel_t`, `scene_t`, `loop_data_t`. |
|
|
||||||
| `engine/src/looper.h` | Function prototypes for callbacks and initialisation. |
|
|
||||||
| `engine/tests/` | Integration tests for the above features. |
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[Add your license information here.]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Orchestrator and Logging
|
|
||||||
|
|
||||||
The orchestrator (`orchestrator.c`, built to `./looper`) launches both the engine and the TUI client in a single process group. It handles graceful shutdown of both children when you press Ctrl+C.
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make # builds engine, client, and orchestrator
|
|
||||||
```
|
|
||||||
|
|
||||||
The target `orchestrator` is built by the top‑level `make` automatically.
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./looper # starts engine and client
|
|
||||||
./looper -s ~/my.rc # loads a custom script file for the launchpad
|
|
||||||
```
|
|
||||||
|
|
||||||
To stop, press `Ctrl+C`. The orchestrator sends `SIGTERM` to both children and waits for them to exit.
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
Both the engine and the client write messages to `/tmp/looper.log` (appended). The file is opened at startup and closed at shutdown. The audio thread (`process_callback`) never calls any logging function, so real‑time performance is unaffected.
|
|
||||||
|
|
||||||
To watch the log in real time:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tail -f /tmp/looper.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Launchpad Scripting (via notes FIFO)
|
|
||||||
|
|
||||||
The client reads note events from `/tmp/looper_notes` (created automatically). You can feed note numbers into this FIFO using any external tool (JACK MIDI‑to‑FIFO bridge, shell script, etc.). Each line must contain a single integer note number (0–127). The client then calls `script_handle_note`, which executes the macro associated with that note in the currently loaded script file.
|
|
||||||
|
|
||||||
The default script path is `~/.config/looper/scripts/launchpad.rc`. A sample script:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir -p ~/.config/looper/scripts
|
|
||||||
cat > ~/.config/looper/scripts/launchpad.rc << 'EOF'
|
|
||||||
# Grid notes 11‑88
|
|
||||||
11 record 0
|
|
||||||
12 record 1
|
|
||||||
13 record 2
|
|
||||||
...
|
|
||||||
# Right column (scene triggers)
|
|
||||||
19 scene_next
|
|
||||||
29 scene_next
|
|
||||||
39 scene_next
|
|
||||||
...
|
|
||||||
# Top row control keys 91‑98
|
|
||||||
91 stop
|
|
||||||
92 scene_prev
|
|
||||||
93 load
|
|
||||||
94 save
|
|
||||||
95 add
|
|
||||||
96 remove
|
|
||||||
97 add_midi
|
|
||||||
98 unbind
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
You can override the script path with the `-s` flag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./looper -s /home/user/my_custom.rc
|
|
||||||
```
|
|
||||||
|
|
||||||
*Manual generated from the looper source code v1.0.*
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
# Manual Test Protocols – Guitar / Audio Looper
|
|
||||||
|
|
||||||
This document provides step‑by‑step manual testing procedures using a real guitar (or any line‑level mono audio source) with the `looper` engine.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- A running JACK server (e.g. `jackd -d alsa -r 48000 -p 256`)
|
|
||||||
- The looper binary compiled (`cd engine && make`)
|
|
||||||
- An audio interface recognised by ALSA (or PulseAudio JACK bridge)
|
|
||||||
- A guitar connected to your interface’s input
|
|
||||||
- `qjackctl` (optional, for visual wiring) or knowledge of `jack_connect` commands
|
|
||||||
|
|
||||||
## Test 1 – Basic Audio Pass‑Through (Guitar Monitor)
|
|
||||||
|
|
||||||
1. Start the looper in a terminal:
|
|
||||||
```sh
|
|
||||||
./looper
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Launch `qjackctl` (or use `jack_connect` from shell) to view available ports.
|
|
||||||
|
|
||||||
3. Connect your interface’s capture port to the looper’s input:
|
|
||||||
```sh
|
|
||||||
jack_connect system:capture_1 looper:input
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Connect the looper’s output to your interface playback ports:
|
|
||||||
```sh
|
|
||||||
jack_connect looper:output system:playback_1
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Pluck a few strings – you should hear your guitar coming through the looper immediately (channel 0 is in **IDLE** state, which passes input straight to output).
|
|
||||||
|
|
||||||
6. To stop, press `Ctrl+C` in the looper terminal.
|
|
||||||
|
|
||||||
**Expected result**: You hear your guitar with no latency issues (depending on JACK buffer size). If you hear nothing, check port names with `jack_lsp`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 2 – Record a Short Loop (MIDI Control)
|
|
||||||
|
|
||||||
### 2a – Using MIDI keyboard (or a MIDI controller)
|
|
||||||
|
|
||||||
1. Start the looper as above.
|
|
||||||
2. Connect your MIDI controller to the looper’s control port:
|
|
||||||
```sh
|
|
||||||
jack_connect <controller>:midi_out looper:control
|
|
||||||
```
|
|
||||||
Replace `<controller>` with the actual MIDI port name (use `jack_lsp` to find it).
|
|
||||||
|
|
||||||
3. Send **note 1** (velocity 127) to switch channel 0 into **RECORD** state.
|
|
||||||
- On most keyboards, this is the C# key two octaves above middle C (MIDI note 1). Press it once.
|
|
||||||
|
|
||||||
4. Play your guitar for about 2 seconds. The looper is recording.
|
|
||||||
|
|
||||||
5. Press **note 1** again. The looper transitions to **LOOPING** state. The recorded 2‑second phrase starts playing back repeatedly.
|
|
||||||
|
|
||||||
6. You should hear the loop repeating. Pluck strings while the loop plays – the IDLE monitoring is still active on channel 0 (the loop is mixed with the live input).
|
|
||||||
|
|
||||||
7. Press **note 1** a third time to **PAUSE** the loop; press again to resume.
|
|
||||||
|
|
||||||
### 2b – Using FIFO commands (if you have no MIDI keyboard)
|
|
||||||
|
|
||||||
1. Start the looper.
|
|
||||||
2. Open a second terminal.
|
|
||||||
3. Send:
|
|
||||||
```sh
|
|
||||||
echo "record 0" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
4. Play guitar for a few seconds.
|
|
||||||
5. Send again:
|
|
||||||
```sh
|
|
||||||
echo "record 0" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
6. Loop should start repeating. Test pause by sending again (second command cycles IDLE→RECORD→LOOPING→PAUSED→…).
|
|
||||||
|
|
||||||
**Expected result**: The loop repeats seamlessly. If you hold a chord while the loop is playing, the live input still passes through.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 3 – Save the Loop to a WAV File
|
|
||||||
|
|
||||||
1. Ensure a loop is playing (LOOPING state) on channel 0.
|
|
||||||
|
|
||||||
2. Send the save command:
|
|
||||||
```sh
|
|
||||||
echo "save" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
or (MIDI) press control‑key (note 64) + note 71.
|
|
||||||
|
|
||||||
3. After a brief delay (the loop buffer is written synchronously), a file `save.wav` appears in the engine directory.
|
|
||||||
|
|
||||||
4. Check the file size is > 44 bytes and play it with any media player:
|
|
||||||
```sh
|
|
||||||
aplay save.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected result**: The saved file contains exactly what the looper was playing (your recorded guitar phrase). The RMS of the playback should be similar to the live signal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 4 – Load a WAV File into a Channel
|
|
||||||
|
|
||||||
1. Put a mono 16‑bit WAV file named `loop.wav` in the engine directory (e.g. a short drum loop or a guitar riff).
|
|
||||||
|
|
||||||
2. Start the looper and send the load command:
|
|
||||||
```sh
|
|
||||||
echo "load" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
or (MIDI) control‑key + note 70.
|
|
||||||
|
|
||||||
3. The loaded audio begins playing immediately on channel 0 (state = LOOPING).
|
|
||||||
|
|
||||||
4. Verify you hear the loop repeating.
|
|
||||||
|
|
||||||
**Expected result**: The WAV is loaded and plays correctly. The loop length matches the duration of the file (up to `LOOP_BUF_SIZE` frames, default 8 seconds).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 5 – Dynamic Channel Creation and Binding
|
|
||||||
|
|
||||||
1. Start the looper.
|
|
||||||
|
|
||||||
2. Add a second audio channel:
|
|
||||||
```sh
|
|
||||||
echo "add" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check that new ports appear:
|
|
||||||
```sh
|
|
||||||
jack_lsp | grep channel1
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Bind the client to channel 1:
|
|
||||||
```sh
|
|
||||||
echo "bind 1" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Connect your guitar to both channels for stereo testing? Not necessary. But you can route differently.
|
|
||||||
|
|
||||||
6. Now when you send `record 1`, the bind ensures the command affects channel 1 instead of channel 0.
|
|
||||||
|
|
||||||
7. Repeat the record/loop process on channel 1, while channel 0 continues its own loop.
|
|
||||||
|
|
||||||
**Expected result**: Two independent loops can play simultaneously without interfering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 6 – Scene Switching
|
|
||||||
|
|
||||||
1. Make sure a loop is playing on channel 0.
|
|
||||||
|
|
||||||
2. Add a second scene to channel 0:
|
|
||||||
```sh
|
|
||||||
echo "scene_add" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
(Only adds scene if `MAX_SCENES` not exceeded, default 4.)
|
|
||||||
|
|
||||||
3. Switch to the new scene:
|
|
||||||
```sh
|
|
||||||
echo "scene_next" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
The playback stops because the new scene is IDLE.
|
|
||||||
|
|
||||||
4. Record a different phrase on the new scene (send `record 0`).
|
|
||||||
|
|
||||||
5. Switch back to the first scene (`scene_prev`) – the original loop resumes.
|
|
||||||
|
|
||||||
**Expected result**: Different independent loops in separate scenes; switching scenes does not lose previously recorded loops.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 7 – MIDI Clock Sync
|
|
||||||
|
|
||||||
If you have an external MIDI clock source (e.g. a drum machine or DAW sending MIDI start/stop):
|
|
||||||
|
|
||||||
1. Connect the clock source to `looper:clock` port.
|
|
||||||
|
|
||||||
2. Send MIDI Start (`0xFA`). The looper’s current scene (if IDLE) transitions to RECORD.
|
|
||||||
|
|
||||||
3. Send MIDI Stop (`0xFC`). The current scene goes IDLE (loop stops).
|
|
||||||
|
|
||||||
4. Send MIDI Continue (`0xFB`) while the scene is PAUSED – it resumes LOOPING.
|
|
||||||
|
|
||||||
**Expected result**: Transport commands control looper state reliably.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test 8 – Edge Cases
|
|
||||||
|
|
||||||
### 8a – Rapid toggling
|
|
||||||
|
|
||||||
Cycle the RECORD/LOOPING/PAUSED states many times in quick succession (send `record 0` every 200 ms for 5 seconds). The looper should not crash or produce glitches.
|
|
||||||
|
|
||||||
### 8b – Remove channel while playing
|
|
||||||
|
|
||||||
Add a channel, start a loop on it, then remove the channel with:
|
|
||||||
```sh
|
|
||||||
echo "remove" > /tmp/looper_cmd
|
|
||||||
```
|
|
||||||
The loop should stop gracefully after a one‑second grace period; the client should not crash.
|
|
||||||
|
|
||||||
### 8c – Save empty loop
|
|
||||||
|
|
||||||
Attempt to `save` when the current scene is not LOOPING or loop_count == 0. No file should be created. The engine should log a message to stderr.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
- `LOOPER_CMD_FIFO` (overrides `/tmp/looper_cmd`) – useful for running multiple instances for testing.
|
|
||||||
- `JACK_DEFAULT_SERVER` (JACK environment) – can be set to run a separate JACK server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **No audio after connection**: Ensure `jack_lsp` shows both source and destination ports, and that the looper is the only client using those ports.
|
|
||||||
- **MIDI not recognised**: Verify that `midi_control_port` is created (`looper:control`). Use `jack_midi_dump` to see if note events arrive.
|
|
||||||
- **“save.wav not created“** after save command: The scene must be in LOOPING state and `loop_count` > 0. Check the engine’s terminal output for error messages.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Carla Plugin Management Manual Tests
|
|
||||||
|
|
||||||
### Test C1 – Load a plugin via colon command
|
|
||||||
|
|
||||||
1. Ensure the looper engine is running, and the client (`looper-client`) is also running.
|
|
||||||
|
|
||||||
2. In the client, enter colon mode (`:`) and type:
|
|
||||||
```
|
|
||||||
from looper:output
|
|
||||||
```
|
|
||||||
then press Enter.
|
|
||||||
|
|
||||||
3. Enter colon mode again and type:
|
|
||||||
```
|
|
||||||
to system:playback_1
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Load a test LV2 plugin (e.g., /usr/lib/lv2/amsynth.lv2/amsynth.so):
|
|
||||||
```
|
|
||||||
addplugin /usr/lib/lv2/amsynth.lv2/amsynth.so
|
|
||||||
```
|
|
||||||
|
|
||||||
5. The plugin should be loaded into Carla and its JACK ports are automatically connected (if `from` and `to` were set). You should see the plugin appear in the rack view when you press `R`.
|
|
||||||
|
|
||||||
6. Play some audio through the looper – it should be processed by the plugin.
|
|
||||||
|
|
||||||
### Test C2 – Toggle bypass
|
|
||||||
|
|
||||||
1. In rack view (`R`), select the plugin using `j`/`k`.
|
|
||||||
|
|
||||||
2. Press `b` or `B` to toggle bypass.
|
|
||||||
|
|
||||||
3. The effect should stop processing (bypass mode active); pressing again reactivates.
|
|
||||||
|
|
||||||
### Test C3 – Disconnect a plugin
|
|
||||||
|
|
||||||
1. In rack view, select the plugin.
|
|
||||||
|
|
||||||
2. Press `x` or `X` to disconnect all its JACK connections.
|
|
||||||
|
|
||||||
3. The plugin should no longer be connected to any looper ports; the audio should pass through unaffected.
|
|
||||||
|
|
||||||
### Test C4 – Unload a plugin
|
|
||||||
|
|
||||||
1. In rack view, select the plugin.
|
|
||||||
|
|
||||||
2. Press `d` or `D` to unload (remove) the plugin.
|
|
||||||
|
|
||||||
3. The plugin disappears from the rack list.
|
|
||||||
|
|
||||||
### Test C5 – Manual connection using colon commands
|
|
||||||
|
|
||||||
1. Set `from` and `to` ports as in Test C1.
|
|
||||||
|
|
||||||
2. Load a plugin without auto‑connection:
|
|
||||||
- Do **not** set `from`/`to`, or set them after loading.
|
|
||||||
- Use `addplugin` with only a path.
|
|
||||||
|
|
||||||
3. Manually connect ports in colon mode:
|
|
||||||
```
|
|
||||||
connect looper:output amsynth:in
|
|
||||||
```
|
|
||||||
The connection should be established.
|
|
||||||
|
|
||||||
4. Verify in `jack_lsp` that the ports are connected.
|
|
||||||
|
|
||||||
### Test C6 – Disconnect using colon commands
|
|
||||||
|
|
||||||
1. After a manual connection, disconnect using:
|
|
||||||
```
|
|
||||||
disconnect looper:output amsynth:in
|
|
||||||
```
|
|
||||||
|
|
||||||
2. The ports should be disconnected.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated:* 18 May 2026
|
|
||||||
@@ -2,7 +2,7 @@ CC ?= gcc
|
|||||||
CFLAGS ?= -Wall -Wextra -g -Isrc
|
CFLAGS ?= -Wall -Wextra -g -Isrc
|
||||||
LDFLAGS ?= -ljack -lm -lsndfile -lpthread
|
LDFLAGS ?= -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
|
||||||
OBJ = $(SRC:.c=.o)
|
OBJ = $(SRC:.c=.o)
|
||||||
|
|
||||||
looper: $(OBJ)
|
looper: $(OBJ)
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ void channel_add(jack_client_t *client, int idx) {
|
|||||||
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
||||||
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
||||||
|
|
||||||
/* Always register audio ports (needed for pass-through even for MIDI
|
/* Always register audio ports (needed for pass-through even for MIDI channels?) */
|
||||||
* channels?) */
|
|
||||||
channels[idx].audio_in = jack_port_register(
|
channels[idx].audio_in = jack_port_register(
|
||||||
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
channels[idx].audio_out = jack_port_register(
|
channels[idx].audio_out = jack_port_register(
|
||||||
@@ -34,10 +33,8 @@ 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), "channel%d_midi_in", next_channel_id);
|
||||||
next_channel_id);
|
snprintf(midi_out_name, sizeof(midi_out_name), "channel%d_midi_out", next_channel_id);
|
||||||
snprintf(midi_out_name, sizeof(midi_out_name), "channel%d_midi_out",
|
|
||||||
next_channel_id);
|
|
||||||
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(
|
||||||
|
|||||||
117
engine/src/channel.c~
Normal file
117
engine/src/channel.c~
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "channel.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
/* Helper: zero a scene and set its state to IDLE */
|
||||||
|
static void init_scene(scene_t *sc) {
|
||||||
|
memset(sc, 0, sizeof(scene_t));
|
||||||
|
atomic_store(&sc->state, STATE_IDLE);
|
||||||
|
atomic_store(&sc->prev_state, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
void channel_add(jack_client_t *client, int idx) {
|
||||||
|
char in_name[64], out_name[64];
|
||||||
|
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
||||||
|
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
||||||
|
|
||||||
|
channels[idx].audio_in = jack_port_register(
|
||||||
|
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
|
channels[idx].audio_out = jack_port_register(
|
||||||
|
client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
|
if (!channels[idx].audio_in || !channels[idx].audio_out) {
|
||||||
|
fprintf(stderr, "Failed to register ports for channel %d\n",
|
||||||
|
next_channel_id);
|
||||||
|
/* Do NOT mark channel active – process loop will skip it */
|
||||||
|
atomic_store(&channels[idx].active, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&channels[idx].active, 1);
|
||||||
|
atomic_store(&channels[idx].state, STATE_IDLE);
|
||||||
|
channels[idx].prev_state = -1;
|
||||||
|
channels[idx].loop_count = 0;
|
||||||
|
channels[idx].record_pos = 0;
|
||||||
|
channels[idx].playback_pos = 0;
|
||||||
|
channels[idx].save_ring = NULL;
|
||||||
|
|
||||||
|
next_channel_id++;
|
||||||
|
channel_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_remove(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
atomic_store(&channels[idx].active, 0);
|
||||||
|
channel_count--;
|
||||||
|
=======
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
atomic_store(&cur[idx].active, 0);
|
||||||
|
atomic_fetch_sub(&channel_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_add_scene(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
if (atomic_load(&cur[idx].scene_count) >= MAX_SCENES)
|
||||||
|
return;
|
||||||
|
int ns = atomic_load(&cur[idx].scene_count);
|
||||||
|
init_scene(&cur[idx].scenes[ns]);
|
||||||
|
atomic_fetch_add(&cur[idx].scene_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_remove_scene(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
int sc = atomic_load(&cur[idx].scene_count);
|
||||||
|
if (sc <= 1)
|
||||||
|
return;
|
||||||
|
int cs = atomic_load(&cur[idx].current_scene);
|
||||||
|
/* shift remaining scenes down (atomic copy of fields) */
|
||||||
|
for (int i = cs; i < sc - 1; i++) {
|
||||||
|
atomic_store(&cur[idx].scenes[i].loop_count,
|
||||||
|
atomic_load(&cur[idx].scenes[i + 1].loop_count));
|
||||||
|
atomic_store(&cur[idx].scenes[i].record_pos,
|
||||||
|
atomic_load(&cur[idx].scenes[i + 1].record_pos));
|
||||||
|
atomic_store(&cur[idx].scenes[i].playback_pos,
|
||||||
|
atomic_load(&cur[idx].scenes[i + 1].playback_pos));
|
||||||
|
atomic_store(&cur[idx].scenes[i].state,
|
||||||
|
atomic_load(&cur[idx].scenes[i + 1].state));
|
||||||
|
atomic_store(&cur[idx].scenes[i].prev_state,
|
||||||
|
atomic_load(&cur[idx].scenes[i + 1].prev_state));
|
||||||
|
/* copy loop data (may race with RT thread; acceptable for this release) */
|
||||||
|
memcpy(cur[idx].scenes[i].loop.audio_buffer,
|
||||||
|
cur[idx].scenes[i + 1].loop.audio_buffer,
|
||||||
|
LOOP_BUF_SIZE * sizeof(float));
|
||||||
|
}
|
||||||
|
atomic_fetch_sub(&cur[idx].scene_count, 1);
|
||||||
|
int new_sc = atomic_load(&cur[idx].scene_count);
|
||||||
|
if (cs >= new_sc)
|
||||||
|
atomic_store(&cur[idx].current_scene, new_sc - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_next_scene(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
int sc = atomic_load(&cur[idx].scene_count);
|
||||||
|
if (sc > 1) {
|
||||||
|
int cs = atomic_load(&cur[idx].current_scene);
|
||||||
|
atomic_store(&cur[idx].current_scene, (cs + 1) % sc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_prev_scene(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
int sc = atomic_load(&cur[idx].scene_count);
|
||||||
|
if (sc > 1) {
|
||||||
|
int cs = atomic_load(&cur[idx].current_scene);
|
||||||
|
atomic_store(&cur[idx].current_scene, (cs - 1 + sc) % sc);
|
||||||
|
}
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#include "log.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
static FILE *logfile = NULL;
|
|
||||||
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
|
|
||||||
void log_init(void) {
|
|
||||||
logfile = fopen("/tmp/looper.log", "a");
|
|
||||||
if (!logfile)
|
|
||||||
logfile = stderr;
|
|
||||||
setbuf(logfile, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void log_msg(const char *fmt, ...) {
|
|
||||||
if (!logfile) return;
|
|
||||||
pthread_mutex_lock(&log_mutex);
|
|
||||||
va_list args;
|
|
||||||
va_start(args, fmt);
|
|
||||||
vfprintf(logfile, fmt, args);
|
|
||||||
va_end(args);
|
|
||||||
fputc('\n', logfile);
|
|
||||||
pthread_mutex_unlock(&log_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void log_close(void) {
|
|
||||||
if (logfile && logfile != stderr)
|
|
||||||
fclose(logfile);
|
|
||||||
logfile = NULL;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#ifndef LOG_H
|
|
||||||
#define LOG_H
|
|
||||||
|
|
||||||
void log_init(void);
|
|
||||||
void log_msg(const char *fmt, ...);
|
|
||||||
void log_close(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
#include "midi.h"
|
#include "midi.h"
|
||||||
#include "pipe.h"
|
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
#include "wav.h"
|
#include "wav.h"
|
||||||
|
#include "pipe.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
@@ -38,23 +38,13 @@ static void looper_write_status(void) {
|
|||||||
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
|
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
|
||||||
const char *state_str;
|
const char *state_str;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE: state_str = "IDLE"; break;
|
||||||
state_str = "IDLE";
|
case STATE_RECORD: state_str = "RECORD"; break;
|
||||||
break;
|
case STATE_LOOPING:state_str = "LOOPING";break;
|
||||||
case STATE_RECORD:
|
case STATE_PAUSED: state_str = "PAUSED"; break;
|
||||||
state_str = "RECORD";
|
default: state_str = "UNKNOWN";
|
||||||
break;
|
|
||||||
case STATE_LOOPING:
|
|
||||||
state_str = "LOOPING";
|
|
||||||
break;
|
|
||||||
case STATE_PAUSED:
|
|
||||||
state_str = "PAUSED";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state_str = "UNKNOWN";
|
|
||||||
}
|
}
|
||||||
int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx,
|
int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str);
|
||||||
state_str);
|
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
int ret = write(status_fd, buf, n);
|
int ret = write(status_fd, buf, n);
|
||||||
(void)ret;
|
(void)ret;
|
||||||
@@ -79,34 +69,34 @@ 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;
|
||||||
|
|
||||||
/* sample rate holder */
|
/* writer thread function and sample rate holder */
|
||||||
|
static void *writer_thread(void *arg);
|
||||||
static int global_sample_rate = 0;
|
static int global_sample_rate = 0;
|
||||||
|
|
||||||
/* execute a single command (called from looper_process_commands) */
|
/* execute a single command (called from looper_process_commands) */
|
||||||
static void exec_command(command_t cmd, jack_client_t *client) {
|
static void exec_command(command_t cmd, jack_client_t *client) {
|
||||||
int ch = cmd.channel;
|
int ch = cmd.channel;
|
||||||
if (ch < 0 || ch >= MAX_CHANNELS)
|
if (ch < 0) ch = 0;
|
||||||
ch = 0;
|
|
||||||
|
|
||||||
switch (cmd.type) {
|
switch (cmd.type) {
|
||||||
case CMD_CYCLE: {
|
case CMD_CYCLE: {
|
||||||
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];
|
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
|
||||||
int state = atomic_load(&sc_ptr->state);
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE:
|
||||||
atomic_store(&sc_ptr->state, STATE_RECORD);
|
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_RECORD);
|
||||||
break;
|
break;
|
||||||
case STATE_RECORD:
|
case STATE_RECORD:
|
||||||
atomic_store(&sc_ptr->state, STATE_LOOPING);
|
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_LOOPING);
|
||||||
break;
|
break;
|
||||||
case STATE_LOOPING:
|
case STATE_LOOPING:
|
||||||
atomic_store(&sc_ptr->state, STATE_PAUSED);
|
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_PAUSED);
|
||||||
break;
|
break;
|
||||||
case STATE_PAUSED:
|
case STATE_PAUSED:
|
||||||
atomic_store(&sc_ptr->state, STATE_LOOPING);
|
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_LOOPING);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
atomic_store(&channels[ch].scenes[sc_idx].prev_state, -1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CMD_STOP:
|
case CMD_STOP:
|
||||||
@@ -227,8 +217,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
/* MIDI channel handling */
|
/* MIDI channel handling */
|
||||||
void *midi_in_buf = jack_port_get_buffer(channels[c].midi_in, nframes);
|
void *midi_in_buf = jack_port_get_buffer(channels[c].midi_in, nframes);
|
||||||
void *midi_out_buf = jack_port_get_buffer(channels[c].midi_out, nframes);
|
void *midi_out_buf = jack_port_get_buffer(channels[c].midi_out, nframes);
|
||||||
if (!midi_out_buf)
|
if (!midi_out_buf) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_RECORD: {
|
case STATE_RECORD: {
|
||||||
@@ -243,8 +232,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
sc->loop.midi_events[rp].timestamp = ev.time;
|
sc->loop.midi_events[rp].timestamp = ev.time;
|
||||||
sc->loop.midi_events[rp].status = ev.buffer[0];
|
sc->loop.midi_events[rp].status = ev.buffer[0];
|
||||||
sc->loop.midi_events[rp].note = (ev.size > 1) ? ev.buffer[1] : 0;
|
sc->loop.midi_events[rp].note = (ev.size > 1) ? ev.buffer[1] : 0;
|
||||||
sc->loop.midi_events[rp].velocity =
|
sc->loop.midi_events[rp].velocity = (ev.size > 2) ? ev.buffer[2] : 0;
|
||||||
(ev.size > 2) ? ev.buffer[2] : 0;
|
|
||||||
atomic_store(&sc->record_pos, rp + 1);
|
atomic_store(&sc->record_pos, rp + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,8 +286,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
jack_default_audio_sample_t *out =
|
jack_default_audio_sample_t *out =
|
||||||
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
channels[c].audio_out, nframes);
|
channels[c].audio_out, nframes);
|
||||||
if (!out)
|
if (!out) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_RECORD:
|
case STATE_RECORD:
|
||||||
@@ -410,6 +397,7 @@ void jack_shutdown_cb(void *arg) {
|
|||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
* looper initialisation
|
* looper initialisation
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
@@ -420,8 +408,7 @@ int looper_init(jack_client_t *client) {
|
|||||||
/* 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);
|
||||||
if (status_fd < 0) {
|
if (status_fd < 0) {
|
||||||
perror("open status FIFO");
|
perror("open status FIFO");
|
||||||
@@ -434,6 +421,7 @@ int looper_init(jack_client_t *client) {
|
|||||||
/* start the FIFO reader thread */
|
/* start the FIFO reader thread */
|
||||||
pipe_start_reader();
|
pipe_start_reader();
|
||||||
|
|
||||||
|
|
||||||
/* channel 0 */
|
/* channel 0 */
|
||||||
channels[0].active = 1;
|
channels[0].active = 1;
|
||||||
channels[0].type = CHANNEL_AUDIO; /* default */
|
channels[0].type = CHANNEL_AUDIO; /* default */
|
||||||
@@ -465,13 +453,56 @@ int looper_init(jack_client_t *client) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Give JACK time to register the ports before clients connect */
|
return 0;
|
||||||
{
|
|
||||||
struct timespec req = {.tv_sec = 0, .tv_nsec = 500000000};
|
|
||||||
nanosleep(&req, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
/* ----------------------------------------------------------------
|
||||||
|
* writer thread – consumes the save ring and writes WAV file
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
static void *writer_thread(void *arg) {
|
||||||
|
struct channel_t *ch = (struct channel_t *)arg;
|
||||||
|
int sc_idx = atomic_load(&ch->current_scene);
|
||||||
|
scene_t *sc = &ch->scenes[sc_idx];
|
||||||
|
RingBuf *ring = (RingBuf *)ch->save_ring;
|
||||||
|
if (!ring)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
static const char *path = "save.wav";
|
||||||
|
unsigned sr = (unsigned)global_sample_rate;
|
||||||
|
if (sr == 0)
|
||||||
|
sr = 48000;
|
||||||
|
|
||||||
|
int lc = atomic_load(&sc->loop_count);
|
||||||
|
float *outbuf = malloc((size_t)lc * sizeof(float));
|
||||||
|
if (!outbuf) {
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
ch->save_ring = NULL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t collected = 0;
|
||||||
|
size_t want = (size_t)lc;
|
||||||
|
while (collected < want) {
|
||||||
|
size_t got = ring_read(ring, outbuf + collected, want - collected);
|
||||||
|
collected += got;
|
||||||
|
if (got == 0) {
|
||||||
|
struct timespec req = {.tv_sec = 0, .tv_nsec = 10000000};
|
||||||
|
nanosleep(&req, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wav_write(path, outbuf, (unsigned)lc, sr);
|
||||||
|
free(outbuf);
|
||||||
|
|
||||||
|
/* Signal the RT thread to stop writing */
|
||||||
|
atomic_store_explicit(&ch->save_complete, 1, memory_order_release);
|
||||||
|
/* Wait for the RT thread to see the flag (one audio period) */
|
||||||
|
struct timespec req = { .tv_sec = 0, .tv_nsec = 10000000 }; /* 10ms */
|
||||||
|
nanosleep(&req, NULL);
|
||||||
|
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
atomic_store_explicit(&ch->save_ring, NULL, memory_order_release);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -547,33 +578,25 @@ void looper_process_commands(jack_client_t *client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- save command (synchronous) ---------- */
|
/* ---------- save command (writer thread) ---------- */
|
||||||
if (atomic_exchange(&cmd_save, 0)) {
|
if (atomic_exchange(&cmd_save, 0)) {
|
||||||
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) {
|
if (atomic_load(&sc->state) == STATE_LOOPING && lc > 0 &&
|
||||||
/* Deactivate channel to prevent RT thread from reading the buffer */
|
channels[0].save_ring == NULL) {
|
||||||
int was_active = atomic_load(&channels[0].active);
|
RingBuf *ring = (RingBuf *)malloc(sizeof(RingBuf));
|
||||||
if (was_active) {
|
if (ring) {
|
||||||
atomic_store(&channels[0].active, 0);
|
size_t sz = (size_t)lc * 2;
|
||||||
struct timespec req = {.tv_sec = 0, .tv_nsec = 500000000}; /* 500 ms */
|
if (ring_init(ring, sz) == 0) {
|
||||||
nanosleep(&req, NULL);
|
atomic_store_explicit(&channels[0].save_ring, (_Atomic RingBuf *)ring,
|
||||||
|
memory_order_release);
|
||||||
|
pthread_t th;
|
||||||
|
pthread_create(&th, NULL, writer_thread, &channels[0]);
|
||||||
|
pthread_detach(th);
|
||||||
|
} else {
|
||||||
|
free(ring);
|
||||||
}
|
}
|
||||||
/* Now safe to copy the loop buffer */
|
|
||||||
float *data = malloc((size_t)lc * sizeof(float));
|
|
||||||
if (data) {
|
|
||||||
memcpy(data, sc->loop.audio_buffer, (size_t)lc * sizeof(float));
|
|
||||||
unsigned sr = (unsigned)global_sample_rate;
|
|
||||||
if (sr == 0) sr = 48000;
|
|
||||||
wav_write("save.wav", data, (unsigned)lc, sr);
|
|
||||||
free(data);
|
|
||||||
}
|
|
||||||
/* Reactivate channel – use a shorter sleep to reduce xrun risk */
|
|
||||||
if (was_active) {
|
|
||||||
struct timespec req = {.tv_sec = 0, .tv_nsec = 200000000}; /* 200 ms */
|
|
||||||
nanosleep(&req, NULL);
|
|
||||||
atomic_store(&channels[0].active, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
675
engine/src/looper.c~
Normal file
675
engine/src/looper.c~
Normal file
@@ -0,0 +1,675 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "looper.h"
|
||||||
|
#include "channel.h"
|
||||||
|
#include "midi.h"
|
||||||
|
<<<<<<< HEAD
|
||||||
|
#include "wav.h"
|
||||||
|
#include "ringbuffer.h"
|
||||||
|
#include "pipe.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
=======
|
||||||
|
#include "queue.h"
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
#include <time.h>
|
||||||
|
#include "queue.h"
|
||||||
|
#include "command.h"
|
||||||
|
|
||||||
|
/* Global command queues */
|
||||||
|
spsc_queue_t cmd_queue;
|
||||||
|
spsc_queue_t cmd_queue_main_midi;
|
||||||
|
spsc_queue_t cmd_queue_main_fifo;
|
||||||
|
=======
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
|
||||||
|
#define STATUS_FIFO "/tmp/looper_status"
|
||||||
|
|
||||||
|
/* writer status fd */
|
||||||
|
static int status_fd = -1;
|
||||||
|
|
||||||
|
static void looper_write_status(void) {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
if (status_fd < 0)
|
||||||
|
return;
|
||||||
|
char buf[256];
|
||||||
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||||
|
if (!atomic_load(&channels[ch].active))
|
||||||
|
continue;
|
||||||
|
int state_val = atomic_load(&channels[ch].state);
|
||||||
|
const char *state_str;
|
||||||
|
switch (state_val) {
|
||||||
|
case STATE_IDLE: state_str = "IDLE"; break;
|
||||||
|
case STATE_RECORD: state_str = "RECORD"; break;
|
||||||
|
case STATE_LOOPING: state_str = "LOOPING"; break;
|
||||||
|
case STATE_PAUSED: state_str = "PAUSED"; break;
|
||||||
|
default: state_str = "UNKNOWN";
|
||||||
|
}
|
||||||
|
int n = snprintf(buf, sizeof(buf),
|
||||||
|
"CH=%d SC=%d STATE=%s\n",
|
||||||
|
ch, 0, state_str);
|
||||||
|
if (n > 0) {
|
||||||
|
int ret = write(status_fd, buf, n);
|
||||||
|
(void)ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
=======
|
||||||
|
int fd = open(STATUS_FIFO, O_WRONLY | O_NONBLOCK);
|
||||||
|
if (fd < 0)
|
||||||
|
return;
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
char buf[256];
|
||||||
|
for (int ch = 0; ch < cap; ch++) {
|
||||||
|
if (!atomic_load(&cur[ch].active))
|
||||||
|
continue;
|
||||||
|
int sc_idx = atomic_load(&cur[ch].current_scene);
|
||||||
|
int state = atomic_load(&cur[ch].scenes[sc_idx].state);
|
||||||
|
const char *state_str;
|
||||||
|
switch (state) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
state_str = "IDLE";
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
state_str = "RECORD";
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
state_str = "LOOPING";
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
state_str = "PAUSED";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state_str = "UNKNOWN";
|
||||||
|
}
|
||||||
|
int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx,
|
||||||
|
state_str);
|
||||||
|
if (n > 0) {
|
||||||
|
int ret = write(fd, buf, n);
|
||||||
|
(void)ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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) */
|
||||||
|
static int pending_unregister_idx = -1;
|
||||||
|
|
||||||
|
/* writer thread function and sample rate holder */
|
||||||
|
static void *writer_thread(void *arg);
|
||||||
|
static int global_sample_rate = 0;
|
||||||
|
|
||||||
|
/* execute a single command (called from looper_process_commands) */
|
||||||
|
static void exec_command(command_t cmd, jack_client_t *client) {
|
||||||
|
int ch = cmd.channel;
|
||||||
|
if (ch < 0) ch = 0;
|
||||||
|
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_CYCLE: {
|
||||||
|
int state = atomic_load(&channels[ch].state);
|
||||||
|
switch (state) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
atomic_store(&channels[ch].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&channels[ch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
atomic_store(&channels[ch].state, STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
atomic_store(&channels[ch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
atomic_store(&channels[ch].prev_state, -1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_STOP:
|
||||||
|
atomic_store(&channels[ch].state, STATE_IDLE);
|
||||||
|
atomic_store(&channels[ch].prev_state, -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_ADD_CHANNEL:
|
||||||
|
case CMD_ADD_MIDI_CHANNEL: {
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < MAX_CHANNELS; idx++)
|
||||||
|
if (!channels[idx].active)
|
||||||
|
break;
|
||||||
|
if (idx < MAX_CHANNELS)
|
||||||
|
channel_add(client, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CMD_REMOVE_CHANNEL: {
|
||||||
|
int remove_idx = -1;
|
||||||
|
for (int idx = 1; idx < MAX_CHANNELS; idx++)
|
||||||
|
if (channels[idx].active)
|
||||||
|
remove_idx = idx;
|
||||||
|
if (remove_idx != -1) {
|
||||||
|
channel_remove(client, remove_idx);
|
||||||
|
pending_unregister_idx = remove_idx;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CMD_BIND_CHANNEL:
|
||||||
|
atomic_store(&bind_channel, cmd.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_UNBIND:
|
||||||
|
atomic_store(&bind_channel, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_LOAD:
|
||||||
|
atomic_store(&cmd_load, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_SAVE:
|
||||||
|
atomic_store(&cmd_save, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_ADD_SCENE:
|
||||||
|
case CMD_REMOVE_SCENE:
|
||||||
|
case CMD_NEXT_SCENE:
|
||||||
|
case CMD_PREV_SCENE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* process callback
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
int process_callback(jack_nframes_t nframes, void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
if (midi_control_port) {
|
||||||
|
void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes);
|
||||||
|
if (midi_ctrl_buf) {
|
||||||
|
midi_handle_events(midi_ctrl_buf, nframes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* process each active channel */
|
||||||
|
for (int c = 0; c < MAX_CHANNELS; c++) {
|
||||||
|
if (!atomic_load(&channels[c].active))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* Guard against NULL ports (e.g. if port registration failed) */
|
||||||
|
if (!channels[c].audio_in || !channels[c].audio_out) {
|
||||||
|
fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jack_default_audio_sample_t *in =
|
||||||
|
(const jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
|
channels[c].audio_in, nframes);
|
||||||
|
jack_default_audio_sample_t *out =
|
||||||
|
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
|
channels[c].audio_out, nframes);
|
||||||
|
if (!out)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int state = atomic_load(&channels[c].state);
|
||||||
|
|
||||||
|
if (state != atomic_load(&channels[c].prev_state)) {
|
||||||
|
switch (state) {
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&channels[c].record_pos, 0);
|
||||||
|
atomic_store(&channels[c].loop_count, 0);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
if (atomic_load(&channels[c].prev_state) == STATE_RECORD &&
|
||||||
|
atomic_load(&channels[c].record_pos) > 0)
|
||||||
|
atomic_store(&channels[c].loop_count,
|
||||||
|
atomic_load(&channels[c].record_pos));
|
||||||
|
atomic_store(&channels[c].playback_pos, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
jack_nframes_t i;
|
||||||
|
switch (state) {
|
||||||
|
case STATE_RECORD:
|
||||||
|
if (in) {
|
||||||
|
float *f_out = (float *)out;
|
||||||
|
const float *f_in = (const float *)in;
|
||||||
|
for (i = 0; i < nframes; i++) {
|
||||||
|
int rp = atomic_fetch_add(&channels[c].record_pos, 1);
|
||||||
|
if (rp < LOOP_BUF_SIZE)
|
||||||
|
channels[c].loop_buffer[rp] = f_in[i];
|
||||||
|
f_out[i] = f_in[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
=======
|
||||||
|
if (active_channels[c].type == CHANNEL_MIDI) {
|
||||||
|
/* MIDI channel handling */
|
||||||
|
switch (state) {
|
||||||
|
case STATE_RECORD: {
|
||||||
|
void *midi_in_buf =
|
||||||
|
jack_port_get_buffer(active_channels[c].midi_in, nframes);
|
||||||
|
if (midi_in_buf) {
|
||||||
|
jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf);
|
||||||
|
jack_midi_event_t ev;
|
||||||
|
for (jack_nframes_t j = 0; j < nevents; j++) {
|
||||||
|
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
|
||||||
|
continue;
|
||||||
|
int rp = atomic_load(&sc->record_pos);
|
||||||
|
if (rp < MAX_MIDI_EVENTS) {
|
||||||
|
sc->loop.midi_events[rp].timestamp = ev.time;
|
||||||
|
sc->loop.midi_events[rp].status = ev.buffer[0];
|
||||||
|
sc->loop.midi_events[rp].note = (ev.size > 1) ? ev.buffer[1] : 0;
|
||||||
|
sc->loop.midi_events[rp].velocity =
|
||||||
|
(ev.size > 2) ? ev.buffer[2] : 0;
|
||||||
|
atomic_store(&sc->record_pos, rp + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* forward incoming MIDI to output during record */
|
||||||
|
void *midi_out_buf =
|
||||||
|
jack_port_get_buffer(active_channels[c].midi_out, nframes);
|
||||||
|
if (midi_out_buf) {
|
||||||
|
jack_midi_clear_buffer(midi_out_buf);
|
||||||
|
for (jack_nframes_t j = 0; j < nevents; j++) {
|
||||||
|
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
|
||||||
|
continue;
|
||||||
|
jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case STATE_LOOPING: {
|
||||||
|
void *midi_out_buf =
|
||||||
|
jack_port_get_buffer(active_channels[c].midi_out, nframes);
|
||||||
|
if (midi_out_buf) {
|
||||||
|
jack_midi_clear_buffer(midi_out_buf);
|
||||||
|
int cnt = atomic_load(&sc->loop_count);
|
||||||
|
if (cnt > 0) {
|
||||||
|
for (int e = 0; e < cnt; e++) {
|
||||||
|
unsigned char msg[3];
|
||||||
|
msg[0] = sc->loop.midi_events[e].status;
|
||||||
|
msg[1] = sc->loop.midi_events[e].note;
|
||||||
|
msg[2] = sc->loop.midi_events[e].velocity;
|
||||||
|
jack_midi_event_write(midi_out_buf, 0, msg, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case STATE_PAUSED:
|
||||||
|
/* no output */
|
||||||
|
break;
|
||||||
|
default: /* IDLE */
|
||||||
|
{
|
||||||
|
void *midi_in_buf =
|
||||||
|
jack_port_get_buffer(active_channels[c].midi_in, nframes);
|
||||||
|
void *midi_out_buf =
|
||||||
|
jack_port_get_buffer(active_channels[c].midi_out, nframes);
|
||||||
|
if (midi_in_buf && midi_out_buf) {
|
||||||
|
jack_midi_clear_buffer(midi_out_buf);
|
||||||
|
jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf);
|
||||||
|
jack_midi_event_t ev;
|
||||||
|
for (jack_nframes_t j = 0; j < nevents; j++) {
|
||||||
|
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
|
||||||
|
continue;
|
||||||
|
jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (state == STATE_LOOPING) {
|
||||||
|
atomic_store(&sc->loop_count, atomic_load(&sc->record_pos));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* audio channel handling */
|
||||||
|
jack_nframes_t i;
|
||||||
|
switch (state) {
|
||||||
|
case STATE_RECORD:
|
||||||
|
if (in) {
|
||||||
|
float *f_out = (float *)out;
|
||||||
|
const float *f_in = (const float *)in;
|
||||||
|
for (i = 0; i < nframes; i++) {
|
||||||
|
int rp = atomic_load(&sc->record_pos);
|
||||||
|
if (rp < LOOP_BUF_SIZE) {
|
||||||
|
sc->loop.audio_buffer[rp] = f_in[i];
|
||||||
|
atomic_store(&sc->record_pos, rp + 1);
|
||||||
|
}
|
||||||
|
f_out[i] = f_in[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_LOOPING: {
|
||||||
|
int loop_cnt = atomic_load(&sc->loop_count);
|
||||||
|
if (loop_cnt > 0) {
|
||||||
|
float *outf = (float *)out;
|
||||||
|
int pp = atomic_load(&sc->playback_pos);
|
||||||
|
for (i = 0; i < nframes; i++) {
|
||||||
|
outf[i] = sc->loop.audio_buffer[pp];
|
||||||
|
pp = (pp + 1) % loop_cnt;
|
||||||
|
}
|
||||||
|
atomic_store(&sc->playback_pos, pp);
|
||||||
|
} else {
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case STATE_PAUSED:
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_LOOPING:
|
||||||
|
int lc = atomic_load(&channels[c].loop_count);
|
||||||
|
if (lc > 0) {
|
||||||
|
float *outf = (float *)out;
|
||||||
|
for (i = 0; i < nframes; i++) {
|
||||||
|
int pp = atomic_load(&channels[c].playback_pos);
|
||||||
|
outf[i] = channels[c].loop_buffer[pp];
|
||||||
|
atomic_store(&channels[c].playback_pos, (pp + 1) % lc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_PAUSED:
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: /* IDLE */
|
||||||
|
if (in) {
|
||||||
|
memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
} else {
|
||||||
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push loop output into save ring if saving (atomic load)
|
||||||
|
RingBuf *r = (RingBuf *)atomic_load_explicit(&channels[c].save_ring,
|
||||||
|
memory_order_acquire);
|
||||||
|
if (r != NULL) {
|
||||||
|
if (state == STATE_LOOPING && atomic_load(&channels[c].loop_count) > 0) {
|
||||||
|
const float *outf = (const float *)out;
|
||||||
|
ring_write(r, outf, nframes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&channels[c].prev_state, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MIDI clock events – affect channel 0 only */
|
||||||
|
if (midi_clock_port) {
|
||||||
|
void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes);
|
||||||
|
if (midi_clock_buf) {
|
||||||
|
jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf);
|
||||||
|
jack_midi_event_t cev;
|
||||||
|
for (jack_nframes_t j = 0; j < n_clock_events; j++) {
|
||||||
|
if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0)
|
||||||
|
continue;
|
||||||
|
if (cev.size >= 1) {
|
||||||
|
unsigned char msg = cev.buffer[0];
|
||||||
|
switch (msg) {
|
||||||
|
case 0xFA: {
|
||||||
|
int s = atomic_load(&channels[0].state);
|
||||||
|
if (s == STATE_IDLE)
|
||||||
|
atomic_store(&channels[0].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xFC:
|
||||||
|
atomic_store(&channels[0].state, STATE_IDLE);
|
||||||
|
break;
|
||||||
|
case 0xFB: {
|
||||||
|
int s = atomic_load(&channels[0].state);
|
||||||
|
if (s == STATE_PAUSED)
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* shutdown callback
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
void jack_shutdown_cb(void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
fprintf(stderr, "JACK shutdown\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* looper initialisation
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
int looper_init(jack_client_t *client) {
|
||||||
|
/* store sample rate for writer thread */
|
||||||
|
global_sample_rate = jack_get_sample_rate(client);
|
||||||
|
|
||||||
|
/* create status FIFO (ignore if already exists) */
|
||||||
|
mkfifo(STATUS_FIFO, 0666);
|
||||||
|
|
||||||
|
/* open the status FIFO for reading+writing so writes work even without reader */
|
||||||
|
status_fd = open(STATUS_FIFO, O_RDWR);
|
||||||
|
if (status_fd < 0) {
|
||||||
|
perror("open status FIFO");
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_init(&cmd_queue);
|
||||||
|
queue_init(&cmd_queue_main_midi);
|
||||||
|
queue_init(&cmd_queue_main_fifo);
|
||||||
|
|
||||||
|
/* start the FIFO reader thread */
|
||||||
|
pipe_start_reader();
|
||||||
|
|
||||||
|
|
||||||
|
/* channel 0 */
|
||||||
|
channels[0].active = 1;
|
||||||
|
atomic_store(&channels[0].state, STATE_IDLE);
|
||||||
|
atomic_store(&channels[0].prev_state, -1);
|
||||||
|
channels[0].loop_count = 0;
|
||||||
|
atomic_store(&channels[0].record_pos, 0);
|
||||||
|
atomic_store(&channels[0].playback_pos, 0);
|
||||||
|
atomic_store_explicit(&channels[0].save_ring, NULL, memory_order_release);
|
||||||
|
|
||||||
|
channels[0].audio_in = jack_port_register(
|
||||||
|
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
|
channels[0].audio_out = jack_port_register(
|
||||||
|
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
|
if (!channels[0].audio_in || !channels[0].audio_out) {
|
||||||
|
fprintf(stderr, "Could not create audio ports for channel 0\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
channel_count = 1;
|
||||||
|
|
||||||
|
midi_control_port = jack_port_register(
|
||||||
|
client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
|
||||||
|
midi_clock_port = jack_port_register(client, "clock", JACK_DEFAULT_MIDI_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!midi_control_port || !midi_clock_port) {
|
||||||
|
fprintf(stderr, "Could not create MIDI ports\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* writer thread – consumes the save ring and writes WAV file
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
static void *writer_thread(void *arg) {
|
||||||
|
struct channel_t *ch = (struct channel_t *)arg;
|
||||||
|
RingBuf *ring = (RingBuf *)ch->save_ring;
|
||||||
|
if (!ring)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
static const char *path = "save.wav";
|
||||||
|
unsigned sr = (unsigned)global_sample_rate;
|
||||||
|
if (sr == 0)
|
||||||
|
sr = 48000;
|
||||||
|
|
||||||
|
int lc = atomic_load(&ch->loop_count);
|
||||||
|
float *outbuf = malloc((size_t)lc * sizeof(float));
|
||||||
|
if (!outbuf) {
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
ch->save_ring = NULL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t collected = 0;
|
||||||
|
size_t want = (size_t)lc;
|
||||||
|
while (collected < want) {
|
||||||
|
size_t got = ring_read(ring, outbuf + collected, want - collected);
|
||||||
|
collected += got;
|
||||||
|
if (got == 0) {
|
||||||
|
struct timespec req = {.tv_sec = 0, .tv_nsec = 10000000};
|
||||||
|
nanosleep(&req, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wav_write(path, outbuf, (unsigned)lc, sr);
|
||||||
|
free(outbuf);
|
||||||
|
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
atomic_store_explicit(&ch->save_ring, NULL, memory_order_release);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* main‑loop command processing
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
void looper_process_commands(jack_client_t *client) {
|
||||||
|
/* process commands from the three queues FIRST */
|
||||||
|
command_t cmd;
|
||||||
|
while (queue_pop(&cmd_queue, &cmd))
|
||||||
|
exec_command(cmd, client);
|
||||||
|
while (queue_pop(&cmd_queue_main_midi, &cmd))
|
||||||
|
exec_command(cmd, client);
|
||||||
|
while (queue_pop(&cmd_queue_main_fifo, &cmd))
|
||||||
|
exec_command(cmd, client);
|
||||||
|
|
||||||
|
/* Unregister any ports that were marked for deferred removal.
|
||||||
|
By now the real‑time thread has had at least one full cycle
|
||||||
|
to see the `active = 0` store. */
|
||||||
|
if (pending_unregister_idx != -1) {
|
||||||
|
int idx = pending_unregister_idx;
|
||||||
|
if (channels[idx].audio_in)
|
||||||
|
jack_port_unregister(client, channels[idx].audio_in);
|
||||||
|
if (channels[idx].audio_out)
|
||||||
|
jack_port_unregister(client, channels[idx].audio_out);
|
||||||
|
pending_unregister_idx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- add channel ---------- */
|
||||||
|
if (atomic_exchange(&cmd_add, 0)) {
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < MAX_CHANNELS; idx++)
|
||||||
|
if (!channels[idx].active)
|
||||||
|
break;
|
||||||
|
if (idx < MAX_CHANNELS) {
|
||||||
|
channel_add(client, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- remove channel ---------- */
|
||||||
|
if (atomic_exchange(&cmd_remove, 0)) {
|
||||||
|
int remove_idx = -1;
|
||||||
|
for (int idx = 1; idx < MAX_CHANNELS; idx++)
|
||||||
|
if (channels[idx].active)
|
||||||
|
remove_idx = idx;
|
||||||
|
if (remove_idx != -1) {
|
||||||
|
/* Mark inactive now; ports will be unregistered next round */
|
||||||
|
channel_remove(client, remove_idx);
|
||||||
|
pending_unregister_idx = remove_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- load command ---------- */
|
||||||
|
if (atomic_exchange(&cmd_load, 0)) {
|
||||||
|
float *buf = NULL;
|
||||||
|
unsigned frames = 0;
|
||||||
|
printf("LOAD: wav_read called\n");
|
||||||
|
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
|
||||||
|
printf("LOAD: success, frames=%u\n", frames);
|
||||||
|
if (frames > LOOP_BUF_SIZE)
|
||||||
|
frames = LOOP_BUF_SIZE;
|
||||||
|
memcpy(channels[0].loop_buffer, buf, frames * sizeof(float));
|
||||||
|
atomic_store(&channels[0].loop_count, (int)frames);
|
||||||
|
atomic_store(&channels[0].record_pos, 0);
|
||||||
|
atomic_store(&channels[0].playback_pos, 0);
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
atomic_store(&channels[0].prev_state, -1);
|
||||||
|
free(buf);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Failed to load loop.wav\n");
|
||||||
|
printf("LOAD: FAILED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- save command (writer thread) ---------- */
|
||||||
|
if (atomic_exchange(&cmd_save, 0)) {
|
||||||
|
int lc = atomic_load(&channels[0].loop_count);
|
||||||
|
if (atomic_load(&channels[0].state) == STATE_LOOPING && lc > 0 &&
|
||||||
|
channels[0].save_ring == NULL) {
|
||||||
|
RingBuf *ring = (RingBuf *)malloc(sizeof(RingBuf));
|
||||||
|
if (ring) {
|
||||||
|
size_t sz = (size_t)lc * 2;
|
||||||
|
if (ring_init(ring, sz) == 0) {
|
||||||
|
atomic_store_explicit(&channels[0].save_ring, (_Atomic RingBuf *)ring,
|
||||||
|
memory_order_release);
|
||||||
|
pthread_t th;
|
||||||
|
pthread_create(&th, NULL, writer_thread, &channels[0]);
|
||||||
|
pthread_detach(th);
|
||||||
|
} else {
|
||||||
|
free(ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* write current state to status FIFO */
|
||||||
|
looper_write_status();
|
||||||
|
}
|
||||||
@@ -1,29 +1,23 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "log.h"
|
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <time.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
(void)argc;
|
(void)argc;
|
||||||
(void)argv;
|
(void)argv;
|
||||||
|
|
||||||
log_init();
|
|
||||||
log_msg("looper engine starting");
|
|
||||||
|
|
||||||
const char *client_name = "looper";
|
const char *client_name = "looper";
|
||||||
jack_options_t options = JackNullOption;
|
jack_options_t options = JackNullOption;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
|
|
||||||
jack_client_t *client = jack_client_open(client_name, options, &status);
|
jack_client_t *client = jack_client_open(client_name, options, &status);
|
||||||
if (client == NULL) {
|
if (client == NULL) {
|
||||||
log_msg("jack_client_open() failed, status = 0x%2.0x", status);
|
fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status);
|
||||||
if (status & JackServerFailed)
|
if (status & JackServerFailed)
|
||||||
log_msg("Unable to connect to JACK server");
|
fprintf(stderr, "Unable to connect to JACK server\n");
|
||||||
log_close();
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,30 +28,24 @@ int main(int argc, char *argv[]) {
|
|||||||
jack_on_shutdown(client, jack_shutdown_cb, NULL);
|
jack_on_shutdown(client, jack_shutdown_cb, NULL);
|
||||||
|
|
||||||
if (looper_init(client) != 0) {
|
if (looper_init(client) != 0) {
|
||||||
log_msg("looper initialisation failed");
|
fprintf(stderr, "looper initialisation failed\n");
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
log_close();
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
log_msg("Cannot activate client");
|
fprintf(stderr, "Cannot activate client\n");
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
log_close();
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_msg("looper running (client name '%s')", client_name);
|
fprintf(stderr, "looper running (client name '%s')\n", client_name);
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
looper_process_commands(client);
|
looper_process_commands(client);
|
||||||
{
|
{ struct timespec ts = { .tv_sec = 0, .tv_nsec = 50000000 }; nanosleep(&ts, NULL); } /* check commands every 50 ms */
|
||||||
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
|
||||||
nanosleep(&ts, NULL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
log_close();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,20 +57,16 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
|||||||
int cur = atomic_load(&channels[bch].scenes[sc_idx].state);
|
int cur = atomic_load(&channels[bch].scenes[sc_idx].state);
|
||||||
switch (cur) {
|
switch (cur) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE:
|
||||||
atomic_store(&channels[bch].scenes[sc_idx].state,
|
atomic_store(&channels[bch].scenes[sc_idx].state, STATE_RECORD);
|
||||||
STATE_RECORD);
|
|
||||||
break;
|
break;
|
||||||
case STATE_RECORD:
|
case STATE_RECORD:
|
||||||
atomic_store(&channels[bch].scenes[sc_idx].state,
|
atomic_store(&channels[bch].scenes[sc_idx].state, STATE_LOOPING);
|
||||||
STATE_LOOPING);
|
|
||||||
break;
|
break;
|
||||||
case STATE_LOOPING:
|
case STATE_LOOPING:
|
||||||
atomic_store(&channels[bch].scenes[sc_idx].state,
|
atomic_store(&channels[bch].scenes[sc_idx].state, STATE_PAUSED);
|
||||||
STATE_PAUSED);
|
|
||||||
break;
|
break;
|
||||||
case STATE_PAUSED:
|
case STATE_PAUSED:
|
||||||
atomic_store(&channels[bch].scenes[sc_idx].state,
|
atomic_store(&channels[bch].scenes[sc_idx].state, STATE_LOOPING);
|
||||||
STATE_LOOPING);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
154
engine/src/midi.c~
Normal file
154
engine/src/midi.c~
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "midi.h"
|
||||||
|
#include "channel.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
|
extern atomic_int control_key_active;
|
||||||
|
extern atomic_int cmd_add;
|
||||||
|
extern atomic_int cmd_remove;
|
||||||
|
extern atomic_int cmd_load;
|
||||||
|
extern atomic_int cmd_save;
|
||||||
|
extern atomic_int bind_channel;
|
||||||
|
|
||||||
|
void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
||||||
|
(void)nframes;
|
||||||
|
jack_nframes_t nevents = jack_midi_get_event_count(port_buffer);
|
||||||
|
jack_midi_event_t ev;
|
||||||
|
|
||||||
|
for (jack_nframes_t i = 0; i < nevents; i++) {
|
||||||
|
if (jack_midi_event_get(&ev, port_buffer, i) != 0)
|
||||||
|
continue;
|
||||||
|
if (ev.size < 3)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
unsigned char status = ev.buffer[0];
|
||||||
|
unsigned char note = ev.buffer[1];
|
||||||
|
unsigned char vel = ev.buffer[2];
|
||||||
|
|
||||||
|
/* note‑on */
|
||||||
|
if ((status & 0xf0) == 0x90 && vel > 0) {
|
||||||
|
if (note == 64) {
|
||||||
|
atomic_store(&control_key_active, 1);
|
||||||
|
} else {
|
||||||
|
int ck = atomic_load(&control_key_active);
|
||||||
|
if (ck) {
|
||||||
|
atomic_store(&control_key_active, 0);
|
||||||
|
if (note < 16) {
|
||||||
|
atomic_store(&bind_channel, note);
|
||||||
|
} else {
|
||||||
|
switch (note) {
|
||||||
|
case 60:
|
||||||
|
atomic_store(&cmd_add, 1);
|
||||||
|
break;
|
||||||
|
case 61:
|
||||||
|
atomic_store(&cmd_remove, 1);
|
||||||
|
break;
|
||||||
|
case 62: /* trigger looper – channel via bind_channel */
|
||||||
|
{
|
||||||
|
int bch = atomic_load(&bind_channel);
|
||||||
|
if (bch >= 0 && bch < MAX_CHANNELS) {
|
||||||
|
int cur = atomic_load(&channels[bch].state);
|
||||||
|
switch (cur) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
atomic_store(&channels[bch].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&channels[bch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
atomic_store(&channels[bch].state, STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
atomic_store(&channels[bch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
case 63: /* unbind – reset bind to channel 0 */
|
||||||
|
atomic_store(&bind_channel, 0);
|
||||||
|
break;
|
||||||
|
case 70: /* load WAV into channel 0 */
|
||||||
|
atomic_store(&cmd_load, 1);
|
||||||
|
break;
|
||||||
|
case 71: /* save WAV of channel 0 */
|
||||||
|
atomic_store(&cmd_save, 1);
|
||||||
|
break;
|
||||||
|
=======
|
||||||
|
case 63: {
|
||||||
|
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} break;
|
||||||
|
case 65: {
|
||||||
|
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} break;
|
||||||
|
case 66: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 67: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 68: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 69: {
|
||||||
|
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 70: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* direct mapping */
|
||||||
|
switch (note) {
|
||||||
|
case 1: /* toggle channel 0 */
|
||||||
|
{
|
||||||
|
int cur0 = atomic_load(&channels[0].state);
|
||||||
|
switch (cur0) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
atomic_store(&channels[0].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
atomic_store(&channels[0].state, STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case 60:
|
||||||
|
atomic_store(&cmd_add, 1);
|
||||||
|
break;
|
||||||
|
case 61:
|
||||||
|
atomic_store(&cmd_remove, 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((status & 0xf0) == 0x80 ||
|
||||||
|
((status & 0xf0) == 0x90 && vel == 0)) {
|
||||||
|
atomic_store(&control_key_active, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
engine/src/pipe.c~
Normal file
111
engine/src/pipe.c~
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#include "pipe.h"
|
||||||
|
#include "command.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define FIFO_PATH "/tmp/looper_cmd"
|
||||||
|
#define LINE_MAX 256
|
||||||
|
|
||||||
|
/* forward‑declare the global queues (defined in looper.c) */
|
||||||
|
extern spsc_queue_t cmd_queue;
|
||||||
|
extern spsc_queue_t cmd_queue_main_fifo;
|
||||||
|
|
||||||
|
static void *pipe_thread_func(void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
char line[LINE_MAX];
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
FILE *fifo = fopen(FIFO_PATH, "r");
|
||||||
|
if (!fifo) {
|
||||||
|
perror("fopen fifo");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fgets(line, sizeof(line), fifo)) {
|
||||||
|
/* strip newline */
|
||||||
|
size_t len = strlen(line);
|
||||||
|
if (len > 0 && line[len - 1] == '\n')
|
||||||
|
line[len - 1] = '\0';
|
||||||
|
|
||||||
|
if (strcmp(line, "add") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "add_midi") == 0) {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
=======
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
>>>>>>> 3-integrate-carla
|
||||||
|
} else if (strcmp(line, "remove") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strncmp(line, "record ", 7) == 0) {
|
||||||
|
int ch = atoi(line + 7);
|
||||||
|
command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "stop") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strncmp(line, "bind ", 5) == 0) {
|
||||||
|
int ch = atoi(line + 5);
|
||||||
|
command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "unbind") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "scene_add") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "scene_remove") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "scene_next") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "scene_prev") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "load") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_LOAD, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "save") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
}
|
||||||
|
/* ignore unknown lines */
|
||||||
|
}
|
||||||
|
/* EOF – all writers closed, reopen for next connection */
|
||||||
|
fclose(fifo);
|
||||||
|
{
|
||||||
|
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
} /* small pause before retrying */
|
||||||
|
}
|
||||||
|
return NULL; /* unreachable */
|
||||||
|
}
|
||||||
|
|
||||||
|
int pipe_start_reader(void) {
|
||||||
|
/* create FIFO if it doesn't exist */
|
||||||
|
if (mkfifo(FIFO_PATH, 0666) != 0 && errno != EEXIST) {
|
||||||
|
perror("mkfifo");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, pipe_thread_func, NULL) != 0) {
|
||||||
|
perror("pthread_create");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pthread_detach(tid); /* we don't need to join */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
0
engine/src/plugins.c
Normal file
0
engine/src/plugins.c
Normal file
0
engine/src/plugins.h
Normal file
0
engine/src/plugins.h
Normal file
@@ -8,18 +8,19 @@ static inline size_t load_tail(const RingBuf *r) {
|
|||||||
return atomic_load_explicit(&r->tail, memory_order_relaxed);
|
return atomic_load_explicit(&r->tail, memory_order_relaxed);
|
||||||
}
|
}
|
||||||
static inline void store_head(RingBuf *r, size_t v) {
|
static inline void store_head(RingBuf *r, size_t v) {
|
||||||
atomic_store_explicit(&r->head, v, memory_order_release); // release after data written
|
atomic_store_explicit(&r->head, v, memory_order_relaxed);
|
||||||
}
|
}
|
||||||
static inline void store_tail(RingBuf *r, size_t v) {
|
static inline void store_tail(RingBuf *r, size_t v) {
|
||||||
atomic_store_explicit(&r->tail, v, memory_order_release); // release after data read
|
atomic_store_explicit(&r->tail, v, memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ring_init(RingBuf *r, size_t capacity) {
|
int ring_init(RingBuf *r, size_t capacity) {
|
||||||
r->buf = (float *)malloc(capacity * sizeof(float));
|
r->buf = (float *)malloc(capacity * sizeof(float));
|
||||||
if (!r->buf) return -1;
|
if (!r->buf)
|
||||||
|
return -1;
|
||||||
r->capacity = capacity;
|
r->capacity = capacity;
|
||||||
atomic_init(&r->head, 0);
|
store_head(r, 0);
|
||||||
atomic_init(&r->tail, 0);
|
store_tail(r, 0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,37 +30,47 @@ void ring_destroy(RingBuf *r) {
|
|||||||
r->capacity = 0;
|
r->capacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ring_write(RingBuf *r, const float *data, size_t count) {
|
static size_t ring_readable(const RingBuf *r) {
|
||||||
size_t tail = load_tail(r); // producer reads consumer's tail (relaxed is fine)
|
size_t h = load_head(r);
|
||||||
size_t head = load_head(r); // own head
|
size_t t = load_tail(r);
|
||||||
size_t cap = r->capacity;
|
if (h >= t)
|
||||||
size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head));
|
return h - t;
|
||||||
size_t avail = cap - 1 - used;
|
else
|
||||||
if (count > avail) count = avail;
|
return r->capacity - (t - h);
|
||||||
if (count == 0) return 0;
|
|
||||||
|
|
||||||
size_t pos = head;
|
|
||||||
for (size_t i = 0; i < count; ++i) {
|
|
||||||
r->buf[pos] = data[i];
|
|
||||||
pos = (pos + 1) % cap;
|
|
||||||
}
|
}
|
||||||
store_head(r, pos); // release – makes data visible to consumer
|
|
||||||
|
static size_t ring_writeable(const RingBuf *r) {
|
||||||
|
return r->capacity - 1 - ring_readable(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ring_write(RingBuf *r, const float *data, size_t count) {
|
||||||
|
size_t avail = ring_writeable(r);
|
||||||
|
if (count > avail)
|
||||||
|
count = avail;
|
||||||
|
if (count == 0)
|
||||||
|
return 0;
|
||||||
|
size_t head = load_head(r);
|
||||||
|
size_t cap = r->capacity;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
r->buf[head] = data[i];
|
||||||
|
head = (head + 1) % cap;
|
||||||
|
}
|
||||||
|
store_head(r, head);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ring_read(RingBuf *r, float *data, size_t count) {
|
size_t ring_read(RingBuf *r, float *data, size_t count) {
|
||||||
size_t head = atomic_load_explicit(&r->head, memory_order_acquire); // acquire – see producer's writes
|
size_t avail = ring_readable(r);
|
||||||
size_t tail = load_tail(r); // own tail
|
if (count > avail)
|
||||||
|
count = avail;
|
||||||
|
if (count == 0)
|
||||||
|
return 0;
|
||||||
|
size_t tail = load_tail(r);
|
||||||
size_t cap = r->capacity;
|
size_t cap = r->capacity;
|
||||||
size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head));
|
|
||||||
if (count > used) count = used;
|
|
||||||
if (count == 0) return 0;
|
|
||||||
|
|
||||||
size_t pos = tail;
|
|
||||||
for (size_t i = 0; i < count; ++i) {
|
for (size_t i = 0; i < count; ++i) {
|
||||||
data[i] = r->buf[pos];
|
data[i] = r->buf[tail];
|
||||||
pos = (pos + 1) % cap;
|
tail = (tail + 1) % cap;
|
||||||
}
|
}
|
||||||
store_tail(r, pos); // release – makes consumer's tail visible to producer
|
store_tail(r, tail);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
#include "wav.h"
|
#include "wav.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
#include <sndfile.h>
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <sndfile.h>
|
||||||
|
|
||||||
int wav_read(const char *path, float **buffer, unsigned *frames) {
|
int wav_read(const char *path, float **buffer, unsigned *frames) {
|
||||||
SF_INFO info;
|
SF_INFO info;
|
||||||
info.format = 0;
|
info.format = 0;
|
||||||
SNDFILE *sf = sf_open(path, SFM_READ, &info);
|
SNDFILE *sf = sf_open(path, SFM_READ, &info);
|
||||||
if (!sf)
|
if (!sf) return -1;
|
||||||
return -1;
|
|
||||||
|
|
||||||
/* We need mono 16-bit PCM; refuse anything else */
|
/* We need mono 16-bit PCM; refuse anything else */
|
||||||
if (info.channels != 1 || info.samplerate <= 0) {
|
if (info.channels != 1 || info.samplerate <= 0) {
|
||||||
@@ -17,14 +16,9 @@ int wav_read(const char *path, float **buffer, unsigned *frames) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned total = (info.frames > (sf_count_t)LOOP_BUF_SIZE)
|
unsigned total = (info.frames > (sf_count_t)LOOP_BUF_SIZE) ? LOOP_BUF_SIZE : (unsigned)info.frames;
|
||||||
? LOOP_BUF_SIZE
|
|
||||||
: (unsigned)info.frames;
|
|
||||||
float *buf = (float*)malloc(total * sizeof(float));
|
float *buf = (float*)malloc(total * sizeof(float));
|
||||||
if (!buf) {
|
if (!buf) { sf_close(sf); return -1; }
|
||||||
sf_close(sf);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
sf_count_t nread = sf_readf_float(sf, buf, total);
|
sf_count_t nread = sf_readf_float(sf, buf, total);
|
||||||
sf_close(sf);
|
sf_close(sf);
|
||||||
@@ -33,15 +27,13 @@ int wav_read(const char *path, float **buffer, unsigned *frames) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int wav_write(const char *path, const float *data, unsigned frames,
|
int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate) {
|
||||||
unsigned sample_rate) {
|
|
||||||
SF_INFO info;
|
SF_INFO info;
|
||||||
info.samplerate = sample_rate;
|
info.samplerate = sample_rate;
|
||||||
info.channels = 1;
|
info.channels = 1;
|
||||||
info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
|
info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
|
||||||
SNDFILE *sf = sf_open(path, SFM_WRITE, &info);
|
SNDFILE *sf = sf_open(path, SFM_WRITE, &info);
|
||||||
if (!sf)
|
if (!sf) return -1;
|
||||||
return -1;
|
|
||||||
|
|
||||||
sf_writef_float(sf, data, frames);
|
sf_writef_float(sf, data, frames);
|
||||||
sf_close(sf);
|
sf_close(sf);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <sndfile.h>
|
|
||||||
|
|
||||||
/* static variables for passthrough test */
|
/* static variables for passthrough test */
|
||||||
static jack_port_t *passthrough_output_port = NULL;
|
static jack_port_t *passthrough_output_port = NULL;
|
||||||
@@ -334,12 +333,20 @@ static int test_looper_looping(void) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* connect audio and activate immediately */
|
/* first note‑on: IDLE -> RECORD */
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(500000); /* allow state to change (500ms) */
|
||||||
|
|
||||||
int sr = jack_get_sample_rate(client);
|
int sr = jack_get_sample_rate(client);
|
||||||
continuous_sine = 0;
|
continuous_sine = 0; /* disable continuous tone */
|
||||||
beep_remaining = (int)(3.0f * sr); /* 3 sec beep – covers entire recording */
|
beep_remaining = (int)(0.1f * sr); /* 0.1 second beep */
|
||||||
bursts = 0;
|
bursts = 0;
|
||||||
prev_above = 0;
|
prev_above = 0;
|
||||||
|
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
passthrough_input_port = audio_in;
|
passthrough_input_port = audio_in;
|
||||||
passthrough_phase = 0.0f;
|
passthrough_phase = 0.0f;
|
||||||
@@ -348,6 +355,7 @@ static int test_looper_looping(void) {
|
|||||||
passthrough_total_samples = 0;
|
passthrough_total_samples = 0;
|
||||||
passthrough_sum_sq = 0.0;
|
passthrough_sum_sq = 0.0;
|
||||||
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_client_close(client);
|
jack_client_close(client);
|
||||||
@@ -355,23 +363,19 @@ static int test_looper_looping(void) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* first note‑on: IDLE -> RECORD */
|
safe_usleep(150000); /* let beep start */
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
/* ensure beep is fully captured */
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
safe_usleep(800000); /* 0.8s after start of beep */
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(3000000); /* 3s to capture beep */
|
|
||||||
|
|
||||||
/* second note‑on: RECORD -> LOOPING */
|
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
if (send_jack_note_on("looper:control", 1, 127) != 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* wait for several loop repetitions */
|
/* wait enough time for several loops (4 seconds to be safe) */
|
||||||
safe_usleep(8000000); /* 8 seconds listen */
|
safe_usleep(4000000);
|
||||||
|
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
@@ -500,7 +504,7 @@ static int test_control_key_modifier(void) {
|
|||||||
/* Wait for looper to enter RECORD and detect audio */
|
/* Wait for looper to enter RECORD and detect audio */
|
||||||
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); /* 0.5 sec beep (was 0.1) */
|
beep_remaining = (int)(0.1f * sr); /* 0.1 second beep */
|
||||||
bursts = 0;
|
bursts = 0;
|
||||||
prev_above = 0;
|
prev_above = 0;
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
@@ -513,30 +517,26 @@ static int test_control_key_modifier(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)) {
|
||||||
fprintf(stderr, " FAIL: cannot activate test client\n");
|
|
||||||
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(500000); /* allow beep to start */
|
safe_usleep(200000); /* allow beep */
|
||||||
|
/* send note 62 again under control key to move RECORD->LOOPING */
|
||||||
/* second note 64 + note 62 to move RECORD → LOOPING */
|
|
||||||
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
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);
|
||||||
fprintf(stderr, " FAIL: control key re‑send\n");
|
fprintf(stderr, " FAIL: control key re‑send\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(500000);
|
safe_usleep(200000);
|
||||||
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
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);
|
||||||
fprintf(stderr, " FAIL: send note 62 for loop\n");
|
fprintf(stderr, " FAIL: send note 62 for loop\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(5000000); /* listen for 5 seconds */
|
safe_usleep(2000000);
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM);
|
kill(pid, SIGTERM);
|
||||||
@@ -619,7 +619,7 @@ static int test_bind_channel(void) {
|
|||||||
/* Wait and detect bursts as before */
|
/* Wait and detect bursts as before */
|
||||||
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); /* 0.5 sec beep (was 0.1) */
|
beep_remaining = (int)(0.1f * sr);
|
||||||
bursts = 0;
|
bursts = 0;
|
||||||
prev_above = 0;
|
prev_above = 0;
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
@@ -632,30 +632,26 @@ static int test_bind_channel(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)) {
|
||||||
fprintf(stderr, " FAIL: cannot activate test client\n");
|
|
||||||
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(500000); /* allow beep to start */
|
safe_usleep(200000); /* allow beep */
|
||||||
|
/* send control+note62 again to move RECORD->LOOPING */
|
||||||
/* second note 64 + note 62 to move RECORD → LOOPING */
|
|
||||||
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
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);
|
||||||
fprintf(stderr, " FAIL: control key for loop\n");
|
fprintf(stderr, " FAIL: control key for loop\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(500000);
|
safe_usleep(200000);
|
||||||
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
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);
|
||||||
fprintf(stderr, " FAIL: toggle for loop\n");
|
fprintf(stderr, " FAIL: toggle for loop\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(5000000); /* listen for 5 seconds */
|
safe_usleep(2000000);
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM);
|
kill(pid, SIGTERM);
|
||||||
@@ -753,7 +749,7 @@ static int test_bind_unbind(void) {
|
|||||||
/* Wait for beep and loop */
|
/* Wait for beep and loop */
|
||||||
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); /* 0.5 sec beep (was 0.1) */
|
beep_remaining = (int)(0.1f * sr);
|
||||||
bursts = 0;
|
bursts = 0;
|
||||||
prev_above = 0;
|
prev_above = 0;
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
@@ -770,7 +766,7 @@ static int test_bind_unbind(void) {
|
|||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(1000000); /* allow recording to start before we set beep */
|
safe_usleep(200000); /* allow beep */
|
||||||
/* second control+62 -> loop */
|
/* second control+62 -> loop */
|
||||||
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
@@ -778,7 +774,7 @@ static int test_bind_unbind(void) {
|
|||||||
fprintf(stderr, " FAIL: control key for loop\n");
|
fprintf(stderr, " FAIL: control key for loop\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(2000000); /* give time to record beep */
|
safe_usleep(200000);
|
||||||
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
@@ -886,17 +882,38 @@ static int test_remove_channel(void) {
|
|||||||
* Helper: generate a simple 440 Hz WAV file for load tests
|
* Helper: generate a simple 440 Hz WAV file for load tests
|
||||||
* ------------------------------------------------------------ */
|
* ------------------------------------------------------------ */
|
||||||
static int generate_test_wav(const char *path, unsigned sample_rate, unsigned duration_frames) {
|
static int generate_test_wav(const char *path, unsigned sample_rate, unsigned duration_frames) {
|
||||||
SF_INFO info;
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
info.samplerate = sample_rate;
|
if (fd < 0) return -1;
|
||||||
info.channels = 1;
|
unsigned data_bytes = duration_frames * 2;
|
||||||
info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
|
unsigned file_size = 44 + data_bytes;
|
||||||
SNDFILE *sf = sf_open(path, SFM_WRITE, &info);
|
unsigned char header[44];
|
||||||
if (!sf) return -1;
|
memset(header, 0, 44);
|
||||||
|
memcpy(header, "RIFF", 4);
|
||||||
|
unsigned chunk_size = file_size - 8;
|
||||||
|
header[4] = chunk_size & 0xff; header[5] = (chunk_size>>8)&0xff;
|
||||||
|
header[6] = (chunk_size>>16)&0xff; header[7] = (chunk_size>>24)&0xff;
|
||||||
|
memcpy(header+8, "WAVE", 4);
|
||||||
|
memcpy(header+12, "fmt ", 4);
|
||||||
|
header[16]=16; header[17]=0; header[18]=0; header[19]=0;
|
||||||
|
header[20]=1; header[21]=0; /* PCM */
|
||||||
|
header[22]=1; header[23]=0; /* mono */
|
||||||
|
header[24]= sample_rate & 0xff; header[25]=(sample_rate>>8)&0xff;
|
||||||
|
header[26]=(sample_rate>>16)&0xff; header[27]=(sample_rate>>24)&0xff;
|
||||||
|
unsigned br = sample_rate * 2;
|
||||||
|
header[28]= br & 0xff; header[29]=(br>>8)&0xff;
|
||||||
|
header[30]=(br>>16)&0xff; header[31]=(br>>24)&0xff;
|
||||||
|
header[32]=2; header[33]=0;
|
||||||
|
header[34]=16; header[35]=0;
|
||||||
|
memcpy(header+36, "data", 4);
|
||||||
|
header[40]= data_bytes & 0xff; header[41]=(data_bytes>>8)&0xff;
|
||||||
|
header[42]=(data_bytes>>16)&0xff; header[43]=(data_bytes>>24)&0xff;
|
||||||
|
if (write(fd, header, 44) != 44) { close(fd); return -1; }
|
||||||
for (unsigned i = 0; i < duration_frames; i++) {
|
for (unsigned i = 0; i < duration_frames; i++) {
|
||||||
float sample = sinf(2.0f * (float)M_PI * 440.0f * i / sample_rate);
|
float sample = sinf(2.0f * (float)M_PI * 440.0f * i / sample_rate);
|
||||||
sf_writef_float(sf, &sample, 1);
|
int16_t s = (int16_t)(sample * 32767);
|
||||||
|
if (write(fd, &s, 2) != 2) { close(fd); return -1; }
|
||||||
}
|
}
|
||||||
sf_close(sf);
|
close(fd);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,17 +1062,15 @@ static int test_wav_save(void) {
|
|||||||
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(800000);
|
||||||
|
/* stop recording (cycle again) */
|
||||||
/* Send second record 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(500000);
|
||||||
|
|
||||||
/* save */
|
/* save */
|
||||||
if (send_fifo_command("save") != 0) {
|
if (send_fifo_command("save") != 0) {
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
@@ -1063,28 +1078,16 @@ static int test_wav_save(void) {
|
|||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(8000000); /* wait for synchronous save to complete (8s) */
|
safe_usleep(2000000);
|
||||||
|
/* check save.wav */
|
||||||
/* check save.wav with retries */
|
|
||||||
int saved = 0;
|
|
||||||
for (int retry = 0; retry < 5; retry++) {
|
|
||||||
int fd = open("save.wav", O_RDONLY);
|
int fd = open("save.wav", O_RDONLY);
|
||||||
if (fd >= 0) {
|
if (fd < 0) {
|
||||||
saved = 1;
|
|
||||||
close(fd);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
safe_usleep(1000000);
|
|
||||||
}
|
|
||||||
if (!saved) {
|
|
||||||
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);
|
||||||
fprintf(stderr, " FAIL: save.wav not created\n");
|
fprintf(stderr, " FAIL: save.wav not created\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
/* verify header */
|
|
||||||
int fd = open("save.wav", O_RDONLY);
|
|
||||||
unsigned char hdr[44];
|
unsigned char hdr[44];
|
||||||
if (read(fd, hdr, 44) != 44) {
|
if (read(fd, hdr, 44) != 44) {
|
||||||
close(fd); unlink("save.wav");
|
close(fd); unlink("save.wav");
|
||||||
|
|||||||
6
looper.log
Normal file
6
looper.log
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
looper engine starting
|
||||||
|
looper running (client name 'looper')
|
||||||
|
Warning: could not load script '/home/jt/.config/looper/scripts/launchpad.rc'
|
||||||
|
SLOW draw_grid: 299.174816 ms
|
||||||
|
SLOW draw_grid: 217.705180 ms
|
||||||
|
SLOW draw_grid: 210.477368 ms
|
||||||
8
makefile
8
makefile
@@ -2,16 +2,13 @@
|
|||||||
|
|
||||||
SUBDIRS = engine client
|
SUBDIRS = engine client
|
||||||
|
|
||||||
.PHONY: all build clean test check format orchestrator $(SUBDIRS)
|
.PHONY: all build clean test check format $(SUBDIRS)
|
||||||
|
|
||||||
all: build orchestrator
|
all: build
|
||||||
|
|
||||||
build: $(SUBDIRS)
|
build: $(SUBDIRS)
|
||||||
@echo "Build complete."
|
@echo "Build complete."
|
||||||
|
|
||||||
orchestrator: orchestrator.c
|
|
||||||
$(CC) -Wall -Wextra -std=c11 -o looper orchestrator.c
|
|
||||||
|
|
||||||
$(SUBDIRS):
|
$(SUBDIRS):
|
||||||
$(MAKE) -C $@
|
$(MAKE) -C $@
|
||||||
|
|
||||||
@@ -20,7 +17,6 @@ test:
|
|||||||
$(MAKE) -C client test
|
$(MAKE) -C client test
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f looper
|
|
||||||
@for dir in $(SUBDIRS); do \
|
@for dir in $(SUBDIRS); do \
|
||||||
echo "Cleaning $$dir..."; \
|
echo "Cleaning $$dir..."; \
|
||||||
$(MAKE) -C $$dir clean; \
|
$(MAKE) -C $$dir clean; \
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
static pid_t engine_pid = 0;
|
|
||||||
static pid_t client_pid = 0;
|
|
||||||
|
|
||||||
static void cleanup(int sig) {
|
|
||||||
(void)sig;
|
|
||||||
if (engine_pid > 0) kill(engine_pid, SIGTERM);
|
|
||||||
if (client_pid > 0) kill(client_pid, SIGTERM);
|
|
||||||
while (wait(NULL) > 0);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
signal(SIGINT, cleanup);
|
|
||||||
signal(SIGTERM, cleanup);
|
|
||||||
|
|
||||||
engine_pid = fork();
|
|
||||||
if (engine_pid == 0) {
|
|
||||||
execl("./engine/looper", "looper", NULL);
|
|
||||||
perror("execl engine");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
client_pid = fork();
|
|
||||||
if (client_pid == 0) {
|
|
||||||
if (argc > 2 && strcmp(argv[1], "-s") == 0) {
|
|
||||||
execl("./client/looper-client", "looper-client", "-s", argv[2], NULL);
|
|
||||||
} else {
|
|
||||||
execl("./client/looper-client", "looper-client", NULL);
|
|
||||||
}
|
|
||||||
perror("execl client");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int status;
|
|
||||||
pid_t exited = wait(&status);
|
|
||||||
if (exited == engine_pid) {
|
|
||||||
kill(client_pid, SIGTERM);
|
|
||||||
wait(NULL);
|
|
||||||
} else if (exited == client_pid) {
|
|
||||||
kill(engine_pid, SIGTERM);
|
|
||||||
wait(NULL);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user