summaryrefslogtreecommitdiff
path: root/ANALYSIS_VARIABLE_TEMPO_V2.md
blob: add347c97e8387e71b135fd28112dc1c3775395b (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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
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!