From d4a811e5528bc22b1bf5ea8e56751b962d602c82 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 19:42:34 +0000 Subject: [PATCH] docs: add scene switching engine documentation and update evaluation Co-authored-by: aider (deepseek/deepseek-reasoner) --- docs/4-implement-scene-switching-engine.md | 91 ++++++++++++++++++++++ evaluation.md | 1 + 2 files changed, 92 insertions(+) diff --git a/docs/4-implement-scene-switching-engine.md b/docs/4-implement-scene-switching-engine.md index e69de29..3e38b72 100644 --- a/docs/4-implement-scene-switching-engine.md +++ b/docs/4-implement-scene-switching-engine.md @@ -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`. diff --git a/evaluation.md b/evaluation.md index 73c1fc2..297aa90 100644 --- a/evaluation.md +++ b/evaluation.md @@ -20,6 +20,7 @@ - `CMD_BIND_CHANNEL`, `CMD_UNBIND`, `CMD_CYCLE`, `CMD_ADD_CHANNEL`, `CMD_REMOVE_CHANNEL` are all wired. - The integration test suite now includes `test_fifo_stop_bind_unbind()` and `test_midi_channel_add()`. - The FIFO pipe reader handles `"stop"`, `"bind "`, `"unbind"`, and `"add_midi"`. + - **Note:** The separate test files in `tests/` (`test_audio.c`, `test_channel.c`, `test_fifo.c`, `test_loop.c`, `main.c`) are not compiled by the makefile and require a missing `test_common.h`. They are not part of the build – they do not affect functionality and may be removed in a future cleanup. ### 2. Potential Segfaults - **Audio channels:** `audio_in`/`audio_out` are checked for NULL before use.