docs: add command architecture documentation
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user