From 829e2112db2c6ee05ed3fa787476456cb137222f Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 4 Feb 2026 13:29:55 +0100 Subject: docs: Add simplified variable tempo approach (V2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User's brilliant insight: Don't change spectrograms, just change WHEN they trigger! Key advantages: - No pitch shifting (drums sound like drums) - No synth changes needed (risk-free) - Simple implementation (~20 lines, 1 hour) - Tiny size impact (~50 bytes) - Perfect audio quality How it works: music_time += dt * tempo_scale tracker_update(music_time) That's it! Patterns trigger faster/slower based on tempo_scale. The 'reset' trick: - When tempo hits 2.0x, reset to 1.0x - Switch to pattern with 2x denser events - Result: Same perceived rate, timeline reset Much simpler than original proposal (12 hours → 1 hour) Co-Authored-By: Claude Sonnet 4.5 --- ANALYSIS_VARIABLE_TEMPO_V2.md | 414 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 ANALYSIS_VARIABLE_TEMPO_V2.md diff --git a/ANALYSIS_VARIABLE_TEMPO_V2.md b/ANALYSIS_VARIABLE_TEMPO_V2.md new file mode 100644 index 0000000..add347c --- /dev/null +++ b/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! -- cgit v1.2.3