# Audio System Architecture ## Problem Statement The legacy audio system had a fragile initialization order dependency: `tracker_init()` had to be called after `synth_init()` because the synth would clear all registered spectrograms. This was brittle, non-obvious, and made testing difficult, as tests needed to respect this internal implementation detail. ## Implemented Solution: The AudioEngine To solve this, the audio system was refactored into a unified, lifecycle-managed architecture centered around two new classes: `AudioEngine` and `SpectrogramResourceManager`. This design eliminates initialization-order dependencies and provides clear ownership of resources. ### Architecture Overview The final architecture separates responsibilities into distinct, manageable components: ``` ┌─────────────────┐ │ AudioEngine │ │ (Facade) │ └────────┬────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌──────▼──────┐ ┌─────▼──────┐ ┌─────▼────────┐ │ Synth │ │ Tracker │ │ Resource │ │ │ │ │ │ Manager │ │ (Playback) │ │ (Sequence) │ │ (Loading) │ └─────────────┘ └────────────┘ └──────┬───────┘ │ ┌──────────┼──────────┐ │ │ ┌──────▼──────┐ ┌──────▼──────┐ │ AssetManager│ │ Procedural │ │ (Assets) │ │ Generator │ └─────────────┘ └─────────────┘ ``` ### 1. AudioEngine The `AudioEngine` acts as a high-level facade for the entire audio subsystem. It is the single entry point for initialization, updates, and shutdown. **Responsibilities:** - Manages the lifecycle of the `Synth`, `Tracker`, and `SpectrogramResourceManager`. - Guarantees the correct initialization order internally. - Provides a unified API for updating the audio state (`update()`) and seeking (`seek()`). - Abstracts away the complexity of resource loading and synth registration. **Before (Fragile Initialization):** ```cpp // In main.cc or tests synth_init(); tracker_init(); // ... tracker_update(music_time); ``` **After (Robust Initialization):** ```cpp // In main.cc or tests #include "audio/audio_engine.h" AudioEngine g_audio_engine; g_audio_engine.init(); // ... g_audio_engine.update(music_time); ``` ### 2. SpectrogramResourceManager This class centralizes all resource loading and memory management for spectrograms, for both binary assets and procedurally generated notes. **Responsibilities:** - **Loading:** Handles loading `.spec` files from the `AssetManager` and generating procedural spectrograms from `NoteParams`. - **Ownership:** Enforces clear memory ownership rules. It *borrows* pointers to static assets but *owns* the memory for any generated procedural spectrograms, which it frees upon shutdown. - **Lazy Loading & Caching:** Resources are not loaded when the application starts. Instead, their metadata is registered. The actual data is loaded on-demand the first time a sample is needed. Once loaded, it is cached for future use. This results in faster startup times and more efficient memory usage. --- ## Lazy Loading and Pre-warming Strategy The system uses a lazy-loading approach to minimize startup delay and memory footprint. **Workflow:** 1. **`AudioEngine::init()`**: The resource manager is initialized, but no samples are loaded. 2. **`AudioEngine::load_music_data()`**: The tracker score is parsed, and metadata for all required samples (both asset-based and procedural) is *registered* with the `SpectrogramResourceManager`. No data is loaded at this stage. 3. **`AudioEngine::update()`**: During the main loop, the tracker identifies which samples will be needed in the near future (a 1-2 second lookahead window). It then asks the resource manager to "pre-warm" these samples, loading them into the cache just before they are needed. 4. **Triggering a sample**: When a tracker event fires, the `AudioEngine` requests the sample from the resource manager. Since it was pre-warmed, it's a fast cache hit. The engine then ensures the spectrogram is registered with the low-level synth and triggers the voice. **Benefits:** - **Fast Startup:** The application doesn't pay the cost of loading all audio assets up front. - **No Stutter:** Pre-warming ensures that samples are already in memory when they need to be played, preventing stutter caused by load-on-trigger. - **Memory Efficient:** Only the samples that are actively being used or are coming up soon are held in memory. --- ## Timeline Seeking & Scrubbing (For Debugging) A key feature enabled by this refactor is robust timeline seeking, which is invaluable for development and debugging. The `AudioEngine::seek()` method (`#if !defined(STRIP_ALL)`) allows jumping to any point in the demo timeline. **`seek(target_time)` Process:** 1. **Reset:** The synth and tracker states are completely reset, clearing all active voices and pattern states. 2. **State Reconstruction:** The tracker re-scans the musical score from the beginning up to `target_time` to determine which patterns should be active. 3. **Pre-warming:** The resource manager pre-loads all samples needed for the time range around `target_time`. 4. **Ready:** The audio system is now in the exact state it would have been if the demo had run normally up to `target_time`, ready for playback to resume. This allows developers to instantly jump to a specific scene to debug audio or visual issues without having to watch the demo from the start.