# 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!