diff --git a/breakup.md b/breakup.md index e69de29..f14b39a 100644 --- a/breakup.md +++ b/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 ` → describe a looper output port (e.g., `looper:out_0`) +- `:to ` → destination port (e.g., `plugin1:in_left`) +- `:addplugin ` → 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 `. + - 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. diff --git a/client/PLAN.md b/client/PLAN.md deleted file mode 100644 index b169a8d..0000000 --- a/client/PLAN.md +++ /dev/null @@ -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 \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 \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 \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. -```` diff --git a/client/makefile b/client/makefile index f4c1971..010e25c 100644 --- a/client/makefile +++ b/client/makefile @@ -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 diff --git a/client/src/carla_host.c b/client/src/carla_host.c new file mode 100644 index 0000000..802dcc8 --- /dev/null +++ b/client/src/carla_host.c @@ -0,0 +1,38 @@ +#include +#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; +} diff --git a/client/src/carla_host.h b/client/src/carla_host.h new file mode 100644 index 0000000..2d07ee1 --- /dev/null +++ b/client/src/carla_host.h @@ -0,0 +1,14 @@ +#ifndef CARLA_HOST_H +#define CARLA_HOST_H + +#include + +/* 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 diff --git a/client/tests/test_carla_host.c b/client/tests/test_carla_host.c new file mode 100644 index 0000000..cb215f3 --- /dev/null +++ b/client/tests/test_carla_host.c @@ -0,0 +1,46 @@ +#include +#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; +} diff --git a/client/tests/test_client.c b/client/tests/test_client.c index fae8fcc..bfc600e 100644 --- a/client/tests/test_client.c +++ b/client/tests/test_client.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include "tui.h" #include #include diff --git a/engine/src/plugins.c b/engine/src/plugins.c new file mode 100644 index 0000000..e69de29 diff --git a/engine/src/plugins.h b/engine/src/plugins.h new file mode 100644 index 0000000..e69de29 diff --git a/engine/tests/test_plugin.c b/engine/tests/test_plugin.c new file mode 100644 index 0000000..e69de29