summaryrefslogtreecommitdiff
path: root/doc/archive/AUDIO_LIFECYCLE_REFACTOR.md
blob: f5e00456bd4f3de5dc069f5e2cb219563184c1c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 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.