diff --git a/docs/12-command-architecture.md b/docs/12-command-architecture.md index e69de29..ec399b5 100644 --- a/docs/12-command-architecture.md +++ b/docs/12-command-architecture.md @@ -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.