92 lines
4.5 KiB
Markdown
92 lines
4.5 KiB
Markdown
# 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`.
|