summaryrefslogtreecommitdiff
path: root/doc/ANALYSIS_VARIABLE_TEMPO_V2.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/ANALYSIS_VARIABLE_TEMPO_V2.md')
-rw-r--r--doc/ANALYSIS_VARIABLE_TEMPO_V2.md414
1 files changed, 414 insertions, 0 deletions
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!