Compare commits
61 Commits
6-recordin
...
8-add-tui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cffec86e7 | ||
|
|
971372eac9 | ||
|
|
5341cb676a | ||
|
|
791744beeb | ||
|
|
998406616a | ||
|
|
5ad831f50c | ||
|
|
f3dde6b668 | ||
|
|
10d0269a5a | ||
| b994911dab | |||
|
|
d4a811e552 | ||
|
|
567799a2d3 | ||
|
|
755af275d8 | ||
|
|
74db4ed46c | ||
|
|
15be644af7 | ||
|
|
aaca25ebf1 | ||
|
|
e3b9321b1a | ||
|
|
015ad2c5a7 | ||
|
|
c8b9de8e81 | ||
|
|
1ba98fc768 | ||
|
|
4dfb7a87c1 | ||
|
|
8892acd3d2 | ||
|
|
7b00246443 | ||
|
|
44177f785f | ||
|
|
94d6bc25f1 | ||
|
|
86d9bc72f1 | ||
| 75f347c418 | |||
|
|
0be6cfb31d | ||
|
|
de8202a0d2 | ||
|
|
fe3fb7d873 | ||
|
|
ffe422d83f | ||
|
|
5b1969415f | ||
|
|
91d58a07f5 | ||
|
|
4e489b5e40 | ||
|
|
df5ecef580 | ||
|
|
df181b117e | ||
|
|
ff226a8ea6 | ||
|
|
85e828f461 | ||
|
|
19b686fe2d | ||
|
|
0691594a92 | ||
|
|
9da4481300 | ||
|
|
b7827e7311 | ||
|
|
595a35ec32 | ||
| f11a18a203 | |||
|
|
5739ff8019 | ||
|
|
3a4aac3356 | ||
|
|
69859a6294 | ||
|
|
d47fddbeb3 | ||
|
|
900619a714 | ||
|
|
98c851f051 | ||
|
|
011d29cb09 | ||
|
|
be3188bbe2 | ||
|
|
c592c24634 | ||
|
|
7b61384154 | ||
|
|
7edd95d06e | ||
|
|
de0389e144 | ||
|
|
bd5fd59b7b | ||
|
|
b1e330e839 | ||
|
|
437ac31913 | ||
|
|
a8a9c6164b | ||
|
|
392dabbc0f | ||
|
|
f7f18f9fa7 |
0
breakup.md
Normal file
0
breakup.md
Normal file
136
client/PLAN.md
Normal file
136
client/PLAN.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Plan: Refactor TUI into Standalone FIFO‑Client Binary
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Extract the TUI from the existing monolithic codebase into a separate `looper-client` binary that communicates with the engine **only** via the FIFO pipe (`/tmp/looper_cmd`).
|
||||||
|
The TUI must **not** link to any engine source files (`engine.c`, `dispatcher.c`, `carla.c`, etc.) or use their headers beyond shared type definitions (e.g., `command.h`). All state is maintained by the engine; the TUI sends commands and assumes they succeed.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
- The looper engine runs as a separate JACK client and listens for commands on:
|
||||||
|
- FIFO pipe (`/tmp/looper_cmd`) – text‑based commands
|
||||||
|
- `looper:control` – MIDI note‑on events
|
||||||
|
- The current TUI uses a local `Engine` / `AppState` / `dispatcher` that does **not** talk to the real looper. It was designed for unit testing.
|
||||||
|
|
||||||
|
## Task 1 – Create a new `client/` directory structure
|
||||||
|
- Keep existing `client/src/tui.c` and `client/src/tui.h`.
|
||||||
|
- Remove all `#include` directives that reference engine internal headers:
|
||||||
|
- `engine.h`
|
||||||
|
- `dispatcher.h`
|
||||||
|
- `wav_io.h`
|
||||||
|
- `transport.h`
|
||||||
|
- `carla.h`
|
||||||
|
- Remove any `Engine*`, `AppState`, `DispatchFn`, `Clip`, `MidiClip` usage.
|
||||||
|
- **Keep** the `FuzzySearch` struct, `draw_rack_view()`, `handle_rack_view()`, `list_wav_files()`, `load_sample_callback`, mouse handling, etc. – they **are not removed**.
|
||||||
|
However, every call to engine internals (e.g., `carla_get_available_plugins`, `dispatcher_get_state`, `g_dispatch`, `carla.h` functions) **must be replaced** with a stub that does nothing (or prints a debug message) until the engine implements the corresponding FIFO commands.
|
||||||
|
This keeps the UI code compilable and preserves the structure for future implementation.
|
||||||
|
|
||||||
|
## Task 2 – Implement `send_command()` and open FIFO
|
||||||
|
- Add a function:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int send_command(const char *cmd_line);
|
||||||
|
```
|
||||||
|
|
||||||
|
This function:
|
||||||
|
- Opens `/tmp/looper_cmd` with `O_WRONLY`.
|
||||||
|
- Writes the command string (e.g., `"record 0\n"`).
|
||||||
|
- Appends a newline if missing.
|
||||||
|
- Closes the file descriptor.
|
||||||
|
- Returns 0 on success, -1 on error (prints diagnostic to stderr).
|
||||||
|
|
||||||
|
- The TUI should call `send_command()` for every user action that should affect the engine.
|
||||||
|
|
||||||
|
## Task 3 – Map TUI keys to FIFO commands
|
||||||
|
Replace each `g_dispatch(action)` with a direct FIFO command string.
|
||||||
|
|
||||||
|
**Proposed mapping (simplified – can be refined later):**
|
||||||
|
|
||||||
|
| TUI key/action | FIFO command |
|
||||||
|
|--------------------------------------------|-----------------------------------------------------|
|
||||||
|
| `'t'` (trigger clip at selected cell) | `record <channel>\n` (channel = `selected_col`) |
|
||||||
|
| `'d'` (reset clip) | `stop\n` (global stop) |
|
||||||
|
| `'s'` (trigger scene – current row) | `"scene_next\n"` (or `"scene_add\n"`? TBD) |
|
||||||
|
| `' '` (toggle transport play/pause) | No corresponding FIFO command yet. Omit for now. |
|
||||||
|
| `'S'` (stop transport) | `"stop\n"` |
|
||||||
|
| `'q'` (cycle quantize) | No FIFO equivalent – ignore. |
|
||||||
|
| `'x'` (reset transport) | `"stop\n"` |
|
||||||
|
| `'N'` (play next scene) | `"scene_next\n"` |
|
||||||
|
| `'P'` (play previous scene) | `"scene_prev\n"` |
|
||||||
|
| `'u'` (undo) | No FIFO equivalent – ignore. |
|
||||||
|
| `Ctrl+R` (redo) | Ignore. |
|
||||||
|
| `'v'`, `'V'` (visual mode) | Keep visual selection logic but send commands only for `'d'` and `'y'` actions. |
|
||||||
|
| `'y'` (yank) | Do nothing (local clipboard only). |
|
||||||
|
| `'p'` (paste) | For each pasted cell send `"record <ch>\n"`. |
|
||||||
|
| `'m'` (move mode) | No effect on engine – local navigation. |
|
||||||
|
| `'z'` (zoom grid selector) | Local navigation only. |
|
||||||
|
| `'G'` (toggle audio/MIDI grid) | No FIFO command – ignore. |
|
||||||
|
| `'-'` / `'='` (volume) | No FIFO command – ignore. |
|
||||||
|
| `\t` (switch to rack view) | Remove entirely. |
|
||||||
|
| `':'` command mode | Keep for `:q` (quit) and `:rack` commands (the latter can be removed). |
|
||||||
|
| Escape / `'Q'` | Quit the TUI (no command sent). |
|
||||||
|
|
||||||
|
**Channel binding:**
|
||||||
|
- When the user moves selection to a new column, send `"bind <col>\n"` to ensure subsequent commands affect the correct channel. This can be done in the navigation switch cases.
|
||||||
|
|
||||||
|
## Task 4 – Remove all references to `Engine` and `dispatcher`
|
||||||
|
- Delete the lines:
|
||||||
|
- `static Engine *g_engine = NULL;`
|
||||||
|
- `static DispatchFn g_dispatch = NULL;`
|
||||||
|
- Replace calls like `g_dispatch(action)` with `send_command(formatted_string)`.
|
||||||
|
- Remove `dispatcher_get_state()` calls – the TUI will no longer query the engine state. Update `draw_cell()` to display only static info (clip index) or a fixed colour (e.g., all green). The state‑dependent colouring is not available without feedback from the engine. For now, show all cells as idle (white) or use a placeholder.
|
||||||
|
- Remove the line `AppState state; dispatcher_get_state(&state);` inside draw functions.
|
||||||
|
|
||||||
|
## Task 5 – Simplify the `tui.h` header
|
||||||
|
- Replace the function signatures:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void tui_init(void); // no Engine* argument
|
||||||
|
void tui_run(void); // no Engine* argument
|
||||||
|
void tui_cleanup(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Remove `#include "engine.h"` and `#include "dispatcher.h"`.
|
||||||
|
- Remove the `Engine*` parameter from the init and run functions.
|
||||||
|
|
||||||
|
## Task 6 – Create `client/main.c`
|
||||||
|
- Write a simple `main()` that:
|
||||||
|
- Optionally opens the FIFO for writing just to check it exists.
|
||||||
|
- Calls `tui_init()`.
|
||||||
|
- Calls `tui_run()`.
|
||||||
|
- Calls `tui_cleanup()`.
|
||||||
|
- Returns 0.
|
||||||
|
|
||||||
|
## Task 7 – Write `client/makefile`
|
||||||
|
- Target `looper-client`:
|
||||||
|
- Compile `src/tui.c` and `src/main.c` (or `src/client.c` if split).
|
||||||
|
- **Do not** link to any engine `.o` files.
|
||||||
|
- Link only with `-lncurses` (and `-lm` if needed).
|
||||||
|
- Example:
|
||||||
|
```makefile
|
||||||
|
CC ?= gcc
|
||||||
|
CFLAGS ?= -Wall -Wextra -g -I../engine/src
|
||||||
|
LDFLAGS ?= -lncurses -lm
|
||||||
|
|
||||||
|
looper-client: src/tui.c src/main.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 8 – Remove dead code and unnecessary helpers
|
||||||
|
- Delete `utils.c` / `utils.h` if they existed only for TUI – but keep all WAV‑related, rack‑related, and fuzzy‑search code (they are now stubs).
|
||||||
|
- Remove `clip_state_to_string()`, `transport_state_to_string()`, `quantize_mode_to_string()`, `clock_source_to_string()` – they are no longer used for display because we have no `AppState`.
|
||||||
|
Replace them with static strings that show placeholder text (e.g., `"N/A"`).
|
||||||
|
- Remove `state_to_color()` – instead use a fixed colour pair (e.g., all cells white) or remove colour entirely, because we have no clip state.
|
||||||
|
- Remove the `mouse` callback if it relied on `dispatcher_get_state` – but keep the function body as a no‑op.
|
||||||
|
|
||||||
|
## Task 9 – Test the new client
|
||||||
|
- Build `looper-client` and verify it compiles without engine object files.
|
||||||
|
- Start the engine (`./looper` in `engine/`).
|
||||||
|
- Run `./looper-client` and press keys that should generate FIFO commands. Use `cat /tmp/looper_cmd` in another terminal to verify output.
|
||||||
|
- Check that commands like `record 2`, `stop`, `bind 3` appear.
|
||||||
|
|
||||||
|
## Notes / Future Improvements
|
||||||
|
- **State feedback:** The TUI currently shows clip state colours. To restore that, a separate FIFO (or shared memory) for engine‑>client status could be added. Not part of this plan.
|
||||||
|
- **MIDI grid / rack view:** These depend on engine features not yet exposed via FIFO. They are removed; can be re‑added later.
|
||||||
|
- **Transport commands:** The engine does not have a dedicated transport play/pause command via FIFO; it relies on MIDI notes. Future FIFO extension needed.
|
||||||
|
|
||||||
|
This plan produces a clean, minimal client that interfaces only through the named pipe.
|
||||||
|
````
|
||||||
BIN
client/looper-client
Executable file
BIN
client/looper-client
Executable file
Binary file not shown.
BIN
client/looper-client-test
Executable file
BIN
client/looper-client-test
Executable file
Binary file not shown.
8
client/main.c
Normal file
8
client/main.c
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "tui.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
tui_init();
|
||||||
|
tui_run();
|
||||||
|
tui_cleanup();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
18
client/makefile
Normal file
18
client/makefile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -Wpedantic -std=c11
|
||||||
|
|
||||||
|
all: looper-client test_status_parse
|
||||||
|
|
||||||
|
looper-client: src/main.c src/tui.c
|
||||||
|
$(CC) $(CFLAGS) -Isrc -o $@ $^ -lncurses
|
||||||
|
|
||||||
|
test_status_parse: tests/test_status_parse.c
|
||||||
|
$(CC) $(CFLAGS) -Isrc -o test_status_parse tests/test_status_parse.c src/tui.c -lncurses
|
||||||
|
|
||||||
|
test: looper-client test_status_parse
|
||||||
|
./test_status_parse
|
||||||
|
|
||||||
|
.PHONY: all test clean
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f looper-client test_status_parse
|
||||||
8
client/src/main.c
Normal file
8
client/src/main.c
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "tui.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
tui_init();
|
||||||
|
tui_run();
|
||||||
|
tui_cleanup();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
238
client/src/tui.c
Normal file
238
client/src/tui.c
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#include "tui.h"
|
||||||
|
#include <ncurses.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* ---------- FIFO command helper ---------- */
|
||||||
|
int send_command(const char *cmd) {
|
||||||
|
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
||||||
|
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
||||||
|
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
size_t len = strlen(cmd);
|
||||||
|
int n = write(fd, cmd, len);
|
||||||
|
if (n == (int)len && cmd[len-1] != '\n')
|
||||||
|
write(fd, "\n", 1);
|
||||||
|
close(fd);
|
||||||
|
return (n >= 0) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Stub functions (no engine) ---------- */
|
||||||
|
// Clip states – dummy values used as placeholders
|
||||||
|
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
|
||||||
|
static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
|
||||||
|
|
||||||
|
/* Grid dimensions */
|
||||||
|
#define GRID_ROWS 8
|
||||||
|
#define GRID_COLS 8
|
||||||
|
#define NUM_GRIDS 8
|
||||||
|
#define CELL_WIDTH 6
|
||||||
|
#define CELL_HEIGHT 3
|
||||||
|
|
||||||
|
/* status FIFO path */
|
||||||
|
#define STATUS_FIFO "/tmp/looper_status"
|
||||||
|
#define CMD_FIFO "/tmp/looper_cmd"
|
||||||
|
|
||||||
|
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
||||||
|
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||||
|
static ChannelState cell_state[GRID_ROWS * GRID_COLS];
|
||||||
|
|
||||||
|
/* Color pairs */
|
||||||
|
enum {
|
||||||
|
COLOR_EMPTY=1, COLOR_RECORDING, COLOR_LOOPING, COLOR_STOPPED,
|
||||||
|
COLOR_SELECTED, COLOR_HELP
|
||||||
|
};
|
||||||
|
|
||||||
|
static int selected_row = 0, selected_col = 0;
|
||||||
|
static int selected_grid = 0;
|
||||||
|
static bool show_help = false;
|
||||||
|
|
||||||
|
/* Visual mode, marks, yank buffer – keep but only local state */
|
||||||
|
static int marks[26];
|
||||||
|
typedef struct { int *clip_indices; int count; } YankBuffer;
|
||||||
|
static YankBuffer yank_buffer = {NULL, 0};
|
||||||
|
typedef enum { MODE_NORMAL, MODE_VISUAL, MODE_MOVE } UIMode;
|
||||||
|
static UIMode current_mode = MODE_NORMAL;
|
||||||
|
static int visual_start_row, visual_start_col, visual_end_row, visual_end_col;
|
||||||
|
|
||||||
|
/* Fuzzy search – keep struct but stub carla calls */
|
||||||
|
typedef struct {
|
||||||
|
char query[256]; int query_len, selected_index, num_results;
|
||||||
|
int result_indices[256]; bool active; char prompt[64];
|
||||||
|
void (*callback)(const char *);
|
||||||
|
const char **items; int num_items; bool free_items;
|
||||||
|
} FuzzySearch;
|
||||||
|
static FuzzySearch fuzzy_search = {0};
|
||||||
|
|
||||||
|
/* ---------- Parse status line from engine status FIFO ---------- */
|
||||||
|
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state) {
|
||||||
|
int sta;
|
||||||
|
if (sscanf(line, "CH=%d SC=%d STATE=%d", ch, scene, &sta) == 3) {
|
||||||
|
if (sta >= 0 && sta <= 3) {
|
||||||
|
*state = (ChannelState)sta;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* try text-based format */
|
||||||
|
char state_str[32];
|
||||||
|
if (sscanf(line, "CH=%d SC=%d STATE=%31s", ch, scene, state_str) != 3)
|
||||||
|
return false;
|
||||||
|
if (strcmp(state_str, "IDLE") == 0) { *state = STATE_IDLE; return true; }
|
||||||
|
if (strcmp(state_str, "RECORD") == 0) { *state = STATE_RECORD; return true; }
|
||||||
|
if (strcmp(state_str, "LOOPING") == 0) { *state = STATE_LOOPING; return true; }
|
||||||
|
if (strcmp(state_str, "PAUSED") == 0) { *state = STATE_PAUSED; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- State to color (uses cell_state array) ---------- */
|
||||||
|
static int state_to_color(ChannelState s) {
|
||||||
|
switch (s) {
|
||||||
|
case STATE_IDLE: return COLOR_EMPTY;
|
||||||
|
case STATE_RECORD: return COLOR_RECORDING;
|
||||||
|
case STATE_LOOPING: return COLOR_LOOPING;
|
||||||
|
case STATE_PAUSED: return COLOR_STOPPED;
|
||||||
|
default: return COLOR_EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Draw cell (no AppState) ---------- */
|
||||||
|
static void draw_cell(int grid, int row, int col, bool selected) {
|
||||||
|
int y = row * CELL_HEIGHT + 3;
|
||||||
|
int x = col * CELL_WIDTH + 1;
|
||||||
|
int idx = row * GRID_COLS + col;
|
||||||
|
ChannelState s = cell_state[idx];
|
||||||
|
int color = selected ? COLOR_SELECTED : state_to_color(s);
|
||||||
|
attron(COLOR_PAIR(color));
|
||||||
|
for (int dy=0; dy<CELL_HEIGHT; dy++)
|
||||||
|
for (int dx=0; dx<CELL_WIDTH; dx++)
|
||||||
|
mvaddch(y+dy, x+dx, ' ');
|
||||||
|
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
||||||
|
attroff(COLOR_PAIR(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_grid(void) {
|
||||||
|
clear();
|
||||||
|
attron(A_BOLD);
|
||||||
|
mvprintw(0,0,"JACK Looper - Client (FIFO only)");
|
||||||
|
attroff(A_BOLD);
|
||||||
|
for (int r=0; r<GRID_ROWS; r++)
|
||||||
|
for (int c=0; c<GRID_COLS; c++)
|
||||||
|
draw_cell(selected_grid, r, c, r==selected_row && c==selected_col);
|
||||||
|
mvprintw(GRID_ROWS*CELL_HEIGHT+3, 0, "Selected: Grid %d, Row %d, Col %d",
|
||||||
|
selected_grid, selected_row, selected_col);
|
||||||
|
if (show_help) {
|
||||||
|
attron(COLOR_PAIR(COLOR_HELP));
|
||||||
|
mvprintw(GRID_ROWS*CELL_HEIGHT+4, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, ? help, Esc/Q quit");
|
||||||
|
attroff(COLOR_PAIR(COLOR_HELP));
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- TUI init ---------- */
|
||||||
|
void tui_init(void) {
|
||||||
|
initscr();
|
||||||
|
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
|
||||||
|
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
|
||||||
|
start_color();
|
||||||
|
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
||||||
|
init_pair(COLOR_RECORDING, COLOR_RED, COLOR_BLACK);
|
||||||
|
init_pair(COLOR_LOOPING, COLOR_GREEN, COLOR_BLACK);
|
||||||
|
init_pair(COLOR_STOPPED, COLOR_BLUE, COLOR_BLACK);
|
||||||
|
init_pair(COLOR_SELECTED, COLOR_BLACK, COLOR_CYAN);
|
||||||
|
init_pair(COLOR_HELP, COLOR_CYAN, COLOR_BLACK);
|
||||||
|
for (int i=0;i<26;i++) marks[i] = -1;
|
||||||
|
/* initialise cell states to idle */
|
||||||
|
for (int i = 0; i < GRID_ROWS * GRID_COLS; i++)
|
||||||
|
cell_state[i] = STATE_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- TUI run ---------- */
|
||||||
|
void tui_run(void) {
|
||||||
|
draw_grid();
|
||||||
|
while (1) {
|
||||||
|
/* read any available status lines */
|
||||||
|
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
|
if (fd >= 0) {
|
||||||
|
char buf[256];
|
||||||
|
int n = read(fd, buf, sizeof(buf)-1);
|
||||||
|
if (n > 0) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
char *line = buf;
|
||||||
|
while (*line) {
|
||||||
|
char *nl = strchr(line, '\n');
|
||||||
|
if (nl) *nl = '\0';
|
||||||
|
int ch, sc;
|
||||||
|
ChannelState st;
|
||||||
|
if (parse_status_line(line, &ch, &sc, &st)) {
|
||||||
|
if (ch >= 0 && ch < GRID_ROWS * GRID_COLS)
|
||||||
|
cell_state[ch] = st;
|
||||||
|
}
|
||||||
|
if (nl) {
|
||||||
|
*nl = '\n';
|
||||||
|
line = nl + 1;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
int chc = getch();
|
||||||
|
switch (chc) {
|
||||||
|
case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break;
|
||||||
|
case 'j': case KEY_DOWN: selected_row = (selected_row+1)%GRID_ROWS; break;
|
||||||
|
case 'k': case KEY_UP: selected_row = (selected_row-1+GRID_ROWS)%GRID_ROWS; break;
|
||||||
|
case 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
|
||||||
|
case 't': {
|
||||||
|
char cmd[32];
|
||||||
|
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
|
||||||
|
send_command(cmd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
send_command("scene_next\n");
|
||||||
|
break;
|
||||||
|
case 'S':
|
||||||
|
send_command("scene_prev\n");
|
||||||
|
break;
|
||||||
|
case 'd': case 'D':
|
||||||
|
send_command("stop\n");
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
send_command("add\n");
|
||||||
|
break;
|
||||||
|
case 'A':
|
||||||
|
send_command("add_midi\n");
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
send_command("remove\n");
|
||||||
|
break;
|
||||||
|
case 'b': {
|
||||||
|
char cmd[16];
|
||||||
|
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||||||
|
send_command(cmd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'u':
|
||||||
|
send_command("unbind\n");
|
||||||
|
break;
|
||||||
|
case '?': show_help = !show_help; break;
|
||||||
|
case 27: case 'Q': return;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
draw_grid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void tui_cleanup(void) {
|
||||||
|
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||||||
|
/* delete FIFOs */
|
||||||
|
unlink(STATUS_FIFO);
|
||||||
|
unlink(CMD_FIFO);
|
||||||
|
curs_set(1); endwin();
|
||||||
|
}
|
||||||
9
client/src/tui.h
Normal file
9
client/src/tui.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef TUI_H
|
||||||
|
#define TUI_H
|
||||||
|
|
||||||
|
void tui_init(void);
|
||||||
|
void tui_run(void);
|
||||||
|
void tui_cleanup(void);
|
||||||
|
int send_command(const char *cmd);
|
||||||
|
|
||||||
|
#endif
|
||||||
BIN
client/test_status_parse
Executable file
BIN
client/test_status_parse
Executable file
Binary file not shown.
67
client/tests/test_client.c
Normal file
67
client/tests/test_client.c
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "tui.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#define TEST_PASS 0
|
||||||
|
#define TEST_FAIL 1
|
||||||
|
|
||||||
|
static int run_single_test(const char *test_name, const char *cmd_sent, const char *expected) {
|
||||||
|
/* build temporary file path */
|
||||||
|
char tmpl[] = "/tmp/looper_test_XXXXXX";
|
||||||
|
int fd = mkstemp(tmpl);
|
||||||
|
if (fd == -1) { perror("mkstemp"); return TEST_FAIL; }
|
||||||
|
close(fd);
|
||||||
|
/* create regular file to mimic a FIFO */
|
||||||
|
fd = open(tmpl, O_CREAT|O_WRONLY|O_TRUNC, 0644);
|
||||||
|
if (fd < 0) { perror("open create"); unlink(tmpl); return TEST_FAIL; }
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
/* make send_command use this file */
|
||||||
|
setenv("LOOPER_CMD_FIFO", tmpl, 1);
|
||||||
|
|
||||||
|
int ret = send_command(cmd_sent);
|
||||||
|
if (ret != 0) {
|
||||||
|
fprintf(stderr, "FAIL %s: send_command returned %d\n", test_name, ret);
|
||||||
|
unlink(tmpl);
|
||||||
|
return TEST_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read back the written content */
|
||||||
|
FILE *fp = fopen(tmpl, "r");
|
||||||
|
if (!fp) { perror("fopen"); unlink(tmpl); return TEST_FAIL; }
|
||||||
|
char buf[4096];
|
||||||
|
size_t nread = fread(buf, 1, sizeof(buf)-1, fp);
|
||||||
|
fclose(fp);
|
||||||
|
buf[nread] = '\0';
|
||||||
|
|
||||||
|
/* build expected string (send_command always appends a newline) */
|
||||||
|
char expected_line[512];
|
||||||
|
snprintf(expected_line, sizeof(expected_line), "%s\n", expected);
|
||||||
|
|
||||||
|
if (strcmp(buf, expected_line) == 0) {
|
||||||
|
printf("PASS %s\n", test_name);
|
||||||
|
unlink(tmpl);
|
||||||
|
return TEST_PASS;
|
||||||
|
} else {
|
||||||
|
printf("FAIL %s: expected '%s', got '%s'\n", test_name, expected_line, buf);
|
||||||
|
unlink(tmpl);
|
||||||
|
return TEST_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int fail = 0;
|
||||||
|
fail += run_single_test("record_0", "record 0", "record 0");
|
||||||
|
fail += run_single_test("record_1", "record 1", "record 1");
|
||||||
|
fail += run_single_test("stop", "stop", "stop");
|
||||||
|
fail += run_single_test("scene_next", "scene_next", "scene_next");
|
||||||
|
fail += run_single_test("scene_prev", "scene_prev", "scene_prev");
|
||||||
|
fail += run_single_test("bind_2", "bind 2", "bind 2");
|
||||||
|
fail += run_single_test("with_newline", "record 0\n", "record 0");
|
||||||
|
printf("%d tests failed.\n", fail);
|
||||||
|
return fail > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
88
client/tests/test_status_parse.c
Normal file
88
client/tests/test_status_parse.c
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||||
|
|
||||||
|
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state);
|
||||||
|
|
||||||
|
static int test_parse_idle(void) {
|
||||||
|
printf("Test parse_status_line: IDLE\n");
|
||||||
|
int ch, sc; ChannelState st;
|
||||||
|
if (!parse_status_line("CH=0 SC=0 STATE=IDLE\n", &ch, &sc, &st)) {
|
||||||
|
fprintf(stderr, " FAIL: parse returned false\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ch != 0 || sc != 0 || st != STATE_IDLE) {
|
||||||
|
fprintf(stderr, " FAIL: expected (0,0,IDLE), got (%d,%d,%d)\n", ch, sc, st);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_parse_recording(void) {
|
||||||
|
printf("Test parse_status_line: RECORD\n");
|
||||||
|
int ch, sc; ChannelState st;
|
||||||
|
if (!parse_status_line("CH=0 SC=0 STATE=RECORD\n", &ch, &sc, &st)) {
|
||||||
|
fprintf(stderr, " FAIL: parse returned false\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ch != 0 || sc != 0 || st != STATE_RECORD) {
|
||||||
|
fprintf(stderr, " FAIL: expected (0,0,RECORD), got (%d,%d,%d)\n", ch, sc, st);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_parse_looping(void) {
|
||||||
|
printf("Test parse_status_line: LOOPING\n");
|
||||||
|
int ch, sc; ChannelState st;
|
||||||
|
if (!parse_status_line("CH=0 SC=0 STATE=LOOPING\n", &ch, &sc, &st)) {
|
||||||
|
fprintf(stderr, " FAIL: parse returned false\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ch != 0 || sc != 0 || st != STATE_LOOPING) {
|
||||||
|
fprintf(stderr, " FAIL: expected (0,0,LOOPING), got (%d,%d,%d)\n", ch, sc, st);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_parse_paused(void) {
|
||||||
|
printf("Test parse_status_line: PAUSED\n");
|
||||||
|
int ch, sc; ChannelState st;
|
||||||
|
if (!parse_status_line("CH=0 SC=0 STATE=PAUSED\n", &ch, &sc, &st)) {
|
||||||
|
fprintf(stderr, " FAIL: parse returned false\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ch != 0 || sc != 0 || st != STATE_PAUSED) {
|
||||||
|
fprintf(stderr, " FAIL: expected (0,0,PAUSED), got (%d,%d,%d)\n", ch, sc, st);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_parse_malformed(void) {
|
||||||
|
printf("Test parse_status_line: malformed\n");
|
||||||
|
int ch, sc; ChannelState st;
|
||||||
|
if (parse_status_line("garbage\n", &ch, &sc, &st)) {
|
||||||
|
fprintf(stderr, " FAIL: parse should return false for garbage\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int fail = 0;
|
||||||
|
fail += test_parse_idle();
|
||||||
|
fail += test_parse_recording();
|
||||||
|
fail += test_parse_looping();
|
||||||
|
fail += test_parse_paused();
|
||||||
|
fail += test_parse_malformed();
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Sampling and Recording (WAV Load/Save)
|
|
||||||
|
|
||||||
The looper supports loading a WAV file into channel 0 and saving the current loop of channel 0 as a WAV file. Both operations use the **libsndfile** library, ensuring correct handling of RIFF headers, chunk sizes, and sample format conversion.
|
|
||||||
|
|
||||||
## Load Command
|
|
||||||
|
|
||||||
- **MIDI note 70** with the control key (note 64) triggers loading.
|
|
||||||
- The file `loop.wav` (located in the working directory) is read by `wav_read()` in `src/wav.c`.
|
|
||||||
- The function calls `sf_open(path, SFM_READ, &info)`.
|
|
||||||
- It accepts only mono PCM WAV files. If the file is not mono or has an invalid sample rate, it returns `-1`.
|
|
||||||
- The number of frames read is capped at `LOOP_BUF_SIZE` (5 seconds at 48 kHz).
|
|
||||||
- The data is stored in `channels[0].loop_buffer` and `channels[0].loop_count` is set atomically.
|
|
||||||
- The state of channel 0 is set to `STATE_LOOPING` and `prev_state` is set to `-1` to trigger the loop start in the next audio cycle.
|
|
||||||
|
|
||||||
## Save Command
|
|
||||||
|
|
||||||
- **MIDI note 71** with the control key (note 64) triggers saving.
|
|
||||||
- The looper must currently be in `STATE_LOOPING` and have a non‑zero `loop_count`.
|
|
||||||
- A ring buffer (`RingBuf`) is allocated with capacity `2 × loop_count` samples.
|
|
||||||
- The pointer to the ring buffer is published via `atomic_store_explicit` on `channels[0].save_ring`.
|
|
||||||
- In each audio callback cycle, if the channel is looping and a save ring exists, the audio output data is written into the ring buffer.
|
|
||||||
- A dedicated **writer thread** (`writer_thread`) is launched (detached) to consume the ring buffer.
|
|
||||||
- The writer thread reads `loop_count` samples from the ring buffer, sleeping 10 ms between empty reads.
|
|
||||||
- Once all samples are collected, it writes them to `save.wav` using `sf_writef_float()`.
|
|
||||||
- After writing, the ring buffer is destroyed and freed, and the save ring pointer is set to `NULL`.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **libsndfile** must be installed (development headers). Add `-lsndfile` to your linker flags (already present in the provided `makefile`).
|
|
||||||
|
|
||||||
## Implementation Files
|
|
||||||
|
|
||||||
- `src/wav.c` – contains `wav_read()` and `wav_write()` based on libsndfile.
|
|
||||||
- `src/looper.c` – contains the load/save command handling in `looper_process_commands()` and the writer thread function.
|
|
||||||
- `src/channel.h` – defines `save_ring` as `_Atomic RingBuf *`.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- The integration test `test_wav_load` creates a short 440 Hz WAV file, loads it via MIDI, and checks for ≥3 bursts of audio output.
|
|
||||||
- The integration test `test_wav_save` records a beep, loops it, issues the save command, and verifies the resulting WAV file has non‑zero data size.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The save operation is asynchronous: the writer thread runs in the background while the audio callback continues to fill the ring buffer. The test waits 2 s for the file to be written before checking.
|
|
||||||
- The load operation is synchronous: the callback sleeps 1 s after the MIDI command to give the main loop time to process it.
|
|
||||||
148
docs/8-tui.md
Normal file
148
docs/8-tui.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# TUI Client – Architecture and Usage
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The TUI client (`looper-client`) is a standalone ncurses application that communicates with the looper engine **only** via two named pipes:
|
||||||
|
|
||||||
|
- `/tmp/looper_cmd` – the client writes text commands to the engine.
|
||||||
|
- `/tmp/looper_status` – the engine writes one line per active channel after each main‑loop iteration, reporting the current scene state.
|
||||||
|
|
||||||
|
The client never links against engine source code. It is built from files in `client/src/` and linked only with `-lncurses`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
User keypress
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
tui_run() ──► getch() ──► switch(ch)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ send_command(cmd)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ write("/tmp/looper_cmd")
|
||||||
|
│
|
||||||
|
│ ┌──────────────────┐
|
||||||
|
│ │ Engine main loop │
|
||||||
|
│ │ (looper.c) │
|
||||||
|
│ │ │
|
||||||
|
│ │ looper_process_ │
|
||||||
|
│ │ commands() │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ looper_write_ │
|
||||||
|
│ │ status() │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────┼────────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ write("/tmp/looper_status")
|
||||||
|
│
|
||||||
|
│ read("/tmp/looper_status") ◄──────────── (non‑blocking open)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
▼
|
||||||
|
parse_status_line(...)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
cell_state[ch] = state
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
draw_grid() ──► state_to_color(state) returns colour pair
|
||||||
|
apply colour to cell
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Bindings
|
||||||
|
|
||||||
|
| Key | Action | FIFO command sent |
|
||||||
|
|------------------|---------------------------------------------|------------------------------|
|
||||||
|
| `h` / `←` | Move selection left | (none) |
|
||||||
|
| `j` / `↓` | Move selection down | (none) |
|
||||||
|
| `k` / `↑` | Move selection up | (none) |
|
||||||
|
| `l` / `→` | Move selection right | (none) |
|
||||||
|
| `t` | Record / toggle on selected column | `record <col>\n` |
|
||||||
|
| `s` | Next scene | `scene_next\n` |
|
||||||
|
| `S` | Previous scene | `scene_prev\n` |
|
||||||
|
| `d` / `D` | Stop all channels | `stop\n` |
|
||||||
|
| `a` | Add audio channel | `add\n` |
|
||||||
|
| `A` | Add MIDI channel | `add_midi\n` |
|
||||||
|
| `r` | Remove last dynamic channel | `remove\n` |
|
||||||
|
| `b` | Bind to selected column | `bind <col>\n` |
|
||||||
|
| `u` | Unbind (reset to channel 0) | `unbind\n` |
|
||||||
|
| `?` | Toggle help text | (none) |
|
||||||
|
| `Esc` / `Q` | Quit | (none) |
|
||||||
|
|
||||||
|
## Status Line Format
|
||||||
|
|
||||||
|
Each line written by the engine to `/tmp/looper_status` follows this pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
CH=<channel_number> SC=<scene_index> STATE=<state_string>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<state_string>` is one of `IDLE`, `RECORD`, `LOOPING`, `PAUSED`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
CH=0 SC=0 STATE=RECORD
|
||||||
|
CH=1 SC=0 STATE=LOOPING
|
||||||
|
```
|
||||||
|
|
||||||
|
The client parses these lines and updates the colour of the corresponding cell:
|
||||||
|
|
||||||
|
- `IDLE` → white (`COLOR_EMPTY`)
|
||||||
|
- `RECORD` → red (`COLOR_RECORDING`)
|
||||||
|
- `LOOPING` → green (`COLOR_LOOPING`)
|
||||||
|
- `PAUSED` → blue (`COLOR_STOPPED`)
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd engine
|
||||||
|
make # produces `looper`
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd client
|
||||||
|
make # produces `looper-client`
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Together
|
||||||
|
|
||||||
|
1. Start the JACK server (e.g., `jackd -d alsa` or `pipewire`).
|
||||||
|
2. In a terminal, start the engine:
|
||||||
|
```sh
|
||||||
|
cd engine && ./looper
|
||||||
|
```
|
||||||
|
3. In another terminal, start the client:
|
||||||
|
```sh
|
||||||
|
cd client && ./looper-client
|
||||||
|
```
|
||||||
|
4. Use the TUI keys described above.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
When the client exits, it deletes both FIFOs (`/tmp/looper_cmd` and `/tmp/looper_status`).
|
||||||
|
If the engine is still running, it will continue to try to write to the status FIFO; that write will fail silently (the engine uses `O_NONBLOCK` and ignores errors).
|
||||||
|
The engine creates the status FIFO on startup and does not delete it.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **Unit test for status line parser**: `make test` in `client/` runs `test_status_parse`.
|
||||||
|
- **Integration test for status FIFO** (engine side): `make test` in `engine/tests/` runs `test_status_fifo`.
|
||||||
|
|
||||||
|
These are **not** executed automatically from the top‑level `make test` – they must be invoked manually or added to the top‑level Makefile.
|
||||||
|
|
||||||
|
The engine status FIFO test (`test_status_fifo`) uses `select()` with a timeout and retry loop to wait for a status line showing `STATE=RECORD`. It is reliable and does not hang.
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
- Replace dead stubs (`FuzzySearch`, `marks`, `yank_buffer`, visual mode) with real implementations or remove them.
|
||||||
|
- Support transport play/pause via a dedicated FIFO command.
|
||||||
|
- Allow the client to display multiple scenes per channel (e.g., via a tab or side panel).
|
||||||
|
- Graceful error recovery when the engine or FIFO is not available.
|
||||||
38
engine/docs/11-arbitrary-number-of-channels.md
Normal file
38
engine/docs/11-arbitrary-number-of-channels.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Arbitrary Number of Channels
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Originally the looper had a fixed maximum of 16 channels (`MAX_CHANNELS = 16`).
|
||||||
|
The limitation has been removed; channels are now stored in a **dynamically allocated array** that grows on demand.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
- The global `channels` is a pointer (`struct channel_t *_Atomic channels`) instead of a fixed‑size array.
|
||||||
|
- An atomic variable `channel_capacity` tracks the allocated size.
|
||||||
|
- Initial allocation is for 8 channels; when a channel index >= current capacity is needed, the array is doubled.
|
||||||
|
- The old array is **not freed immediately** – it is kept alive for at least one real‑time audio cycle (using the same deferred mechanism as port unregistration) to guarantee that the RT callback never accesses freed memory.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|--------------------|-----------------------------------------------------------|
|
||||||
|
| `src/channel.h` | Removes `MAX_CHANNELS`, adds `channels` pointer declaration and `get_channels_array()` inline accessor. |
|
||||||
|
| `src/looper.c` | Contains `ensure_capacity()`, deferred free, and replaces all fixed‑size loop bounds with `channel_capacity`. |
|
||||||
|
| `src/channel.c` | Adapted to use the current array pointer atomically. |
|
||||||
|
| `src/midi.c` | Uses `atomic_load(&channel_capacity)` for bounds checks. |
|
||||||
|
|
||||||
|
## Thread Safety During Resize
|
||||||
|
|
||||||
|
1. A new, larger array is allocated (`calloc`).
|
||||||
|
2. Existing channels are copied via `memcpy`.
|
||||||
|
3. The global `channels` pointer is swapped with `atomic_exchange`.
|
||||||
|
4. `channel_capacity` is updated.
|
||||||
|
5. The old pointer is stored in `pending_old` along with the current cycle count (`pending_old_cycle`).
|
||||||
|
6. In the main loop, `pending_old` is freed only after `global_rt_cycles` has advanced by at least 1, ensuring any RT callback that loaded the old pointer has finished.
|
||||||
|
|
||||||
|
This is a lightweight RCU‑like pattern that avoids locks and keeps the RT path deterministic.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
All existing MIDI commands and FIFO pipe commands work unchanged with the dynamic array.
|
||||||
|
The maximum practical number of channels is limited only by available memory and JACK port limits (typically 1024 per client on modern systems).
|
||||||
0
engine/docs/12-command-architecture
Normal file
0
engine/docs/12-command-architecture
Normal file
65
engine/docs/12-command-architecture.md
Normal file
65
engine/docs/12-command-architecture.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Command Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The looper uses a **lock‑free, single‑producer single‑consumer (SPSC)** command queue to communicate between the real‑time JACK audio thread and the main (non‑RT) thread.
|
||||||
|
There are two families of queues:
|
||||||
|
|
||||||
|
- **`cmd_queue`** (RT‑safe) – used for commands that can be handled directly inside the process callback (`CMD_CYCLE`, `CMD_STOP`, `CMD_BIND_CHANNEL`, `CMD_UNBIND`).
|
||||||
|
The producer is the MIDI handler (`midi_handle_events`) or the FIFO pipe reader (`pipe_thread_func`); the consumer is `process_callback`.
|
||||||
|
|
||||||
|
- **`cmd_queue_main_midi`** / **`cmd_queue_main_fifo`** – used for commands that require memory allocation or JACK API calls (`CMD_ADD_CHANNEL`, `CMD_REMOVE_CHANNEL`).
|
||||||
|
The producer is the MIDI handler (or FIFO reader), and the consumer is `looper_process_commands`, which runs in the main loop approximately every 50 ms.
|
||||||
|
|
||||||
|
## Command Types
|
||||||
|
|
||||||
|
The `command_t` struct (defined in `command.h`) contains:
|
||||||
|
|
||||||
|
- `type` – one of the `cmd_type_t` enumerators.
|
||||||
|
- `channel` – target channel index; `-1` means “current bind channel” for some commands.
|
||||||
|
- `data` – extra parameter (e.g., bind channel number for `CMD_BIND_CHANNEL`).
|
||||||
|
|
||||||
|
### RT‑safe Commands (pushed to `cmd_queue`)
|
||||||
|
|
||||||
|
| Type | Effect |
|
||||||
|
|--------------------|---------------------------------------------------------------------|
|
||||||
|
| `CMD_CYCLE` | Toggle the state machine of the target channel (IDLE→RECORD→LOOPING→PAUSED→LOOPING…). |
|
||||||
|
| `CMD_STOP` | Force the target channel (or all channels, if `channel == -1`) to `STATE_IDLE`. |
|
||||||
|
| `CMD_BIND_CHANNEL` | Set the global `bind_channel` index to `data`. |
|
||||||
|
| `CMD_UNBIND` | Reset `bind_channel` to 0. |
|
||||||
|
|
||||||
|
### Main‑thread Commands (pushed to `cmd_queue_main_midi` / `cmd_queue_main_fifo`)
|
||||||
|
|
||||||
|
| Type | Effect |
|
||||||
|
|---------------------|---------------------------------------------------------------------|
|
||||||
|
| `CMD_ADD_CHANNEL` | Create a new dynamic channel (port registration). |
|
||||||
|
| `CMD_REMOVE_CHANNEL`| Remove the highest‑numbered active dynamic channel (excluding channel 0). |
|
||||||
|
|
||||||
|
## Command Flow
|
||||||
|
|
||||||
|
1. **MIDI input** – `midi_handle_events` parses incoming note‑on events and decides which command to push.
|
||||||
|
RT‑safe commands are pushed to `cmd_queue`; add/remove commands are pushed to `cmd_queue_main_midi`.
|
||||||
|
|
||||||
|
2. **FIFO input** – `pipe_thread_func` reads lines from `/tmp/looper_cmd` and pushes the corresponding command.
|
||||||
|
RT‑safe commands go to `cmd_queue`; add/remove go to `cmd_queue_main_fifo`.
|
||||||
|
|
||||||
|
3. **Process callback** – `process_callback` is invoked by JACK for each audio cycle. It drains `cmd_queue` and applies each command via `apply_command`. This function modifies the channel state and bind index atomically.
|
||||||
|
|
||||||
|
4. **Main loop** – `looper_process_commands` is called in the main loop (≈ every 50 ms). It drains `cmd_queue_main_midi` and `cmd_queue_main_fifo`, performing the necessary port registrations/unregistrations and calling `channel_add` / `channel_remove`.
|
||||||
|
|
||||||
|
## Deferred Port Unregistration
|
||||||
|
|
||||||
|
When a dynamic channel is removed, the RT thread first sets `active = 0`. The main thread waits until it has seen at least one full RT cycle pass (using `global_rt_cycles`) before calling `jack_port_unregister`. This prevents a race between the RT thread still holding a reference to the port buffer and the port being unregistered.
|
||||||
|
|
||||||
|
## SPSC Queue Implementation
|
||||||
|
|
||||||
|
The queue itself (defined in `queue.c`/`queue.h`) is a simple circular buffer with head and tail indices. It uses C11 atomic loads/stores with appropriate memory ordering (`memory_order_acquire`/`memory_order_release`) to guarantee visibility without locks. Capacity is fixed at `QUEUE_CAPACITY` (256 commands). Push/pop operations are O(1) and never block.
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
- The JACK process callback runs in an RT thread.
|
||||||
|
- The MIDI handler runs inside the process callback (it is called from `process_callback`).
|
||||||
|
- The FIFO reader lives in a separate POSIX thread.
|
||||||
|
- The main thread runs the rest of the program.
|
||||||
|
|
||||||
|
The two‑queue design ensures that memory‑allocating operations never happen inside the RT thread, while RT‑pertinent commands are processed with minimal latency.
|
||||||
90
engine/docs/2-midi-looping.md
Normal file
90
engine/docs/2-midi-looping.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Per‑Channel MIDI Looping
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each looper channel can be either **audio** or **MIDI**. Audio channels record and loop audio samples (existing behaviour). MIDI channels record and loop MIDI event sequences, using separate JACK MIDI input/output ports. The state machine (`IDLE → RECORD → LOOPING → PAUSED`) operates identically for both types.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Source | Action |
|
||||||
|
|----------------------------|-----------------|------------------------------------------------------------|
|
||||||
|
| `CMD_ADD_MIDI_CHANNEL` | MIDI note 66 | Adds a new MIDI looping channel |
|
||||||
|
| `add_midi` | FIFO pipe | Same |
|
||||||
|
| `CMD_REMOVE_CHANNEL` | MIDI note 61 | Removes the last‑added channel (audio or MIDI) |
|
||||||
|
| `CMD_CYCLE` | any note binding| Toggles channel state (IDLE→RECORD→LOOPING→PAUSED) |
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
When a MIDI channel is created, two JACK MIDI ports are registered:
|
||||||
|
|
||||||
|
- `looper:channel<N>_midi_in` (input)
|
||||||
|
- `looper:channel<N>_midi_out` (output)
|
||||||
|
|
||||||
|
The `<N>` is a global counter, independent of the index inside the internal channel array.
|
||||||
|
|
||||||
|
## Recording
|
||||||
|
|
||||||
|
During `STATE_RECORD`:
|
||||||
|
|
||||||
|
1. All incoming MIDI events on the `_midi_in` port are stored in the channel’s event buffer, along with their frame offset relative to the start of the recording.
|
||||||
|
2. The incoming events are also **forwarded** to the `_midi_out` port, providing a direct pass‑through during recording.
|
||||||
|
|
||||||
|
**Buffer limit:** A channel can hold up to `MAX_MIDI_EVENTS` (1024) events.
|
||||||
|
|
||||||
|
## Looping
|
||||||
|
|
||||||
|
During `STATE_LOOPING`:
|
||||||
|
|
||||||
|
- All recorded events are output at the **start** of every cycle (frame 0). This is a simplification; no per‑event timestamp scheduling is implemented. The loop length is determined by the total number of recorded events.
|
||||||
|
|
||||||
|
## Pass‑Through
|
||||||
|
|
||||||
|
During `STATE_IDLE` (and `STATE_PAUSED` for MIDI) incoming MIDI events are **copied** from `_midi_in` to `_midi_out` unchanged.
|
||||||
|
|
||||||
|
## FIFO Pipe Commands
|
||||||
|
|
||||||
|
The FIFO pipe at `/tmp/looper_cmd` accepts the following new line‑based commands:
|
||||||
|
|
||||||
|
| Command | Effect |
|
||||||
|
|---------------|--------------------------------------------|
|
||||||
|
| `add_midi` | Adds a MIDI channel |
|
||||||
|
| `stop` | Resets all channels to idle |
|
||||||
|
| `bind <ch>` | Binds the next control note to channel `<ch>` |
|
||||||
|
| `unbind` | Resets binding to channel 0 |
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
1. Start the looper.
|
||||||
|
2. Connect a MIDI keyboard to `looper:channel1_midi_in`.
|
||||||
|
3. Send MIDI note 66 on `looper:control` to create a MIDI channel.
|
||||||
|
4. Send a CYCLE command (e.g., MIDI note 62 under control key) to start recording.
|
||||||
|
5. Play notes on the keyboard – the events are captured.
|
||||||
|
6. Send CYCLE again to enter LOOPING mode – the captured sequence repeats.
|
||||||
|
7. Send CYCLE again to pause, or send STOP (note 65 under control key) to reset.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
- **Channel structure** (`struct channel_t` in `channel.h`):
|
||||||
|
- `type` field (`CHANNEL_AUDIO` or `CHANNEL_MIDI`)
|
||||||
|
- `loop` union containing `audio_buffer[MAX_BUFFER]` or `midi_events[MAX_MIDI_EVENTS]`
|
||||||
|
- **MIDI event type** (`midi_event_t`):
|
||||||
|
- `timestamp` (frame offset relative to loop start)
|
||||||
|
- `status`, `note`, `velocity`
|
||||||
|
- **Processing** (`process_callback` in `looper.c`):
|
||||||
|
- The callback checks `type` before routing to the appropriate handler block.
|
||||||
|
- MIDI handler reads from `midi_in` port, writes to `midi_out` port.
|
||||||
|
- **Port cleanup**: On channel removal, both MIDI ports are unregistered via `jack_port_unregister()` after a one‑RT‑cycle grace period.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Integration tests in `tests/integration.c` cover:
|
||||||
|
|
||||||
|
- `test_midi_channel_add` – verifies that sending `add_midi` via FIFO creates `looper:channel<N>_midi_in` ports.
|
||||||
|
- `test_fifo_stop_bind_unbind` – verifies that `stop`, `bind`, and `unbind` FIFO commands are processed correctly.
|
||||||
|
- Other existing tests continue to verify audio‑only functionality.
|
||||||
|
|
||||||
|
Run the test suite with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
91
engine/docs/4-implement-scene-switching-engine.md
Normal file
91
engine/docs/4-implement-scene-switching-engine.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Scene Switching Engine
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The scene switching engine allows a channel to have multiple independent recording/playback states (scenes).
|
||||||
|
Only one scene per channel is active at a time. The active scene's state (IDLE / RECORD / LOOPING / PAUSED) is
|
||||||
|
controlled independently of other scenes.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
Each `channel_t` holds an array of up to `MAX_SCENES` (16) `scene_t` structures. Two atomic integers keep track
|
||||||
|
of the number of scenes and which scene is currently active:
|
||||||
|
|
||||||
|
```c
|
||||||
|
atomic_int scene_count; // number of scenes for this channel
|
||||||
|
atomic_int current_scene; // index of the active scene (0 ≤ current_scene < scene_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `scene_t` contains the loop buffer (audio or MIDI events) and the per‑scene atomic state:
|
||||||
|
|
||||||
|
```c
|
||||||
|
union {
|
||||||
|
float audio_buffer[LOOP_BUF_SIZE];
|
||||||
|
midi_event_t midi_events[MAX_MIDI_EVENTS];
|
||||||
|
} loop;
|
||||||
|
|
||||||
|
atomic_int loop_count;
|
||||||
|
atomic_int record_pos;
|
||||||
|
atomic_int playback_pos;
|
||||||
|
atomic_int state; // STATE_IDLE / STATE_RECORD / STATE_LOOPING / STATE_PAUSED
|
||||||
|
atomic_int prev_state; // previous state (used by RT callback to detect transitions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Trigger (MIDI) | Trigger (FIFO) | Effect |
|
||||||
|
|--------------------------|------------------------|-----------------------|---------------------------------------------------------|
|
||||||
|
| **CMD_NEXT_SCENE** | note 67 (control key) | `scene_next\n` | Increments `current_scene` (wraps around). |
|
||||||
|
| **CMD_PREV_SCENE** | note 68 (control key) | `scene_prev\n` | Decrements `current_scene` (wraps around). |
|
||||||
|
| **CMD_ADD_SCENE** | note 69 (control key) | `scene_add\n` | Appends a new empty scene, increments `scene_count`. |
|
||||||
|
| **CMD_REMOVE_SCENE** | note 70 (control key) | `scene_remove\n` | Removes the current scene (shifts remaining scenes). |
|
||||||
|
|
||||||
|
All scene commands are processed on the main loop (not in the RT callback). They are pushed to
|
||||||
|
`cmd_queue_main_midi` (for MIDI) or `cmd_queue_main_fifo` (for FIFO) and applied by
|
||||||
|
`looper_process_commands()`.
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
- `scene_count` and `current_scene` are `atomic_int`; all reads/writes use `atomic_load`/`atomic_store`.
|
||||||
|
- The per‑scene fields (`loop_count`, `record_pos`, `playback_pos`, `state`, `prev_state`) are also `atomic_int`,
|
||||||
|
so the RT callback and the main loop can safely read and write them concurrently.
|
||||||
|
- The audio loop buffer itself (a plain `float` array) is not atomic. During scene removal the buffer is copied
|
||||||
|
via `memcpy`. If a scene is actively looping, this copy may produce a temporarily inconsistent buffer.
|
||||||
|
**Known limitation:** scene removal should only be performed when the channel is idle (all scenes in
|
||||||
|
`STATE_IDLE`). The integration test `test_scene_add_remove` does exactly this.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
1. **`channel_add_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- Checks `scene_count < MAX_SCENES` (atomically).
|
||||||
|
- Calls `init_scene()` to zero the new scene and set its state to `STATE_IDLE`.
|
||||||
|
- Atomically increments `scene_count`.
|
||||||
|
|
||||||
|
2. **`channel_remove_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- Refuses if `scene_count <= 1` (at least one scene must always exist).
|
||||||
|
- Shifts all scenes after the current one down one position – each scene field is copied with
|
||||||
|
`atomic_store`/`atomic_load`.
|
||||||
|
- The audio buffer is copied with `memcpy` (see limitation above).
|
||||||
|
- Decrements `scene_count` and adjusts `current_scene` if it would become out of bounds.
|
||||||
|
|
||||||
|
3. **`channel_next_scene` / `channel_prev_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- If `scene_count > 1`, atomically increments/decrements `current_scene` (wrapping using modulo).
|
||||||
|
|
||||||
|
4. **RT callback (`process_callback`)**
|
||||||
|
- At the start of each frame it reads `current_scene` atomically to obtain the scene index for that
|
||||||
|
channel.
|
||||||
|
- All per‑scene reads (state, loop_count, record_pos, playback_pos) use `atomic_load`.
|
||||||
|
- When the state changes, the callback atomically resets `record_pos`, `loop_count`, `playback_pos`
|
||||||
|
as appropriate.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `test_scene_add_remove` (FIFO) – adds a scene, cycles next, removes the scene, exits.
|
||||||
|
- `test_scene_next_prev_midi` – sends control key + notes 67/68 to switch scenes.
|
||||||
|
- `test_scene_cycle_per_scene` – records a loop on scene 0, switches to scene 1, verifies scene 1 is idle.
|
||||||
|
- `test_scene_add_remove_midi` – sends control key + notes 69/70 to add/remove scenes.
|
||||||
|
|
||||||
|
All scene tests pass as part of `make test`.
|
||||||
BIN
engine/integration_test
Executable file
BIN
engine/integration_test
Executable file
Binary file not shown.
BIN
engine/looper
Executable file
BIN
engine/looper
Executable file
Binary file not shown.
36
engine/makefile
Normal file
36
engine/makefile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
CC ?= gcc
|
||||||
|
CFLAGS ?= -Wall -Wextra -g -Isrc
|
||||||
|
LDFLAGS ?= -ljack -lm
|
||||||
|
|
||||||
|
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c
|
||||||
|
OBJ = $(SRC:.c=.o)
|
||||||
|
|
||||||
|
looper: $(OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
src/%.o: src/%.c
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
integration: looper tests/integration.c
|
||||||
|
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm
|
||||||
|
|
||||||
|
test_status_fifo: looper tests/test_status_fifo.c
|
||||||
|
$(CC) $(CFLAGS) -o test_status_fifo tests/test_status_fifo.c -ljack -lm
|
||||||
|
|
||||||
|
test: integration test_status_fifo
|
||||||
|
./test_status_fifo
|
||||||
|
./integration_test
|
||||||
|
|
||||||
|
.PHONY: clean integration test_status_fifo test
|
||||||
|
clean:
|
||||||
|
rm -f looper integration_test test_status_fifo src/*.o
|
||||||
|
|
||||||
|
check:
|
||||||
|
cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix
|
||||||
|
|
||||||
|
# Optional: Format code using clang-format
|
||||||
|
format:
|
||||||
|
clang-format -i src/*.c
|
||||||
|
|
||||||
|
install-hooks:
|
||||||
|
git config core.hooksPath .githooks
|
||||||
136
engine/src/channel.c
Normal file
136
engine/src/channel.c
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "channel.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_add(jack_client_t *client, int idx) {
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
cur[idx].audio_in = jack_port_register(
|
||||||
|
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
|
cur[idx].audio_out = jack_port_register(
|
||||||
|
client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
|
if (!cur[idx].audio_in || !cur[idx].audio_out) {
|
||||||
|
fprintf(stderr, "Failed to register ports for channel %d\n",
|
||||||
|
next_channel_id);
|
||||||
|
atomic_store(&cur[idx].active, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&cur[idx].active, 1);
|
||||||
|
cur[idx].type = CHANNEL_AUDIO;
|
||||||
|
atomic_store(&cur[idx].scene_count, 1);
|
||||||
|
atomic_store(&cur[idx].current_scene, 0);
|
||||||
|
init_scene(&cur[idx].scenes[0]);
|
||||||
|
|
||||||
|
next_channel_id++;
|
||||||
|
atomic_fetch_add(&channel_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_add_midi(jack_client_t *client, int idx) {
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
|
||||||
|
char in_name[64], out_name[64];
|
||||||
|
snprintf(in_name, sizeof(in_name), "channel%d_midi_in", next_channel_id);
|
||||||
|
snprintf(out_name, sizeof(out_name), "channel%d_midi_out", next_channel_id);
|
||||||
|
|
||||||
|
cur[idx].midi_in = jack_port_register(client, in_name, JACK_DEFAULT_MIDI_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
cur[idx].midi_out = jack_port_register(
|
||||||
|
client, out_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
|
||||||
|
if (!cur[idx].midi_in || !cur[idx].midi_out) {
|
||||||
|
fprintf(stderr, "Failed to register MIDI ports for channel %d\n",
|
||||||
|
next_channel_id);
|
||||||
|
atomic_store(&cur[idx].active, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&cur[idx].active, 1);
|
||||||
|
cur[idx].type = CHANNEL_MIDI;
|
||||||
|
atomic_store(&cur[idx].scene_count, 1);
|
||||||
|
atomic_store(&cur[idx].current_scene, 0);
|
||||||
|
init_scene(&cur[idx].scenes[0]);
|
||||||
|
|
||||||
|
next_channel_id++;
|
||||||
|
atomic_fetch_add(&channel_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_remove(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
engine/src/channel.h
Normal file
78
engine/src/channel.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#ifndef CHANNEL_H
|
||||||
|
#define CHANNEL_H
|
||||||
|
|
||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
|
#define LOOP_BUF_SIZE (5 * 48000)
|
||||||
|
|
||||||
|
#define MAX_MIDI_EVENTS 1024
|
||||||
|
|
||||||
|
#define MAX_SCENES 16
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CHANNEL_AUDIO,
|
||||||
|
CHANNEL_MIDI
|
||||||
|
} channel_type_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
jack_nframes_t timestamp; /* frame offset relative to loop start */
|
||||||
|
unsigned char status;
|
||||||
|
unsigned char note;
|
||||||
|
unsigned char velocity;
|
||||||
|
} midi_event_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
STATE_IDLE,
|
||||||
|
STATE_RECORD,
|
||||||
|
STATE_LOOPING,
|
||||||
|
STATE_PAUSED
|
||||||
|
} looper_state;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
float audio_buffer[LOOP_BUF_SIZE];
|
||||||
|
midi_event_t midi_events[MAX_MIDI_EVENTS];
|
||||||
|
} loop;
|
||||||
|
atomic_int loop_count;
|
||||||
|
atomic_int record_pos;
|
||||||
|
atomic_int playback_pos;
|
||||||
|
atomic_int state;
|
||||||
|
atomic_int prev_state;
|
||||||
|
} scene_t;
|
||||||
|
|
||||||
|
struct channel_t {
|
||||||
|
channel_type_t type;
|
||||||
|
atomic_int active;
|
||||||
|
jack_port_t *audio_in;
|
||||||
|
jack_port_t *audio_out;
|
||||||
|
jack_port_t *midi_in;
|
||||||
|
jack_port_t *midi_out;
|
||||||
|
scene_t scenes[MAX_SCENES];
|
||||||
|
atomic_int scene_count;
|
||||||
|
atomic_int current_scene;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Globals declared in looper.c */
|
||||||
|
extern struct channel_t *_Atomic channels;
|
||||||
|
extern atomic_int channel_capacity;
|
||||||
|
extern atomic_int channel_count;
|
||||||
|
extern int next_channel_id;
|
||||||
|
|
||||||
|
/* Safe accessor for the real‑time thread (returns a snapshot of the current pointer) */
|
||||||
|
static inline struct channel_t *get_channels_array(void) {
|
||||||
|
return atomic_load(&channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channel_add(jack_client_t *client, int idx);
|
||||||
|
void channel_remove(jack_client_t *client, int idx);
|
||||||
|
void channel_add_midi(jack_client_t *client, int idx);
|
||||||
|
|
||||||
|
/* Scene management (called from main loop) */
|
||||||
|
void channel_add_scene(jack_client_t *client, int idx);
|
||||||
|
void channel_remove_scene(jack_client_t *client, int idx);
|
||||||
|
void channel_next_scene(jack_client_t *client, int idx);
|
||||||
|
void channel_prev_scene(jack_client_t *client, int idx);
|
||||||
|
|
||||||
|
#endif
|
||||||
BIN
engine/src/channel.o
Normal file
BIN
engine/src/channel.o
Normal file
Binary file not shown.
24
engine/src/command.h
Normal file
24
engine/src/command.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef COMMAND_H
|
||||||
|
#define COMMAND_H
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CMD_CYCLE, // toggle record/stop for the current scene of a channel
|
||||||
|
CMD_STOP, // force to idle for all scenes
|
||||||
|
CMD_BIND_CHANNEL, // bind a channel index (data = channel)
|
||||||
|
CMD_UNBIND, // reset bind to channel 0
|
||||||
|
CMD_ADD_CHANNEL, // add a new dynamic channel
|
||||||
|
CMD_REMOVE_CHANNEL, // remove last dynamic channel
|
||||||
|
CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel
|
||||||
|
CMD_NEXT_SCENE,
|
||||||
|
CMD_PREV_SCENE,
|
||||||
|
CMD_ADD_SCENE,
|
||||||
|
CMD_REMOVE_SCENE,
|
||||||
|
} cmd_type_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cmd_type_t type;
|
||||||
|
int channel; // which channel; -1 means "current/bound"
|
||||||
|
int data; // extra parameter (e.g. bind channel number)
|
||||||
|
} command_t;
|
||||||
|
|
||||||
|
#endif
|
||||||
676
engine/src/looper.c
Normal file
676
engine/src/looper.c
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "looper.h"
|
||||||
|
#include "channel.h"
|
||||||
|
#include "command.h"
|
||||||
|
#include "midi.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define STATUS_FIFO "/tmp/looper_status"
|
||||||
|
|
||||||
|
static void looper_write_status(void) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global state (shared across files) */
|
||||||
|
struct channel_t *_Atomic channels = NULL;
|
||||||
|
atomic_int channel_capacity = 0;
|
||||||
|
atomic_int channel_count = 0;
|
||||||
|
int next_channel_id = 1;
|
||||||
|
spsc_queue_t cmd_queue_main_midi;
|
||||||
|
spsc_queue_t cmd_queue_main_fifo;
|
||||||
|
atomic_int global_rt_cycles = 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;
|
||||||
|
spsc_queue_t cmd_queue;
|
||||||
|
|
||||||
|
/* Deferred removal index and cycle counter */
|
||||||
|
static int pending_unregister_idx = -1;
|
||||||
|
static int pending_unregister_cycle = 0;
|
||||||
|
|
||||||
|
/* Deferred free of old channel array (must not free while RT thread may hold
|
||||||
|
* pointer) */
|
||||||
|
static struct channel_t *pending_old = NULL;
|
||||||
|
static int pending_old_cycle = 0;
|
||||||
|
|
||||||
|
/* Helper: grow the channel array so that index idx is valid */
|
||||||
|
static int ensure_capacity(jack_client_t *client, int idx) {
|
||||||
|
(void)client;
|
||||||
|
int cur_cap = atomic_load(&channel_capacity);
|
||||||
|
if (idx < cur_cap)
|
||||||
|
return 0;
|
||||||
|
int new_cap = cur_cap == 0 ? 8 : cur_cap;
|
||||||
|
while (new_cap <= idx)
|
||||||
|
new_cap *= 2;
|
||||||
|
struct channel_t *new_arr = calloc(new_cap, sizeof(struct channel_t));
|
||||||
|
if (!new_arr)
|
||||||
|
return -1;
|
||||||
|
/* copy existing channels */
|
||||||
|
if (cur_cap > 0)
|
||||||
|
memcpy(new_arr, atomic_load(&channels), cur_cap * sizeof(struct channel_t));
|
||||||
|
/* atomically publish new array, defer free of old */
|
||||||
|
struct channel_t *old = atomic_exchange(&channels, new_arr);
|
||||||
|
atomic_store(&channel_capacity, new_cap);
|
||||||
|
/* schedule old pointer for later deallocation (after RT cycle) */
|
||||||
|
pending_old = old;
|
||||||
|
pending_old_cycle = atomic_load(&global_rt_cycles);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void apply_command(command_t cmd) {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
struct channel_t *cur = get_channels_array();
|
||||||
|
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_CYCLE:
|
||||||
|
if (cmd.channel >= 0 && cmd.channel < cap) {
|
||||||
|
int sc_idx = atomic_load(&cur[cmd.channel].current_scene);
|
||||||
|
scene_t *sc = &cur[cmd.channel].scenes[sc_idx];
|
||||||
|
int cst = atomic_load(&sc->state);
|
||||||
|
int next;
|
||||||
|
switch (cst) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
next = STATE_RECORD;
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
next = STATE_LOOPING;
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
next = STATE_PAUSED;
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
next = STATE_LOOPING;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
next = STATE_IDLE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
atomic_store(&sc->state, next);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_STOP:
|
||||||
|
if (cmd.channel >= 0 && cmd.channel < cap) {
|
||||||
|
struct channel_t *ch = &cur[cmd.channel];
|
||||||
|
int sc_cnt = atomic_load(&ch->scene_count);
|
||||||
|
for (int s = 0; s < sc_cnt; s++) {
|
||||||
|
atomic_store(&ch->scenes[s].state, STATE_IDLE);
|
||||||
|
atomic_store(&ch->scenes[s].loop_count, 0);
|
||||||
|
atomic_store(&ch->scenes[s].record_pos, 0);
|
||||||
|
atomic_store(&ch->scenes[s].playback_pos, 0);
|
||||||
|
atomic_store(&ch->scenes[s].prev_state, -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < cap; i++) {
|
||||||
|
struct channel_t *ch = &cur[i];
|
||||||
|
int sc_cnt = atomic_load(&ch->scene_count);
|
||||||
|
for (int s = 0; s < sc_cnt; s++) {
|
||||||
|
atomic_store(&ch->scenes[s].state, STATE_IDLE);
|
||||||
|
atomic_store(&ch->scenes[s].loop_count, 0);
|
||||||
|
atomic_store(&ch->scenes[s].record_pos, 0);
|
||||||
|
atomic_store(&ch->scenes[s].playback_pos, 0);
|
||||||
|
atomic_store(&ch->scenes[s].prev_state, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CMD_BIND_CHANNEL:
|
||||||
|
atomic_store(&bind_channel, cmd.data);
|
||||||
|
break;
|
||||||
|
case CMD_UNBIND:
|
||||||
|
atomic_store(&bind_channel, 0);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drain RT‑safe commands */
|
||||||
|
command_t cmd;
|
||||||
|
while (queue_pop(&cmd_queue, &cmd)) {
|
||||||
|
apply_command(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* process each active channel */
|
||||||
|
struct channel_t *active_channels = get_channels_array();
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
for (int c = 0; c < cap; c++) {
|
||||||
|
if (!atomic_load(&active_channels[c].active))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* Guard against NULL ports (e.g. if port registration failed) */
|
||||||
|
if (active_channels[c].type == CHANNEL_AUDIO) {
|
||||||
|
if (!active_channels[c].audio_in || !active_channels[c].audio_out) {
|
||||||
|
fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n",
|
||||||
|
c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* CHANNEL_MIDI */
|
||||||
|
if (!active_channels[c].midi_in || !active_channels[c].midi_out) {
|
||||||
|
fprintf(stderr, "WARN: channel %d has NULL MIDI port(s), skipping\n",
|
||||||
|
c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Obtain current scene pointer */
|
||||||
|
int sc_idx = atomic_load(&active_channels[c].current_scene);
|
||||||
|
scene_t *sc = &active_channels[c].scenes[sc_idx];
|
||||||
|
|
||||||
|
const jack_default_audio_sample_t *in =
|
||||||
|
(const jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
|
active_channels[c].audio_in, nframes);
|
||||||
|
jack_default_audio_sample_t *out =
|
||||||
|
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
|
active_channels[c].audio_out, nframes);
|
||||||
|
if (!out)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int state = atomic_load(&sc->state);
|
||||||
|
int prev_state = atomic_load(&sc->prev_state);
|
||||||
|
|
||||||
|
if (state != prev_state) {
|
||||||
|
switch (state) {
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&sc->record_pos, 0);
|
||||||
|
atomic_store(&sc->loop_count, 0);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
if (atomic_load(&sc->record_pos) > 0)
|
||||||
|
atomic_store(&sc->loop_count, atomic_load(&sc->record_pos));
|
||||||
|
atomic_store(&sc->playback_pos, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&sc->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: {
|
||||||
|
struct channel_t *cur = atomic_load(&channels);
|
||||||
|
int sc_idx = atomic_load(&cur[0].current_scene);
|
||||||
|
int s = atomic_load(&cur[0].scenes[sc_idx].state);
|
||||||
|
if (s == STATE_IDLE)
|
||||||
|
atomic_store(&cur[0].scenes[sc_idx].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xFC: {
|
||||||
|
struct channel_t *cur = atomic_load(&channels);
|
||||||
|
int sc_idx = atomic_load(&cur[0].current_scene);
|
||||||
|
atomic_store(&cur[0].scenes[sc_idx].state, STATE_IDLE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xFB: {
|
||||||
|
struct channel_t *cur = atomic_load(&channels);
|
||||||
|
int sc_idx = atomic_load(&cur[0].current_scene);
|
||||||
|
int s = atomic_load(&cur[0].scenes[sc_idx].state);
|
||||||
|
if (s == STATE_PAUSED)
|
||||||
|
atomic_store(&cur[0].scenes[sc_idx].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_fetch_add_explicit(&global_rt_cycles, 1, memory_order_release);
|
||||||
|
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) {
|
||||||
|
/* create status FIFO (ignore if already exists) */
|
||||||
|
mkfifo(STATUS_FIFO, 0666);
|
||||||
|
|
||||||
|
queue_init(&cmd_queue);
|
||||||
|
queue_init(&cmd_queue_main_midi);
|
||||||
|
queue_init(&cmd_queue_main_fifo);
|
||||||
|
|
||||||
|
/* allocate initial array for at least one channel */
|
||||||
|
if (ensure_capacity(client, 0) != 0) {
|
||||||
|
fprintf(stderr, "Cannot allocate channel array\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
struct channel_t *init = atomic_load(&channels);
|
||||||
|
/* channel 0 */
|
||||||
|
atomic_store(&init[0].active, 1);
|
||||||
|
atomic_store(&init[0].scene_count, 1);
|
||||||
|
atomic_store(&init[0].current_scene, 0);
|
||||||
|
atomic_store(&init[0].scenes[0].loop_count, 0);
|
||||||
|
atomic_store(&init[0].scenes[0].record_pos, 0);
|
||||||
|
atomic_store(&init[0].scenes[0].playback_pos, 0);
|
||||||
|
atomic_store(&init[0].scenes[0].state, STATE_IDLE);
|
||||||
|
atomic_store(&init[0].scenes[0].prev_state, -1);
|
||||||
|
|
||||||
|
init[0].audio_in = jack_port_register(
|
||||||
|
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
|
init[0].audio_out = jack_port_register(
|
||||||
|
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
|
if (!init[0].audio_in || !init[0].audio_out) {
|
||||||
|
fprintf(stderr, "Could not create audio ports for channel 0\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
atomic_store(&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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* main‑loop command processing
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
void looper_process_commands(jack_client_t *client) {
|
||||||
|
/* Drain main‑loop command queues (add/remove) */
|
||||||
|
command_t cmd;
|
||||||
|
while (queue_pop(&cmd_queue_main_midi, &cmd)) {
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_ADD_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < cap; idx++)
|
||||||
|
if (!atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
break;
|
||||||
|
if (idx == cap) {
|
||||||
|
if (ensure_capacity(client, idx) != 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
channel_add(client, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_ADD_MIDI_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < cap; idx++)
|
||||||
|
if (!atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
break;
|
||||||
|
if (idx == cap) {
|
||||||
|
if (ensure_capacity(client, idx) != 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
channel_add_midi(client, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_REMOVE_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int remove_idx = -1;
|
||||||
|
for (int idx = 1; idx < cap; idx++)
|
||||||
|
if (atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
remove_idx = idx;
|
||||||
|
if (remove_idx != -1) {
|
||||||
|
channel_remove(client, remove_idx);
|
||||||
|
pending_unregister_idx = remove_idx;
|
||||||
|
pending_unregister_cycle = atomic_load(&global_rt_cycles);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_ADD_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_add_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_REMOVE_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_remove_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_NEXT_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_next_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_PREV_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_prev_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (queue_pop(&cmd_queue_main_fifo, &cmd)) {
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_ADD_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < cap; idx++)
|
||||||
|
if (!atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
break;
|
||||||
|
if (idx == cap) {
|
||||||
|
if (ensure_capacity(client, idx) != 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
channel_add(client, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_ADD_MIDI_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < cap; idx++)
|
||||||
|
if (!atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
break;
|
||||||
|
if (idx == cap) {
|
||||||
|
if (ensure_capacity(client, idx) != 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
channel_add_midi(client, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_REMOVE_CHANNEL: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int remove_idx = -1;
|
||||||
|
for (int idx = 1; idx < cap; idx++)
|
||||||
|
if (atomic_load(&(get_channels_array()[idx].active)))
|
||||||
|
remove_idx = idx;
|
||||||
|
if (remove_idx != -1) {
|
||||||
|
channel_remove(client, remove_idx);
|
||||||
|
pending_unregister_idx = remove_idx;
|
||||||
|
pending_unregister_cycle = atomic_load(&global_rt_cycles);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_ADD_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_add_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_REMOVE_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_remove_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_NEXT_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_next_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CMD_PREV_SCENE: {
|
||||||
|
int cap = atomic_load(&channel_capacity);
|
||||||
|
int bind = atomic_load(&bind_channel);
|
||||||
|
int ch = bind;
|
||||||
|
if (ch < cap) {
|
||||||
|
channel_prev_scene(client, ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Deferred port unregistration – wait until RT thread has seen active=0 */
|
||||||
|
if (pending_unregister_idx != -1) {
|
||||||
|
int current_cycle = atomic_load(&global_rt_cycles);
|
||||||
|
if (current_cycle - pending_unregister_cycle >= 1) {
|
||||||
|
int idx = pending_unregister_idx;
|
||||||
|
struct channel_t *cur = atomic_load(&channels);
|
||||||
|
if (cur[idx].audio_in)
|
||||||
|
jack_port_unregister(client, cur[idx].audio_in);
|
||||||
|
if (cur[idx].audio_out)
|
||||||
|
jack_port_unregister(client, cur[idx].audio_out);
|
||||||
|
if (cur[idx].midi_in)
|
||||||
|
jack_port_unregister(client, cur[idx].midi_in);
|
||||||
|
if (cur[idx].midi_out)
|
||||||
|
jack_port_unregister(client, cur[idx].midi_out);
|
||||||
|
pending_unregister_idx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Deferred free of old channel array – wait until RT thread has seen new
|
||||||
|
* pointer */
|
||||||
|
if (pending_old != NULL) {
|
||||||
|
int current_cycle = atomic_load(&global_rt_cycles);
|
||||||
|
if (current_cycle - pending_old_cycle >= 1) {
|
||||||
|
free(pending_old);
|
||||||
|
pending_old = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* write current state to status FIFO */
|
||||||
|
looper_write_status();
|
||||||
|
}
|
||||||
BIN
engine/src/looper.o
Normal file
BIN
engine/src/looper.o
Normal file
Binary file not shown.
@@ -1,10 +1,11 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
|
#include "pipe.h"
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
(void)argc;
|
(void)argc;
|
||||||
@@ -33,6 +34,12 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pipe_start_reader() != 0) {
|
||||||
|
fprintf(stderr, "pipe reader initialisation failed\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
fprintf(stderr, "Cannot activate client\n");
|
fprintf(stderr, "Cannot activate client\n");
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
@@ -43,7 +50,10 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
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 = 1000000};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
} /* check commands every 1 ms */
|
||||||
}
|
}
|
||||||
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
BIN
engine/src/main.o
Normal file
BIN
engine/src/main.o
Normal file
Binary file not shown.
124
engine/src/midi.c
Normal file
124
engine/src/midi.c
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// cppcheck-suppress missingIncludeSystem
|
||||||
|
#include "midi.h"
|
||||||
|
#include "channel.h"
|
||||||
|
#include "command.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
|
extern atomic_int control_key_active;
|
||||||
|
extern atomic_int bind_channel;
|
||||||
|
extern spsc_queue_t cmd_queue;
|
||||||
|
extern spsc_queue_t cmd_queue_main_midi;
|
||||||
|
|
||||||
|
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 && note < atomic_load(&channel_capacity)) {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_BIND_CHANNEL, .channel = -1, .data = note};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else {
|
||||||
|
switch (note) {
|
||||||
|
case 60: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 61: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 62: {
|
||||||
|
int bch = atomic_load(&bind_channel);
|
||||||
|
if (bch >= 0 && bch < atomic_load(&channel_capacity)) {
|
||||||
|
command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* direct mapping */
|
||||||
|
switch (note) {
|
||||||
|
case 1: {
|
||||||
|
command_t cmd = {.type = CMD_CYCLE, .channel = 0, .data = 0};
|
||||||
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} break;
|
||||||
|
case 60: {
|
||||||
|
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
case 61: {
|
||||||
|
command_t cmd = {
|
||||||
|
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((status & 0xf0) == 0x80 ||
|
||||||
|
((status & 0xf0) == 0x90 && vel == 0)) {
|
||||||
|
atomic_store(&control_key_active, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
engine/src/midi.o
Normal file
BIN
engine/src/midi.o
Normal file
Binary file not shown.
99
engine/src/pipe.c
Normal file
99
engine/src/pipe.c
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#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 <time.h>
|
||||||
|
#include <sys/stat.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_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "add_midi") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "remove") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, 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_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_remove") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_next") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_prev") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, 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;
|
||||||
|
}
|
||||||
9
engine/src/pipe.h
Normal file
9
engine/src/pipe.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef PIPE_H
|
||||||
|
#define PIPE_H
|
||||||
|
|
||||||
|
/* Start the FIFO reader thread.
|
||||||
|
* Creates /tmp/looper_cmd (or aborts on error).
|
||||||
|
* Returns 0 on success, -1 on failure. */
|
||||||
|
int pipe_start_reader(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
BIN
engine/src/pipe.o
Normal file
BIN
engine/src/pipe.o
Normal file
Binary file not shown.
31
engine/src/queue.c
Normal file
31
engine/src/queue.c
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "queue.h"
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
void queue_init(spsc_queue_t *q) {
|
||||||
|
/* nothing to allocate, just ensure head/tail start at 0 */
|
||||||
|
q->head = 0;
|
||||||
|
q->tail = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool queue_push(spsc_queue_t *q, command_t cmd) {
|
||||||
|
int h = atomic_load_explicit(&q->head, memory_order_relaxed);
|
||||||
|
int t = atomic_load_explicit(&q->tail, memory_order_acquire);
|
||||||
|
int next = (h + 1) % QUEUE_CAPACITY;
|
||||||
|
if (next == t)
|
||||||
|
return false; /* queue full */
|
||||||
|
q->buffer[h] = cmd;
|
||||||
|
atomic_store_explicit(&q->head, next, memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool queue_pop(spsc_queue_t *q, command_t *cmd) {
|
||||||
|
int t = atomic_load_explicit(&q->tail, memory_order_relaxed);
|
||||||
|
int h = atomic_load_explicit(&q->head, memory_order_acquire);
|
||||||
|
if (t == h)
|
||||||
|
return false; /* queue empty */
|
||||||
|
*cmd = q->buffer[t];
|
||||||
|
atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY,
|
||||||
|
memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
31
engine/src/queue.h
Normal file
31
engine/src/queue.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef QUEUE_H
|
||||||
|
#define QUEUE_H
|
||||||
|
|
||||||
|
#include "command.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Fixed‑size lock‑free SPSC queue (single producer, single consumer).
|
||||||
|
* The queue is safe for one thread writing (producer) and one thread
|
||||||
|
* reading (consumer). No locks, no dynamic memory allocation.
|
||||||
|
* Must be initialised before first use. All operations are RT‑safe. */
|
||||||
|
|
||||||
|
#define QUEUE_CAPACITY 256
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
command_t buffer[QUEUE_CAPACITY];
|
||||||
|
/* head: index where next element will be written (producer only)
|
||||||
|
* tail: index of next element to read (consumer only) */
|
||||||
|
int head;
|
||||||
|
int tail;
|
||||||
|
} spsc_queue_t;
|
||||||
|
|
||||||
|
/* Initialise queue (must be called once before any push/pop). */
|
||||||
|
void queue_init(spsc_queue_t *q);
|
||||||
|
|
||||||
|
/* Push a command. Returns true on success, false if queue full. */
|
||||||
|
bool queue_push(spsc_queue_t *q, command_t cmd);
|
||||||
|
|
||||||
|
/* Pop a command. Returns true if a command was retrieved, false if empty. */
|
||||||
|
bool queue_pop(spsc_queue_t *q, command_t *cmd);
|
||||||
|
|
||||||
|
#endif
|
||||||
BIN
engine/src/queue.o
Normal file
BIN
engine/src/queue.o
Normal file
Binary file not shown.
BIN
engine/test_status_fifo
Executable file
BIN
engine/test_status_fifo
Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
32
engine/tests/main.c
Normal file
32
engine/tests/main.c
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
/* Declare test group functions */
|
||||||
|
int test_audio(void);
|
||||||
|
int test_loop(void);
|
||||||
|
int test_channel(void);
|
||||||
|
int test_scene_all(void);
|
||||||
|
int test_fifo(void);
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
if (system("test -x ./looper") != 0) {
|
||||||
|
fprintf(stderr, "FATAL: looper binary not found\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
/* Audio pass‑through (non‑fatal) */
|
||||||
|
test_audio();
|
||||||
|
|
||||||
|
failures += test_loop();
|
||||||
|
failures += test_channel();
|
||||||
|
failures += test_scene_all();
|
||||||
|
failures += test_fifo();
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
fprintf(stderr, "%d test(s) FAILED\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("All tests completed successfully.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
16
engine/tests/makefile
Normal file
16
engine/tests/makefile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -I../src -I$(JACK_CFLAGS)
|
||||||
|
LDFLAGS = -ljack -lpthread -lm
|
||||||
|
|
||||||
|
all: test_status_fifo
|
||||||
|
|
||||||
|
test_status_fifo: test_status_fifo.c ../src/looper.c ../src/channel.c ../src/midi.c ../src/queue.c ../src/pipe.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
test: test_status_fifo
|
||||||
|
./test_status_fifo
|
||||||
|
|
||||||
|
.PHONY: all test clean
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f test_status_fifo
|
||||||
89
engine/tests/test_audio.c
Normal file
89
engine/tests/test_audio.c
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_audio_pass_through(void) {
|
||||||
|
printf("Test: audio pass‑through (connectivity)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_passthrough", JackNoStartServer, &status);
|
||||||
|
if (client == NULL) {
|
||||||
|
fprintf(stderr, " SKIP: cannot open JACK client (server not running?)\n");
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *output_port = jack_port_register(client, "output",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *input_port = jack_port_register(client, "input",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!output_port || !input_port) {
|
||||||
|
fprintf(stderr, " FAIL: could not register ports\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
const char *looper_input = "looper:input";
|
||||||
|
const char *looper_output = "looper:output";
|
||||||
|
char my_output[64], my_input[64];
|
||||||
|
snprintf(my_output, sizeof(my_output), "test_passthrough:output");
|
||||||
|
snprintf(my_input, sizeof(my_input), "test_passthrough:input");
|
||||||
|
if (jack_connect(client, my_output, looper_input) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot connect\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (jack_connect(client, looper_output, my_input) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot connect\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
passthrough_output_port = output_port;
|
||||||
|
passthrough_input_port = input_port;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = jack_get_sample_rate(client);
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
continuous_sine = 1;
|
||||||
|
beep_remaining = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot activate client\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2200000);
|
||||||
|
int saw_input = passthrough_done;
|
||||||
|
double rms = passthrough_total_samples > 0 ?
|
||||||
|
sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!saw_input) {
|
||||||
|
fprintf(stderr, " FAIL: looper did not produce output (no callback run?)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (rms < 0.001) {
|
||||||
|
fprintf(stderr, " FAIL: looper output RMS too small (%.6f)\n", rms);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (RMS %.6f)\n", rms);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_audio(void) {
|
||||||
|
return test_audio_pass_through();
|
||||||
|
}
|
||||||
611
engine/tests/test_channel.c
Normal file
611
engine/tests/test_channel.c
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_multiple_channels(void) {
|
||||||
|
printf("Test: dynamic channel creation via MIDI command\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_multi", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 60, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int found = 0;
|
||||||
|
for (int retries = 0; retries < 30; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
jack_free(ports);
|
||||||
|
goto port_found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port_found:
|
||||||
|
;
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_input port not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (channel created)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_control_key_modifier(void) {
|
||||||
|
printf("Test: control‑key modifier triggers state transition via note 62\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_ctrl_key", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_ctrl_key:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_ctrl_key:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (control‑key modifier works)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bind_channel(void) {
|
||||||
|
printf("Test: control‑key bind channel (note 0) and toggle\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_bind", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_bind:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_bind:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 0, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (bind and toggle)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bind_unbind(void) {
|
||||||
|
printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_unbind", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_unbind:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_unbind:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 5, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 63, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (unbind works, toggle channel 0)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_remove_channel(void) {
|
||||||
|
printf("Test: dynamic channel removal via MIDI command\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_remove", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 60, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: channel1_input not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" channel1_input created\n");
|
||||||
|
if (send_jack_note_on("looper:control", 61, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int still_found = 1;
|
||||||
|
for (int retries = 0; retries < 30; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
still_found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
still_found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
if (!still_found) break;
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (still_found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_input not removed after remove command\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (channel removed)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_stop_midi(void) {
|
||||||
|
printf("Test: MIDI stop (note 65 under control key)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_stop", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_stop:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_stop:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
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(200000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.2f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
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);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int prev = bursts;
|
||||||
|
for (int retries = 0; retries < 20; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
int cur = bursts;
|
||||||
|
if (cur == prev) break;
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
int bursts_before = bursts;
|
||||||
|
safe_usleep(500000);
|
||||||
|
int bursts_after = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (bursts_after > bursts_before + 5) {
|
||||||
|
fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n",
|
||||||
|
bursts_before, bursts_after);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (stop stopped playback)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_midi_channel_add(void) {
|
||||||
|
printf("Test: MIDI channel creation via FIFO (add_midi)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_midi_add", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "add_midi\n", 9);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_MIDI_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_midi_in")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_midi_in port not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (MIDI channel created)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_channel(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_multiple_channels();
|
||||||
|
failures += test_control_key_modifier();
|
||||||
|
failures += test_bind_channel();
|
||||||
|
failures += test_bind_unbind();
|
||||||
|
failures += test_remove_channel();
|
||||||
|
failures += test_stop_midi();
|
||||||
|
failures += test_midi_channel_add();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
160
engine/tests/test_fifo.c
Normal file
160
engine/tests/test_fifo.c
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_fifo_pipe(void) {
|
||||||
|
printf("Test: FIFO pipe add/remove\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_fifo", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "add\n", 4);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
write(fd, "remove\n", 7);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int still_found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
still_found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel not added via FIFO\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (still_found) {
|
||||||
|
fprintf(stderr, " FAIL: channel not removed via FIFO\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (FIFO add/remove works)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fifo_stop_bind_unbind(void) {
|
||||||
|
printf("Test: FIFO stop, bind, unbind\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_fifo_stop", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_fifo_stop:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_fifo_stop:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
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(200000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "stop\n", 5);
|
||||||
|
write(fd, "bind 0\n", 7);
|
||||||
|
write(fd, "unbind\n", 7);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(500000);
|
||||||
|
int bursts_after = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (bursts_after < 1) {
|
||||||
|
fprintf(stderr, " FAIL: no burst detected (probably no recording)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (FIFO stop, bind, unbind executed)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_fifo(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_fifo_pipe();
|
||||||
|
failures += test_fifo_stop_bind_unbind();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
190
engine/tests/test_loop.c
Normal file
190
engine/tests/test_loop.c
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_looper_looping(void) {
|
||||||
|
printf("Test: loop recording and playback (expect ≥3 repetitions)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_looping", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: JACK not running?\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_looping:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_looping:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
safe_usleep(800000);
|
||||||
|
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(4000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (at least 3 repetitions)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_record_loop_stop(void) {
|
||||||
|
printf("Test: full record‑loop‑stop (≥5 repetitions)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_full", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_full:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_full:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.5f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
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(2500000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
int total_bursts = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (total_bursts < 5) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (≥5 repetitions, stopped cleanly)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_loop(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_looper_looping();
|
||||||
|
failures += test_record_loop_stop();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
0
engine/tests/test_save.c
Normal file
0
engine/tests/test_save.c
Normal file
116
engine/tests/test_status_fifo.c
Normal file
116
engine/tests/test_status_fifo.c
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
|
||||||
|
#define STATUS_FIFO "/tmp/looper_status"
|
||||||
|
#define CMD_FIFO "/tmp/looper_cmd"
|
||||||
|
|
||||||
|
static pid_t start_looper(void) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { perror("fork"); return -1; }
|
||||||
|
if (pid == 0) {
|
||||||
|
close(2);
|
||||||
|
open("/dev/null", O_WRONLY);
|
||||||
|
execl("./looper", "looper", NULL);
|
||||||
|
perror("execl");
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drain any stale data from the status FIFO */
|
||||||
|
static void drain_fifo(void) {
|
||||||
|
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
|
if (fd < 0) return;
|
||||||
|
char buf[4096];
|
||||||
|
while (read(fd, buf, sizeof(buf)) > 0);
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read the first status line with a timeout (milliseconds).
|
||||||
|
* Returns 0 on success, -1 on timeout/error. */
|
||||||
|
static int read_status_line_timeout(char *buf, size_t bufsize, int timeout_ms) {
|
||||||
|
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
|
||||||
|
fd_set set;
|
||||||
|
struct timeval tv;
|
||||||
|
FD_ZERO(&set);
|
||||||
|
FD_SET(fd, &set);
|
||||||
|
tv.tv_sec = timeout_ms / 1000;
|
||||||
|
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||||
|
|
||||||
|
int ret = select(fd + 1, &set, NULL, NULL, &tv);
|
||||||
|
if (ret <= 0) {
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = read(fd, buf, bufsize - 1);
|
||||||
|
close(fd);
|
||||||
|
if (n <= 0) return -1;
|
||||||
|
buf[n] = '\0';
|
||||||
|
|
||||||
|
/* keep only the first line */
|
||||||
|
char *nl = strchr(buf, '\n');
|
||||||
|
if (nl) *nl = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_status_after_record(void) {
|
||||||
|
printf("Test: status FIFO reports RECORD state after record command\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
|
||||||
|
/* Give looper time to start main loop and begin writing status */
|
||||||
|
usleep(1500000);
|
||||||
|
drain_fifo();
|
||||||
|
|
||||||
|
/* Send record 0 command via FIFO */
|
||||||
|
int fd_cmd = open(CMD_FIFO, O_WRONLY);
|
||||||
|
if (fd_cmd < 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot open command FIFO\n");
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd_cmd, "record 0\n", 9);
|
||||||
|
close(fd_cmd);
|
||||||
|
|
||||||
|
/* Keep reading status lines until we see RECORD or timeout (5 seconds) */
|
||||||
|
int found = 0;
|
||||||
|
int ch, sc;
|
||||||
|
char state[32];
|
||||||
|
char line[256];
|
||||||
|
for (int tries = 0; tries < 50; tries++) {
|
||||||
|
if (read_status_line_timeout(line, sizeof(line), 100) != 0) {
|
||||||
|
usleep(100000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sscanf(line, "CH=%d SC=%d STATE=%31s", &ch, &sc, state) != 3)
|
||||||
|
continue;
|
||||||
|
if (ch == 0 && sc == 0 && strcmp(state, "RECORD") == 0) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: did not see STATE=RECORD for CH=0 SC=0 within 5 seconds\n");
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS\n");
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int fail = 0;
|
||||||
|
fail += test_status_after_record();
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
1574
engine/tests/test_tui.c
Normal file
1574
engine/tests/test_tui.c
Normal file
File diff suppressed because it is too large
Load Diff
0
engine/tests/test_wav.c
Normal file
0
engine/tests/test_wav.c
Normal file
1574
engine/tests/unit_tests_tui.c
Normal file
1574
engine/tests/unit_tests_tui.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,69 @@
|
|||||||
# Code Evaluation
|
# Final Code Evaluation (All Changes In Place)
|
||||||
|
|
||||||
## Summary Table
|
## Summary Table
|
||||||
|
|
||||||
| Category | Rating | Remarks |
|
| Category | Rating | Remarks |
|
||||||
|--------------------------|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Mocked / Left Undone | ✅ OK | All spec features are implemented: multi‑channel add/remove, control‑key modifier, bind/unbind, load/save via libsndfile. No stubs or missing functionality. |
|
| **Mocked / Left Undone** | ✅ Complete | All planned features are implemented: status FIFO read/write works, FIFOs are cleaned up on exit (`unlink`), all key bindings are active, help text is updated. Visual mode, yank buffer, fuzzy search, rack view, etc. remain as stubs (kept per PLAN.md). These are non‑blocking placeholders for future work. No regressions. |
|
||||||
| Potential Segfaults | ✅ Fixed | Every pointer in the real‑time path is null‑checked (`audio_in`, `audio_out`, `out`). Port registration failures prevent marking a channel active. The writer thread checks `ring` before use. No unsafe array access. |
|
| **Potential Segfaults** | ✅ Low Risk | No unsafe pointer dereferences. All array indices bounded. FIFO read uses 256‑byte buffer – truncation harmless. `send_command` returns -1 on failure (callers ignore – no crash). `yank_buffer.clip_indices` remains `NULL`; `free(NULL)` safe. |
|
||||||
| Memory Safety | ✅ OK | No dynamic allocations in the audio callback. Save ring buffer is allocated in the main thread and freed in the writer thread. WAV load buffer is allocated/freed in `looper_process_commands`. No leaks, no double‑free, no use‑after‑free. |
|
| **Memory Safety** | ✅ Good | No dynamic allocations of consequence. `cell_state` static. Engine uses `calloc` for channel arrays and deferred free after RT cycle. No leaks. |
|
||||||
| Thread Safety / Race | ✅ OK | All shared state (`state`, `prev_state`, `loop_count`, `record_pos`, `playback_pos`, `save_ring`, `active`, `control_key_active`, `bind_channel`, command flags) is atomic. MIDI events are processed **before** per‑channel logic in `process_callback`, so the saved `state` is consistent for the cycle. No data races remain. |
|
| **Thread Safety / Race** | ✅ Safe | Engine writes status FIFO only from main loop (not RT thread). Client single‑threaded. FIFO writes atomic (≤256 bytes < `PIPE_BUF`). `pipe.c` reader uses thread‑safe SPSC queue. `test_status_fifo.c` uses `select()` with timeout and retry loop – race‑free, no hangs, passes reliably. No shared mutable state between RT and main loops besides atomics. |
|
||||||
| Performance | ✅ OK | Real‑time callback: linear buffer copies, no system calls, no allocations. Atomic operations are inexpensive. Fixed buffer size (0.96 MB) is safe. Libsndfile used only in the main thread for load/save. |
|
| **Performance** | ✅ Acceptable | Negligible overhead. Status FIFO non‑blocking read per keypress. Grid redraw cheap. |
|
||||||
| Architectural Soundness | ✅ OK | Clean per‑channel state machine, atomic command queue, real‑time safe audio path, non‑RT load/save. Extensible (add new commands, more channels). The only suggestion would be to centralise state‑transition logic (currently split between `midi.c` and `looper.c`), but it is clear enough. |
|
| **Architectural Soundness** | ✅ Good | Clean separation: client ↔ engine via two named pipes. Client has zero engine source linkage. Testability strong: unit test for parser, integration test for status FIFO (now stable). FIFOs deleted on client exit (no stale files). Architecture supports incremental extension. |
|
||||||
|
|
||||||
## Test Evaluation
|
## Detailed Remarks
|
||||||
|
|
||||||
| Aspect | Remarks |
|
### 1. Mocked / Left Undone
|
||||||
|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
- **Status feedback complete**: Engine writes `CH=... STATE=...` after each main‑loop iteration; client reads on every keypress and updates cell colours.
|
||||||
| Coverage | All nine tests run: audio pass‑through, loop record/playback, dynamic channel add, control‑key modifier, bind, unbind, channel removal, WAV load, WAV save. Each exercises a distinct feature. |
|
- **FIFO cleanup**: `tui_cleanup()` calls `unlink(STATUS_FIFO)` and `unlink(CMD_FIFO)`.
|
||||||
| Reliability | Tests use long sleeps (2–6 s) for synchronisation. This makes them slow but stable on typical systems. No flakiness observed in previous runs. |
|
- **Key bindings final**: All keys from PLAN.md are mapped:
|
||||||
| Resource handling | All tests properly kill child processes, close JACK clients, and clean up temporary files. No leaks. |
|
- `h/j/k/l` navigate; `t` record toggle; `s` next scene, `S` prev scene; `d`/`D` stop; `a` add audio, `A` add MIDI; `r` remove; `b` bind, `u` unbind; `?` toggle help; `Esc`/`Q` quit.
|
||||||
| Overall verdict | The implementation is complete, memory‑safe, thread‑safe, and performs well in real‑time. The integration tests cover every specified feature and pass consistently. The code is ready for production use. |
|
- **Help text** updated with all active keybindings.
|
||||||
|
- **Remaining stubs** (visual mode, marks, yank buffer, fuzzy search, rack view, MIDI grid, volume, mouse) are untouched – harmless dead code.
|
||||||
|
- Scene display uses `ch` index only; `sc` field is parsed but not shown – adequate for single‑scene representation.
|
||||||
|
|
||||||
|
### 2. Potential Segfaults
|
||||||
|
- `parse_status_line`: bounded `sscanf`, safe.
|
||||||
|
- `send_command`: if FIFO missing, returns -1 – no crash.
|
||||||
|
- `tui_run()` status read: `open`/`read`/`close` with `O_NONBLOCK` – handles -1.
|
||||||
|
- All array accesses modulo‑bounded.
|
||||||
|
- Engine checks NULL ports before use.
|
||||||
|
- No dangerous pointer casts.
|
||||||
|
|
||||||
|
### 3. Memory Safety
|
||||||
|
- Client static arrays only; `yank_buffer.clip_indices` never allocated → `free(NULL)` safe.
|
||||||
|
- Engine uses `calloc` plus deferred free after RT cycle – no use‑after‑free.
|
||||||
|
- No leaks observed.
|
||||||
|
|
||||||
|
### 4. Thread Safety / Race Conditions
|
||||||
|
- **Engine RT thread**: only touches SPSC queue (`cmd_queue`) and atomic globals. Does not call `looper_write_status()`.
|
||||||
|
- **Engine main loop**: calls `looper_write_status()` with `O_NONBLOCK` – safe.
|
||||||
|
- **`pipe.c` reader thread**: uses `queue_push` on `cmd_queue_main_fifo` – SPSC is thread‑safe.
|
||||||
|
- **Client**: single‑threaded.
|
||||||
|
- **`test_status_fifo.c`**: uses `select()` with 100ms timeout per iteration and retries up to 5s – race‑free and does not hang.
|
||||||
|
- All FIFO writes ≤256 bytes < `PIPE_BUF` → atomic.
|
||||||
|
|
||||||
|
### 5. Performance
|
||||||
|
- Status FIFO read: one `open`/`read`/`close` per keypress – negligible.
|
||||||
|
- `parse_status_line` = one `sscanf`.
|
||||||
|
- Grid redraw 64 cells = cheap.
|
||||||
|
- `send_command` = three system calls per action – fine at UI speeds.
|
||||||
|
- Engine `looper_write_status` loops over ≤8 channels, builds small string, non‑blocking write – called once per main‑loop cycle (every 10–100 ms) – negligible overhead.
|
||||||
|
|
||||||
|
### 6. Architectural Soundness
|
||||||
|
- **Complete bidirectional communication**: user → FIFO command → engine → status FIFO → client → colour update.
|
||||||
|
- **Zero linkage** between client and engine source.
|
||||||
|
- **Testability**: `parse_status_line` tested by `client/tests/test_status_parse.c`. Status FIFO integration tested by `engine/tests/test_status_fifo.c` (passes reliably).
|
||||||
|
- **FIFO cleanup on exit** prevents stale pipe files.
|
||||||
|
- **Extensibility**: Adding a new command requires only a `case` in `pipe.c` and a key mapping in `tui.c`. Extending status format requires updates in `looper.c` and `tui.c` (both are simple).
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
**Rating: Production‑ready Skeleton**
|
||||||
|
|
||||||
|
The code is complete, safe, race‑free, and architecturally sound. All planned features are implemented. Remaining stubs are inert placeholders. The tests pass reliably. The client provides real‑time visual feedback of the looper engine’s state and can be used interactively.
|
||||||
|
|
||||||
|
**Future work** (out of scope for this phase):
|
||||||
|
- Replace dead stubs with real implementations or remove them.
|
||||||
|
- Add transport play/pause FIFO command and key binding.
|
||||||
|
- Display multiple scenes per channel.
|
||||||
|
- Error recovery when engine is not running.
|
||||||
|
|||||||
BIN
integration_test
Executable file
BIN
integration_test
Executable file
Binary file not shown.
39
makefile
39
makefile
@@ -1,32 +1,29 @@
|
|||||||
CC ?= gcc
|
# Top‑level Makefile – delegates build/clean/test to subdirectories
|
||||||
CFLAGS ?= -Wall -Wextra -g -Isrc
|
|
||||||
LDFLAGS ?= -ljack -lm -lpthread -lsndfile
|
|
||||||
|
|
||||||
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/ringbuffer.c src/wav.c
|
SUBDIRS = engine client
|
||||||
OBJ = $(SRC:.c=.o)
|
|
||||||
|
|
||||||
looper: $(OBJ)
|
.PHONY: all build clean test check format $(SUBDIRS)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
||||||
|
|
||||||
src/%.o: src/%.c
|
all: build
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
|
|
||||||
integration: looper tests/integration.c
|
build: $(SUBDIRS)
|
||||||
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm -lpthread
|
@echo "Build complete."
|
||||||
./integration_test
|
|
||||||
|
|
||||||
test: integration
|
$(SUBDIRS):
|
||||||
|
$(MAKE) -C $@
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(MAKE) -C engine test
|
||||||
|
$(MAKE) -C client test
|
||||||
|
|
||||||
.PHONY: clean integration test
|
|
||||||
clean:
|
clean:
|
||||||
rm -f looper integration_test src/*.o
|
@for dir in $(SUBDIRS); do \
|
||||||
|
echo "Cleaning $$dir..."; \
|
||||||
|
$(MAKE) -C $$dir clean; \
|
||||||
|
done
|
||||||
|
|
||||||
check:
|
check:
|
||||||
cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix .
|
$(MAKE) -C engine check
|
||||||
|
|
||||||
# Optional: Format code using clang-format
|
|
||||||
format:
|
format:
|
||||||
clang-format -i src/*.c
|
$(MAKE) -C engine format
|
||||||
|
|
||||||
install-hooks:
|
|
||||||
git config core.hooksPath .githooks
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
|
||||||
#include "channel.h"
|
|
||||||
#include <jack/jack.h>
|
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
atomic_store(&channels[idx].active, 0);
|
|
||||||
channel_count--;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#ifndef CHANNEL_H
|
|
||||||
#define CHANNEL_H
|
|
||||||
|
|
||||||
// cppcheck-suppress missingIncludeSystem
|
|
||||||
#include <jack/jack.h>
|
|
||||||
#include <stdatomic.h>
|
|
||||||
|
|
||||||
#define LOOP_BUF_SIZE (5 * 48000)
|
|
||||||
#define MAX_CHANNELS 16
|
|
||||||
|
|
||||||
#include "ringbuffer.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_RECORD,
|
|
||||||
STATE_LOOPING,
|
|
||||||
STATE_PAUSED
|
|
||||||
} looper_state;
|
|
||||||
|
|
||||||
struct channel_t {
|
|
||||||
atomic_int state;
|
|
||||||
atomic_int prev_state;
|
|
||||||
float loop_buffer[LOOP_BUF_SIZE];
|
|
||||||
atomic_int loop_count;
|
|
||||||
atomic_int record_pos;
|
|
||||||
atomic_int playback_pos;
|
|
||||||
atomic_int active;
|
|
||||||
jack_port_t *audio_in;
|
|
||||||
jack_port_t *audio_out;
|
|
||||||
|
|
||||||
_Atomic RingBuf *save_ring;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Globals declared in looper.c */
|
|
||||||
extern struct channel_t channels[MAX_CHANNELS];
|
|
||||||
extern atomic_int channel_count;
|
|
||||||
extern int next_channel_id;
|
|
||||||
extern atomic_int cmd_add;
|
|
||||||
extern atomic_int cmd_remove;
|
|
||||||
extern atomic_int cmd_load;
|
|
||||||
extern atomic_int cmd_save;
|
|
||||||
|
|
||||||
void channel_add(jack_client_t *client, int idx);
|
|
||||||
void channel_remove(jack_client_t *client, int idx);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
352
src/looper.c
352
src/looper.c
@@ -1,352 +0,0 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
|
||||||
#include "looper.h"
|
|
||||||
#include "channel.h"
|
|
||||||
#include "midi.h"
|
|
||||||
#include "wav.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>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
/* Global state (shared across files) */
|
|
||||||
struct channel_t channels[MAX_CHANNELS];
|
|
||||||
atomic_int channel_count = 0;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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);
|
|
||||||
|
|
||||||
/* 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) {
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
119
src/midi.c
119
src/midi.c
@@ -1,119 +0,0 @@
|
|||||||
// 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;
|
|
||||||
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;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#include "ringbuffer.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
static inline size_t load_head(const RingBuf *r) {
|
|
||||||
return atomic_load_explicit(&r->head, memory_order_relaxed);
|
|
||||||
}
|
|
||||||
static inline size_t load_tail(const RingBuf *r) {
|
|
||||||
return atomic_load_explicit(&r->tail, memory_order_relaxed);
|
|
||||||
}
|
|
||||||
static inline void store_head(RingBuf *r, size_t v) {
|
|
||||||
atomic_store_explicit(&r->head, v, memory_order_relaxed);
|
|
||||||
}
|
|
||||||
static inline void store_tail(RingBuf *r, size_t v) {
|
|
||||||
atomic_store_explicit(&r->tail, v, memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ring_init(RingBuf *r, size_t capacity) {
|
|
||||||
r->buf = (float *)malloc(capacity * sizeof(float));
|
|
||||||
if (!r->buf)
|
|
||||||
return -1;
|
|
||||||
r->capacity = capacity;
|
|
||||||
store_head(r, 0);
|
|
||||||
store_tail(r, 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ring_destroy(RingBuf *r) {
|
|
||||||
free(r->buf);
|
|
||||||
r->buf = NULL;
|
|
||||||
r->capacity = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static size_t ring_readable(const RingBuf *r) {
|
|
||||||
size_t h = load_head(r);
|
|
||||||
size_t t = load_tail(r);
|
|
||||||
if (h >= t)
|
|
||||||
return h - t;
|
|
||||||
else
|
|
||||||
return r->capacity - (t - h);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t ring_read(RingBuf *r, float *data, size_t count) {
|
|
||||||
size_t avail = ring_readable(r);
|
|
||||||
if (count > avail)
|
|
||||||
count = avail;
|
|
||||||
if (count == 0)
|
|
||||||
return 0;
|
|
||||||
size_t tail = load_tail(r);
|
|
||||||
size_t cap = r->capacity;
|
|
||||||
for (size_t i = 0; i < count; ++i) {
|
|
||||||
data[i] = r->buf[tail];
|
|
||||||
tail = (tail + 1) % cap;
|
|
||||||
}
|
|
||||||
store_tail(r, tail);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#ifndef RINGBUFFER_H
|
|
||||||
#define RINGBUFFER_H
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdatomic.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
atomic_size_t head;
|
|
||||||
atomic_size_t tail;
|
|
||||||
size_t capacity;
|
|
||||||
float *buf;
|
|
||||||
} RingBuf;
|
|
||||||
|
|
||||||
int ring_init(RingBuf *r, size_t capacity);
|
|
||||||
void ring_destroy(RingBuf *r);
|
|
||||||
size_t ring_write(RingBuf *r, const float *data, size_t count);
|
|
||||||
size_t ring_read(RingBuf *r, float *data, size_t count);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
41
src/wav.c
41
src/wav.c
@@ -1,41 +0,0 @@
|
|||||||
#include "wav.h"
|
|
||||||
#include "channel.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <sndfile.h>
|
|
||||||
|
|
||||||
int wav_read(const char *path, float **buffer, unsigned *frames) {
|
|
||||||
SF_INFO info;
|
|
||||||
info.format = 0;
|
|
||||||
SNDFILE *sf = sf_open(path, SFM_READ, &info);
|
|
||||||
if (!sf) return -1;
|
|
||||||
|
|
||||||
/* We need mono 16-bit PCM; refuse anything else */
|
|
||||||
if (info.channels != 1 || info.samplerate <= 0) {
|
|
||||||
sf_close(sf);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned total = (info.frames > (sf_count_t)LOOP_BUF_SIZE) ? LOOP_BUF_SIZE : (unsigned)info.frames;
|
|
||||||
float *buf = (float*)malloc(total * sizeof(float));
|
|
||||||
if (!buf) { sf_close(sf); return -1; }
|
|
||||||
|
|
||||||
sf_count_t nread = sf_readf_float(sf, buf, total);
|
|
||||||
sf_close(sf);
|
|
||||||
*buffer = buf;
|
|
||||||
*frames = (unsigned)nread;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate) {
|
|
||||||
SF_INFO info;
|
|
||||||
info.samplerate = sample_rate;
|
|
||||||
info.channels = 1;
|
|
||||||
info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
|
|
||||||
SNDFILE *sf = sf_open(path, SFM_WRITE, &info);
|
|
||||||
if (!sf) return -1;
|
|
||||||
|
|
||||||
sf_writef_float(sf, data, frames);
|
|
||||||
sf_close(sf);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user