summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-05 02:01:57 +0100
committerskal <pascal.massimino@gmail.com>2026-02-05 02:01:57 +0100
commit93a65b43094641b4c188b4fc260b8ed44c883728 (patch)
tree93480071658129c06fe43d72e9235fc3ed10bbda /doc
parentefcfce7a88bcf369d6c39564320b8c026554572b (diff)
move MD filesHEADmain
Diffstat (limited to 'doc')
-rw-r--r--doc/ANALYSIS_VARIABLE_TEMPO.md372
-rw-r--r--doc/ANALYSIS_VARIABLE_TEMPO_V2.md414
2 files changed, 786 insertions, 0 deletions
diff --git a/doc/ANALYSIS_VARIABLE_TEMPO.md b/doc/ANALYSIS_VARIABLE_TEMPO.md
new file mode 100644
index 0000000..f09996b
--- /dev/null
+++ b/doc/ANALYSIS_VARIABLE_TEMPO.md
@@ -0,0 +1,372 @@
+# Analysis: Variable Tempo / Unified Music Time
+
+## Current Architecture Analysis
+
+### 1. BPM Dependencies (Problems Found)
+
+#### Location 1: `TrackerScore` (tracker.h:32)
+```cpp
+struct TrackerScore {
+ const TrackerPatternTrigger* triggers;
+ uint32_t num_triggers;
+ float bpm; // ❌ FIXED BPM - Cannot change dynamically
+};
+```
+
+#### Location 2: `tracker_update()` (tracker.cc:57)
+```cpp
+float beat_to_sec = 60.0f / g_tracker_score.bpm; // ❌ Used to convert beats→seconds
+```
+
+**Critical Issue (tracker.cc:84):**
+```cpp
+int frame_offset = (int)(event.beat * beat_to_sec * 32000.0f / DCT_SIZE);
+```
+
+**Problem:** The BPM is **baked into the spectrogram** at pattern trigger time!
+- Events within a pattern are composited once with fixed spacing
+- The resulting spectrogram frames encode the tempo
+- Once triggered, the pattern plays at the encoded tempo
+- **Cannot speed up/slow down dynamically**
+
+#### Location 3: `main.cc` (lines 99, 166)
+```cpp
+// Bass pattern timing
+if (t - last_beat_time > (60.0f / g_tracker_score.bpm) / 2.0) { // ❌ BPM dependency
+
+// Visual beat calculation
+float beat = fmodf((float)current_time * g_tracker_score.bpm / 60.0f, 1.0f); // ❌ BPM dependency
+```
+
+---
+
+## Current Time Flow
+
+```
+Physical Time (hardware clock)
+ ↓
+platform_state.time (real seconds elapsed)
+ ↓
+current_time = platform_state.time + seek_offset
+ ↓
+tracker_update(current_time) ← checks if patterns should trigger
+ ↓
+Pattern compositing: event.beat * (60.0 / BPM) * 32kHz
+ ↓
+Spectrogram generated with FIXED frame spacing
+ ↓
+synth_trigger_voice() → registers spectrogram
+ ↓
+Audio callback @ 32kHz pulls samples via synth_render()
+ ↓
+Voice advances through spectral frames at FIXED rate (one frame per DCT_SIZE samples)
+```
+
+**The Fundamental Problem:**
+- Music time = Physical time (1:1 mapping)
+- Patterns are pre-rendered at fixed tempo
+- Synth playback rate is locked to 32kHz sample clock
+
+---
+
+## What Needs to Change
+
+### Architecture Requirements
+
+1. **Separate Music Time from Physical Time**
+ - Music time advances independently: `music_time += dt * tempo_scale`
+ - `tempo_scale = 1.0` → normal speed
+ - `tempo_scale = 2.0` → double speed
+ - `tempo_scale = 0.5` → half speed
+
+2. **Remove BPM from Pattern Compositing**
+ - Events should use abstract "beat" units, not seconds
+ - Pattern generation should NOT convert beats→frames using BPM
+ - Keep events in "music time" space
+
+3. **Variable Playback Rate in Synth**
+ - Synth must support dynamic playback speed
+ - As tempo changes, synth advances through spectral frames faster/slower
+
+---
+
+## Proposed Solution
+
+### Phase 1: Introduce Unified Music Time
+
+#### Change 1: Update `TrackerScore`
+```cpp
+struct TrackerScore {
+ const TrackerPatternTrigger* triggers;
+ uint32_t num_triggers;
+ // REMOVE: float bpm;
+};
+```
+
+#### Change 2: Update `TrackerPatternTrigger`
+```cpp
+struct TrackerPatternTrigger {
+ float music_time; // Renamed from time_sec, now abstract units
+ uint16_t pattern_id;
+};
+```
+
+#### Change 3: Update `TrackerEvent`
+```cpp
+struct TrackerEvent {
+ float beat; // Keep as-is, already abstract
+ uint16_t sample_id;
+ float volume;
+ float pan;
+};
+```
+
+#### Change 4: Update `tracker_update()` signature
+```cpp
+// OLD:
+void tracker_update(float time_sec);
+
+// NEW:
+void tracker_update(float music_time);
+```
+
+**Note:** Pattern compositing still needs tempo for now (see Phase 2).
+
+---
+
+### Phase 2: Variable Tempo Synth Playback
+
+The hard part: **Dynamic playback speed without pitch shifting.**
+
+#### Option A: Naive Approach (Pitch Shifts - NOT GOOD)
+```cpp
+// Simply advance through frames faster/slower
+v.current_spectral_frame += playback_speed; // ❌ Changes pitch!
+```
+
+**Problem:** Advancing through spectral frames faster/slower changes pitch.
+- 2x speed → 2x frequency (octave up)
+- 0.5x speed → 0.5x frequency (octave down)
+
+#### Option B: Resample Spectrograms (Complex)
+- Generate spectrograms at "reference tempo" (e.g., 1.0)
+- At playback time, resample to match current tempo
+- **Pros:** Preserves pitch
+- **Cons:** CPU-intensive, requires interpolation
+
+#### Option C: Time-Stretching (Best Quality)
+- Use phase vocoder or similar algorithm
+- Stretches/compresses time without changing pitch
+- **Pros:** High quality
+- **Cons:** Very complex, may exceed 64k budget
+
+#### Option D: Pre-render at Reference Tempo (Recommended for 64k)
+**Key Insight:** For a 64k demo, pre-render everything at a fixed internal tempo.
+
+```cpp
+// At pattern trigger time:
+// 1. Composite pattern at "reference tempo" (e.g., 120 BPM baseline)
+int frame_offset = (int)(event.beat * REFERENCE_BEAT_DURATION * 32000.0f / DCT_SIZE);
+
+// 2. Store tempo multiplier with voice
+struct Voice {
+ float playback_speed; // NEW: 1.0 = normal, 2.0 = double speed, etc.
+ // ... existing fields
+};
+
+// 3. In synth_render(), advance at variable rate:
+void synth_render(float* output_buffer, int num_frames) {
+ for each voice {
+ // Advance through spectral frames at tempo-adjusted rate
+ float frame_advance = playback_speed / DCT_SIZE;
+
+ // Use fractional frame index
+ float spectral_frame_pos; // NEW: float instead of int
+
+ // Interpolate between frames
+ int frame0 = (int)spectral_frame_pos;
+ int frame1 = frame0 + 1;
+ float frac = spectral_frame_pos - frame0;
+
+ // Blend spectral data from frame0 and frame1
+ for (int j = 0; j < DCT_SIZE; ++j) {
+ windowed_frame[j] = lerp(spectral[frame0][j], spectral[frame1][j], frac);
+ }
+ }
+}
+```
+
+**Trade-off:** This changes pitch! But for a demo, this might be acceptable.
+
+---
+
+## Sync with Physical Audio Device
+
+### Current System
+```
+Audio Callback (32kHz fixed)
+ ↓
+synth_render(buffer, num_frames) ← pulls samples
+ ↓
+Voices advance: ++buffer_pos, ++current_spectral_frame
+ ↓
+Fixed playback rate (one spectral frame = DCT_SIZE samples)
+```
+
+### With Variable Tempo
+```
+Audio Callback (32kHz fixed - CANNOT CHANGE)
+ ↓
+synth_render(buffer, num_frames, current_playback_speed)
+ ↓
+Voices advance: buffer_pos += playback_speed
+ ↓
+Variable playback rate (playback_speed can be > 1.0 or < 1.0)
+```
+
+**Key Point:** Hardware audio rate (32kHz) is FIXED. We cannot change it.
+
+**Solution:** Adjust how fast we consume spectrogram data:
+- `playback_speed = 2.0` → consume frames 2x faster → music plays 2x speed
+- `playback_speed = 0.5` → consume frames 0.5x slower → music plays 0.5x speed
+
+---
+
+## Implementation Roadmap
+
+### Step 1: Remove BPM from TrackerScore ✅ Ready
+- Remove `float bpm` field
+- Update tracker_compiler to not generate BPM
+- Update tests
+
+### Step 2: Add Global Tempo State
+```cpp
+// In audio/tracker.h or main.cc
+float g_current_tempo = 1.0f; // Multiplier: 1.0 = normal, 2.0 = double speed
+
+void tracker_set_tempo(float tempo_multiplier);
+float tracker_get_tempo();
+```
+
+### Step 3: Update tracker_update()
+```cpp
+void tracker_update(float music_time) {
+ // music_time is now abstract, not physical seconds
+ // Patterns trigger when music_time >= trigger.music_time
+}
+```
+
+### Step 4: Update Pattern Compositing (Keep Reference Tempo)
+```cpp
+// Use fixed reference tempo for compositing (e.g., 120 BPM)
+const float REFERENCE_BPM = 120.0f;
+const float reference_beat_to_sec = 60.0f / REFERENCE_BPM;
+
+int frame_offset = (int)(event.beat * reference_beat_to_sec * 32000.0f / DCT_SIZE);
+```
+
+### Step 5: Variable Playback in Synth (Advanced)
+```cpp
+// Store playback speed with each voice
+struct Voice {
+ float playback_speed; // Set when voice is triggered
+ float spectral_frame_pos; // Change to float for interpolation
+ // ...
+};
+
+// In synth_trigger_voice():
+v.playback_speed = g_current_tempo;
+
+// In synth_render():
+// Advance with fractional frame increment
+v.spectral_frame_pos += v.playback_speed / DCT_SIZE;
+
+// Interpolate spectral data (required for smooth tempo changes)
+```
+
+### Step 6: Update Main Loop
+```cpp
+// In main.cc
+static float g_music_time = 0.0f;
+static float g_tempo_scale = 1.0f; // Can be animated!
+
+void update_game_logic(double physical_time) {
+ float dt = get_delta_time();
+
+ // Music time advances at variable rate
+ g_music_time += dt * g_tempo_scale;
+
+ // Animate tempo (example)
+ g_tempo_scale = 1.0f + 0.5f * sinf(physical_time * 0.1f); // Oscillate 0.5x - 1.5x
+
+ tracker_set_tempo(g_tempo_scale);
+ tracker_update(g_music_time);
+}
+```
+
+---
+
+## Open Questions
+
+1. **Pitch Shifting Acceptable?**
+ - If tempo changes, should pitch change too?
+ - For demoscene effect: probably YES (classic effect)
+ - For "realistic" music: NO (need time-stretching)
+
+2. **Tempo Curve Continuity**
+ - Should tempo changes be smoothed (acceleration curves)?
+ - Or instant jumps (may cause audio glitches)?
+
+3. **Spectral Frame Interpolation**
+ - Linear interpolation sufficient?
+ - Or need cubic/sinc for quality?
+
+4. **Tracker Compilation**
+ - Should tracker_compiler still know about tempo?
+ - Or output pure "beat" units?
+
+---
+
+## Recommendation
+
+**For a 64k demo, I recommend:**
+
+### Minimal Change Approach (Easiest)
+1. Keep BPM for compositing (as "reference tempo")
+2. Add `g_current_tempo` multiplier
+3. Make synth advance through frames at `tempo * playback_speed`
+4. Accept pitch shifting as intentional effect
+
+**Changes needed:**
+- Add `float g_current_tempo` global
+- Update `synth_render()` to use `playback_speed`
+- Main loop: `music_time += dt * g_current_tempo`
+- Tracker: rename `time_sec` → `music_time` (conceptual only)
+
+**Size impact:** ~100 bytes
+**Complexity:** Low
+**Enables:** Full variable tempo with pitch shift effect
+
+### Advanced Approach (Better Quality)
+- Implement spectral frame interpolation
+- Add time-stretching (phase vocoder lite)
+- Size impact: +2-3 KB
+
+---
+
+## Conclusion
+
+**Is the code ready?** ❌ NO
+
+**What needs to change?**
+1. BPM must become a "reference tempo" for compositing only
+2. Add global `tempo_scale` variable
+3. Synth must support variable playback speed
+4. Main loop must track `music_time` separately from physical time
+
+**Sync with audio device:**
+- Hardware rate (32kHz) is FIXED
+- We control playback speed by advancing through spectral frames faster/slower
+- This inherently changes pitch (unless we add time-stretching)
+
+**Recommended next step:** Implement minimal change approach first, then iterate.
diff --git a/doc/ANALYSIS_VARIABLE_TEMPO_V2.md b/doc/ANALYSIS_VARIABLE_TEMPO_V2.md
new file mode 100644
index 0000000..add347c
--- /dev/null
+++ b/doc/ANALYSIS_VARIABLE_TEMPO_V2.md
@@ -0,0 +1,414 @@
+# Variable Tempo: Simplified Approach (V2)
+
+## User's Proposal: Trigger Timing Only
+
+**Key Insight:** Don't change spectrograms or playback speed - just change **when** they trigger!
+
+### The Simple Solution
+
+```cpp
+// In main loop
+static float music_time = 0.0f;
+static float tempo_scale = 1.0f; // Can be animated/changed
+
+void update_game_logic(float dt) {
+ // Music time advances at variable rate
+ music_time += dt * tempo_scale;
+
+ // Patterns trigger based on music_time (not physical time)
+ tracker_update(music_time);
+}
+```
+
+**That's it!** 🎉
+
+### What This Achieves
+
+**Drums, melodies, samples:** All sound identical (no pitch shift)
+**Tempo changes:** Patterns trigger faster/slower based on `tempo_scale`
+**No synth changes:** Playback rate stays at 32kHz (unchanged)
+
+### Example Timeline
+
+**Pattern triggers at music_time = 4.0**
+
+| tempo_scale | Physical Time | Effect |
+|-------------|---------------|--------|
+| 1.0 | 4.0s | Normal tempo |
+| 2.0 | 2.0s | Triggers 2x earlier (accelerated) |
+| 0.5 | 8.0s | Triggers 2x later (slowed) |
+
+**Visual:**
+```
+Physical Time: 0s----1s----2s----3s----4s----5s----6s----7s----8s
+ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
+
+tempo_scale=1.0: 🥁 (triggers at 4s)
+
+tempo_scale=2.0: 🥁 (triggers at 2s - accelerated!)
+
+tempo_scale=0.5: 🥁 (triggers at 8s - slowed)
+```
+
+### The "Reset" Trick
+
+**Problem:** Music has accelerated to 2.0x, feels too fast
+**Solution:** Reset tempo and switch to denser pattern
+
+```cpp
+// Music accelerating
+tempo_scale = 1.0 + t * 0.1; // Gradually speeds up
+
+// At some point (e.g., tempo_scale = 2.0)
+if (tempo_scale >= 2.0) {
+ tempo_scale = 1.0; // Reset to normal
+ trigger_pattern_dense(); // Switch to 2x denser pattern
+}
+```
+
+**Pattern Authoring:**
+```
+Pattern A (sparse): [kick]---[snare]---[kick]---[snare]---
+ beat 0 beat 1 beat 2 beat 3
+
+Pattern B (dense): [kick]-[snare]-[kick]-[snare]-
+ beat 0 beat 0.5 beat 1 beat 1.5
+```
+
+**Result:** Music feels the same tempo, but you've "reset" the timeline!
+
+---
+
+## What Needs to Change in Current Code
+
+### Change 1: Main Loop (main.cc)
+
+**Current:**
+```cpp
+void update_game_logic(double t) {
+ tracker_update((float)t); // ❌ Uses physical time directly
+}
+```
+
+**New:**
+```cpp
+static float g_music_time = 0.0f;
+static float g_tempo_scale = 1.0f;
+
+void update_game_logic(double t) {
+ float dt = get_delta_time(); // Physical time delta
+
+ // Music time advances at variable rate
+ g_music_time += dt * g_tempo_scale;
+
+ tracker_update(g_music_time); // ✅ Uses music time
+}
+```
+
+### Change 2: Tempo Control API (NEW)
+
+```cpp
+// In tracker.h or main.cc
+void set_tempo_scale(float scale);
+float get_tempo_scale();
+float get_music_time();
+```
+
+### Change 3: Tracker (NO CHANGES NEEDED!)
+
+**tracker.h:** Keep as-is
+**tracker.cc:** Keep as-is
+**TrackerScore.bpm:** Keep it! (used for compositing patterns)
+
+The tracker already works with abstract "time" - we just feed it `music_time` instead of physical time.
+
+### Change 4: Synth (NO CHANGES NEEDED!)
+
+Spectrograms play back at fixed rate (32kHz).
+No pitch shifting, no interpolation needed.
+
+---
+
+## Detailed Example: Accelerating Drum Beat
+
+### Setup
+```cpp
+// Pattern: kick on beats 0 and 2, snare on beats 1 and 3
+TrackerEvent drum_events[] = {
+ {0.0f, KICK_ID, 1.0f, 0.0f}, // Beat 0
+ {1.0f, SNARE_ID, 0.9f, 0.0f}, // Beat 1
+ {2.0f, KICK_ID, 1.0f, 0.0f}, // Beat 2
+ {3.0f, SNARE_ID, 0.9f, 0.0f}, // Beat 3
+};
+
+TrackerPattern drum_pattern = {drum_events, 4, 4.0f};
+```
+
+### Scenario: Gradual Acceleration
+
+```cpp
+// Main loop
+float tempo_scale = 1.0f;
+
+void update_game_logic(float dt) {
+ // Gradually accelerate (0.05x faster per second)
+ tempo_scale += dt * 0.05f;
+
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+}
+```
+
+**Timeline:**
+```
+Physical Time: 0s 1s 2s 3s 4s 5s 6s 7s 8s
+tempo_scale: 1.0 1.05 1.10 1.15 1.20 1.25 1.30 1.35 1.40
+
+Music Time: 0.0 1.05 2.15 3.30 4.50 5.75 7.05 8.40 9.80
+ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
+Pattern Trigs: 🥁 🥁 🥁 🥁 🥁 🥁 🥁 🥁 🥁
+(every 4 beats) 0 4 8 12 16 20 24 28 32
+
+Physical times when patterns trigger:
+- Pattern 0: t=0.0s
+- Pattern 1: t≈3.8s (instead of 4.0s)
+- Pattern 2: t≈7.2s (instead of 8.0s)
+- Pattern 3: t≈10.2s (instead of 12.0s)
+```
+
+**Effect:** Drum beat gradually feels faster, even though drums sound the same!
+
+---
+
+## The "Reset" Strategy
+
+### Problem
+After accelerating to 2.0x, music feels too rushed. Want to maintain energy but reset tempo.
+
+### Solution: Tempo Reset + Pattern Swap
+
+**Before Reset:**
+- tempo_scale = 2.0
+- Pattern A (sparse): kicks every 1 beat
+
+**After Reset:**
+- tempo_scale = 1.0
+- Pattern B (dense): kicks every 0.5 beats
+
+**Result:** Same physical rate of kicks, but tempo has "reset"!
+
+### Code Example
+
+```cpp
+void update_game_logic(float dt) {
+ music_time += dt * tempo_scale;
+
+ // Gradually accelerate
+ tempo_scale += dt * 0.1f;
+
+ // Check for reset point
+ if (tempo_scale >= 2.0f) {
+ // Reset tempo
+ tempo_scale = 1.0f;
+
+ // Switch to denser pattern
+ current_pattern_density *= 2; // 2x more events per beat
+
+ // Optionally: retrigger current section with new density
+ retrigger_section_with_density(current_pattern_density);
+ }
+
+ tracker_update(music_time);
+}
+```
+
+### Pattern Authoring Strategy
+
+**Create patterns at multiple densities:**
+
+```cpp
+// Sparse pattern (quarter notes)
+TrackerEvent pattern_1x[] = {
+ {0.0f, KICK, 1.0f, 0.0f},
+ {1.0f, SNARE, 0.9f, 0.0f},
+ {2.0f, KICK, 1.0f, 0.0f},
+ {3.0f, SNARE, 0.9f, 0.0f},
+};
+
+// Dense pattern (eighth notes)
+TrackerEvent pattern_2x[] = {
+ {0.0f, KICK, 1.0f, 0.0f},
+ {0.5f, HIHAT, 0.6f, 0.0f},
+ {1.0f, SNARE, 0.9f, 0.0f},
+ {1.5f, HIHAT, 0.6f, 0.0f},
+ {2.0f, KICK, 1.0f, 0.0f},
+ {2.5f, HIHAT, 0.6f, 0.0f},
+ {3.0f, SNARE, 0.9f, 0.0f},
+ {3.5f, HIHAT, 0.6f, 0.0f},
+};
+
+// Very dense pattern (sixteenth notes)
+TrackerEvent pattern_4x[] = {
+ // ... even more events
+};
+```
+
+**Use pattern density to match tempo:**
+- tempo_scale = 1.0x → use pattern_1x
+- tempo_scale = 2.0x → reset to 1.0x, use pattern_2x
+- tempo_scale = 4.0x → reset to 1.0x, use pattern_4x
+
+---
+
+## Comparison: Old Proposal vs. New Proposal
+
+| Aspect | Old Proposal (Complex) | New Proposal (Simple) |
+|--------|------------------------|----------------------|
+| **Synth Changes** | Variable playback speed, interpolation | ❌ None |
+| **Pitch Shifting** | Yes (side effect) | ❌ No |
+| **Spectrograms** | Modified at playback | ✅ Unchanged |
+| **Complexity** | High (12 hours) | Low (1 hour) |
+| **Code Changes** | ~500 lines | ~20 lines |
+| **Size Impact** | +2-3 KB | +50 bytes |
+| **Quality** | Good with interpolation | ✅ Perfect (no artifacts) |
+
+---
+
+## Implementation Checklist
+
+### Step 1: Add Music Time State (5 minutes)
+```cpp
+// In main.cc (global scope)
+static float g_music_time = 0.0f;
+static float g_tempo_scale = 1.0f;
+static float g_last_physical_time = 0.0f;
+```
+
+### Step 2: Update Main Loop (10 minutes)
+```cpp
+void update_game_logic(double physical_time) {
+ // Calculate delta
+ float dt = (float)(physical_time - g_last_physical_time);
+ g_last_physical_time = physical_time;
+
+ // Advance music time at scaled rate
+ g_music_time += dt * g_tempo_scale;
+
+ // Update tracker with music time (not physical time)
+ tracker_update(g_music_time);
+}
+
+// In main loop
+while (!should_close) {
+ double current_time = platform_state.time + seek_time;
+ update_game_logic(current_time); // Pass physical time
+ // ...
+}
+```
+
+### Step 3: Add Tempo Control API (10 minutes)
+```cpp
+// In main.cc or new file
+void set_tempo_scale(float scale) {
+ g_tempo_scale = scale;
+}
+
+float get_tempo_scale() {
+ return g_tempo_scale;
+}
+
+float get_music_time() {
+ return g_music_time;
+}
+```
+
+### Step 4: Test with Simple Acceleration (10 minutes)
+```cpp
+// Temporary test: gradual acceleration
+void update_game_logic(double physical_time) {
+ float dt = get_delta_time();
+
+ // TEST: Accelerate from 1.0x to 2.0x over 10 seconds
+ g_tempo_scale = 1.0f + (physical_time / 10.0f);
+ g_tempo_scale = fminf(g_tempo_scale, 2.0f);
+
+ g_music_time += dt * g_tempo_scale;
+ tracker_update(g_music_time);
+}
+```
+
+### Step 5: Implement Reset Logic (15 minutes)
+```cpp
+// When tempo hits threshold, reset and switch patterns
+if (g_tempo_scale >= 2.0f) {
+ g_tempo_scale = 1.0f;
+ // Trigger denser pattern here
+}
+```
+
+### Step 6: Create Pattern Variants (tracker compiler work)
+- Author patterns at 1x, 2x, 4x densities
+- Map tempo ranges to pattern variants
+
+---
+
+## Advantages of This Approach
+
+✅ **Simple:** ~20 lines of code
+✅ **No pitch shift:** Samples sound identical
+✅ **No synth changes:** Risk-free
+✅ **Flexible:** Easy to animate tempo curves
+✅ **Tiny size impact:** ~50 bytes
+✅ **Perfect quality:** No interpolation artifacts
+
+---
+
+## Example: Tempo Curves
+
+### Linear Acceleration
+```cpp
+tempo_scale = 1.0f + t * 0.1f; // 0.1x faster per second
+```
+
+### Exponential Acceleration
+```cpp
+tempo_scale = powf(2.0f, t / 10.0f); // Doubles every 10 seconds
+```
+
+### Oscillating Tempo
+```cpp
+tempo_scale = 1.0f + 0.3f * sinf(t * 0.5f); // Wave between 0.7x and 1.3x
+```
+
+### Manual Control (BPM curve from score)
+```cpp
+// Define tempo curve in tracker score
+float tempo_curve[] = {1.0f, 1.1f, 1.3f, 1.6f, 2.0f, 1.0f, ...};
+tempo_scale = interpolate_tempo_curve(music_time);
+```
+
+---
+
+## Conclusion
+
+**Your proposal is brilliant!** 🎯
+
+### What You Get
+- Variable tempo without modifying spectrograms
+- No pitch shifting (drums sound like drums)
+- Simple implementation (~1 hour)
+- Tiny code size (~50 bytes)
+- Perfect audio quality
+
+### What You Need to Do
+1. Add `g_music_time` and `g_tempo_scale` to main loop
+2. Compute `music_time += dt * tempo_scale`
+3. Pass `music_time` to `tracker_update()`
+4. Animate `tempo_scale` however you want!
+
+### For the "Reset" Trick
+- Author patterns at 1x, 2x, 4x note densities
+- When tempo hits 2.0x, reset to 1.0x and switch to denser pattern
+- Result: Same perceived tempo, but timeline has reset
+
+**Ready to implement?** This is a 1-hour task!