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