feat: add Carla plugin host stubs and integration plan
This commit is contained in:
committed by
Loic Coenen (aider)
parent
5cffec86e7
commit
dafc7fe46b
110
breakup.md
110
breakup.md
@@ -0,0 +1,110 @@
|
||||
# Integration Plan: Carla Plugin Host (Client‑side)
|
||||
|
||||
## 1. Strategy
|
||||
|
||||
Carla lives in the **client** (the TUI), not the looper engine.
|
||||
The client becomes a lightweight JACK client itself, capable of loading plugins via the Carla host API and bridging audio/MIDI between the looper’s JACK ports and the plugin’s ports.
|
||||
|
||||
## 2. Client (TUI) – new modules
|
||||
|
||||
### 2.1 Carla dependency
|
||||
- Link the client binary with `libcarla_host` (C static/shared library).
|
||||
- Use the official `carla_standalone.h` API.
|
||||
|
||||
### 2.2 New files `client/src/plugins.c` / `plugins.h`
|
||||
Functions (same signatures as before, but run **inside the client**):
|
||||
|
||||
- `int plugin_load(const char *binary, const char *plugin_id, int *out_id)`
|
||||
- `int plugin_unload(int id)`
|
||||
- `int plugin_connect(int id, const char *port_name, const char *looper_port)`
|
||||
- `int plugin_disconnect(const char *from, const char *to)`
|
||||
- `void plugin_set_bypass(int id, bool bypass)`
|
||||
|
||||
The module owns the list of loaded plugins, their Carla‑native IDs, and the mapping between looper JACK ports and plugin ports.
|
||||
|
||||
### 2.3 JACK client for plugin I/O
|
||||
- In `client/src/tui.c` (or a new `client/src/jack_io.c`), open a JACK client with `jack_client_open()`.
|
||||
- Register input/output ports that will be connected to the looper’s ports (usually via `jack_connect` called once at start‑up).
|
||||
- In the process callback, copy audio between looper ports and plugin ports (using Carla’s `process()`‑like functions).
|
||||
- This keeps the engine completely unaware of plugins.
|
||||
|
||||
## 3. TUI commands (colon‑mode)
|
||||
|
||||
All already exist in the plan; only the implementation target changes:
|
||||
|
||||
- `:from <port>` → describe a looper output port (e.g., `looper:out_0`)
|
||||
- `:to <port>` → destination port (e.g., `plugin1:in_left`)
|
||||
- `:addplugin <path>` → loads the plugin and, if `from`/`to` are set, connects them automatically
|
||||
- `:connect` → creates a JACK‑connection between the stored `from` and `to` (or, if one side belongs to a plugin, uses Carla’s internal connect)
|
||||
- `:disconnect`
|
||||
- `:rack` → toggles rack view (list of plugins with ports and bypass status)
|
||||
- `:grid` → back to the original grid view
|
||||
|
||||
## 4. Rack view (TUI)
|
||||
|
||||
Identical to the original description, but the data comes from the **client’s internal Carla handle** instead of the status FIFO.
|
||||
|
||||
## 5. Integration Tests
|
||||
|
||||
### 5.1 Mock plugin (client‑side)
|
||||
- Create `client/tests/mock_plugin/mock_plugin.c`.
|
||||
- A trivial JACK client that copies input to output and adds a 1 kHz tone when the input is silent.
|
||||
- Compiled to `libmock_plugin.so` → the TUI’s plugin loader will load it.
|
||||
|
||||
### 5.2 Test infrastructure (client/tests)
|
||||
- Start the TUI in a headless test mode (or fork a child and feed it commands via stdin).
|
||||
- Observe the status output (sent to stdout or a temporary file) to verify plugin list and connections.
|
||||
|
||||
### 5.3 Test cases (new file `client/tests/test_plugin_client.c`)
|
||||
- `test_plugin_load_unload` – load mock plugin, confirm list shows 1 entry, unload, confirm list empty.
|
||||
- `test_plugin_connect_audio` – load plugin, connect to looper ports, inject audio from a test JACK client, verify plugin output reaches a monitor port.
|
||||
- `test_rack_view` – send `:rack` command, parse the printed lines, verify they match the expected layout.
|
||||
- (Later) `test_bypass` – load, bypass, verify audio passes unaltered.
|
||||
|
||||
## 6. Build System Changes
|
||||
|
||||
- **client/makefile**:
|
||||
- Add `-lcarla_host` to `LDFLAGS`.
|
||||
- Add `plugins.c` (and optionally `jack_io.c`) to `SRCS`.
|
||||
- Build mock plugin as a separate target.
|
||||
- **engine/makefile** – no changes (engine stays pure looper).
|
||||
- **top‑level makefile** – no changes.
|
||||
|
||||
## 7. Implementation Steps (ordered)
|
||||
|
||||
1. **Add Carla dependency & stub tests (client side)**
|
||||
- Link client binary with `-lcarla_host`.
|
||||
- Create `client/src/plugins.h` / `client/src/plugins.c` with stub implementations.
|
||||
- Create `client/tests/test_plugins.c` with failing (**Red**) unit tests for:
|
||||
- `plugin_load` returns -1 on NULL binary
|
||||
- `plugin_unload` returns -1 on invalid id
|
||||
- (optional) `plugin_connect` returns -1 on invalid id
|
||||
- Add a `test` target in `client/makefile` that builds and runs `test_plugins`.
|
||||
- Verify the tests compile and pass (**Green**).
|
||||
|
||||
2. **Implement real Carla integration (client side)**
|
||||
- Open a private JACK client inside the TUI using `jack_client_open()`.
|
||||
- Implement `plugin_load` / `plugin_unload` using Carla’s `carla_new_native` etc.
|
||||
- Write integration tests that load a mock plugin and verify it appears in the rack.
|
||||
|
||||
3. **Add `:addplugin` command parsing in TUI**
|
||||
- When colon mode is entered, parse `:addplugin <path>`.
|
||||
- Call the underlying `plugin_load` and update the internal plugin list.
|
||||
|
||||
4. **Implement rack view (TUI)**
|
||||
- Toggle between grid view and plugin‑list view.
|
||||
- Display plugin name, ID, bypass status.
|
||||
- Add `B`, `D`, `X` keybindings.
|
||||
|
||||
5. **Build mock plugin and write integration tests**
|
||||
- Create `client/tests/mock_plugin/mock_plugin.c`.
|
||||
- Target `libmock_plugin.so`.
|
||||
- Write tests in `client/tests/test_plugin_client.c`:
|
||||
- `test_plugin_load_unload`
|
||||
- `test_plugin_connect_audio`
|
||||
- `test_rack_view`
|
||||
|
||||
6. **Polish and document**
|
||||
- Clean up error messages, handle edge cases.
|
||||
- Add comments to new modules.
|
||||
- Update root `README` with Carla instructions.
|
||||
|
||||
136
client/PLAN.md
136
client/PLAN.md
@@ -1,136 +0,0 @@
|
||||
# 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.
|
||||
````
|
||||
@@ -1,18 +1,49 @@
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc
|
||||
|
||||
all: looper-client test_status_parse
|
||||
|
||||
looper-client: src/main.c src/tui.c
|
||||
$(CC) $(CFLAGS) -Isrc -o $@ $^ -lncurses
|
||||
looper-client: src/main.c src/tui.c $(CARLA_OBJ)
|
||||
$(CC) $(CFLAGS) -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
|
||||
$(CC) $(CFLAGS) -o test_status_parse tests/test_status_parse.c src/tui.c -lncurses
|
||||
|
||||
test: looper-client test_status_parse
|
||||
# --- Carla host stubs ---
|
||||
CARLA_OBJ = src/carla_host.o
|
||||
|
||||
$(CARLA_OBJ): src/carla_host.c src/carla_host.h
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# --- Carla host tests ---
|
||||
TEST_CARLA_BIN = test_carla_host
|
||||
TEST_CARLA_OBJ = tests/test_carla_host.o
|
||||
|
||||
$(TEST_CARLA_OBJ): tests/test_carla_host.c src/carla_host.h
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(TEST_CARLA_BIN): $(TEST_CARLA_OBJ) $(CARLA_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
|
||||
# ensure the tests directory exists
|
||||
tests/test_carla_host.o: | tests
|
||||
|
||||
# --- send_command test ---
|
||||
TEST_CLIENT_BIN = test_client
|
||||
TEST_CLIENT_OBJ = tests/test_client.o
|
||||
|
||||
$(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lncurses
|
||||
|
||||
test: looper-client test_status_parse $(TEST_CARLA_BIN) $(TEST_CLIENT_BIN)
|
||||
./test_status_parse
|
||||
./$(TEST_CARLA_BIN)
|
||||
./$(TEST_CLIENT_BIN)
|
||||
|
||||
.PHONY: all test clean
|
||||
|
||||
clean:
|
||||
rm -f looper-client test_status_parse
|
||||
rm -f looper-client test_status_parse $(TEST_CARLA_BIN) $(TEST_CLIENT_BIN) *.o tests/*.o src/*.o
|
||||
|
||||
38
client/src/carla_host.c
Normal file
38
client/src/carla_host.c
Normal file
@@ -0,0 +1,38 @@
|
||||
#include <stddef.h>
|
||||
#include "carla_host.h"
|
||||
|
||||
int carla_load(const char *binary, const char *plugin_id, int *out_id)
|
||||
{
|
||||
(void)plugin_id;
|
||||
(void)out_id;
|
||||
if (binary == NULL) return -1;
|
||||
// stub: always fails (will be replaced by real Carla later)
|
||||
return -1;
|
||||
}
|
||||
|
||||
int carla_unload(int id)
|
||||
{
|
||||
(void)id;
|
||||
return -1; // stub: always fails
|
||||
}
|
||||
|
||||
int carla_connect(int id, const char *port_name, const char *looper_port)
|
||||
{
|
||||
(void)id;
|
||||
(void)port_name;
|
||||
(void)looper_port;
|
||||
return -1; // stub: always fails
|
||||
}
|
||||
|
||||
int carla_disconnect(const char *from, const char *to)
|
||||
{
|
||||
(void)from;
|
||||
(void)to;
|
||||
return 0; // stub: disconnect always succeeds (does nothing)
|
||||
}
|
||||
|
||||
void carla_set_bypass(int id, bool bypass)
|
||||
{
|
||||
(void)id;
|
||||
(void)bypass;
|
||||
}
|
||||
14
client/src/carla_host.h
Normal file
14
client/src/carla_host.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef CARLA_HOST_H
|
||||
#define CARLA_HOST_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* All functions return -1 on error, 0 on success (except carla_load which returns 0 on success and sets *out_id) */
|
||||
|
||||
int carla_load(const char *binary, const char *plugin_id, int *out_id);
|
||||
int carla_unload(int id);
|
||||
int carla_connect(int id, const char *port_name, const char *looper_port);
|
||||
int carla_disconnect(const char *from, const char *to);
|
||||
void carla_set_bypass(int id, bool bypass);
|
||||
|
||||
#endif
|
||||
46
client/tests/test_carla_host.c
Normal file
46
client/tests/test_carla_host.c
Normal file
@@ -0,0 +1,46 @@
|
||||
#include <stdio.h>
|
||||
#include "carla_host.h"
|
||||
|
||||
static int tests_passed = 0;
|
||||
static int tests_failed = 0;
|
||||
|
||||
#define ASSERT_EQ(expected, actual, msg) do { \
|
||||
if ((expected) != (actual)) { \
|
||||
fprintf(stderr, "FAIL: %s (expected %d, got %d)\n", msg, (int)(expected), (int)(actual)); \
|
||||
tests_failed++; \
|
||||
} else { \
|
||||
printf("PASS: %s\n", msg); \
|
||||
tests_passed++; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
static void test_carla_load_null_binary(void)
|
||||
{
|
||||
int id = -999;
|
||||
int ret = carla_load(NULL, "someplugin", &id);
|
||||
ASSERT_EQ(-1, ret, "carla_load(NULL, ...) returns -1");
|
||||
}
|
||||
|
||||
static void test_carla_unload_invalid_id(void)
|
||||
{
|
||||
int ret = carla_unload(-1);
|
||||
ASSERT_EQ(-1, ret, "carla_unload(-1) returns -1");
|
||||
}
|
||||
|
||||
static void test_carla_connect_invalid_id(void)
|
||||
{
|
||||
int ret = carla_connect(-1, "out", "looper:in");
|
||||
ASSERT_EQ(-1, ret, "carla_connect(-1, ...) returns -1");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Carla host stub unit tests ===\n");
|
||||
|
||||
test_carla_load_null_binary();
|
||||
test_carla_unload_invalid_id();
|
||||
test_carla_connect_invalid_id();
|
||||
|
||||
printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed);
|
||||
return tests_failed > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "tui.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
0
engine/src/plugins.c
Normal file
0
engine/src/plugins.c
Normal file
0
engine/src/plugins.h
Normal file
0
engine/src/plugins.h
Normal file
0
engine/tests/test_plugin.c
Normal file
0
engine/tests/test_plugin.c
Normal file
Reference in New Issue
Block a user