summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.cc14
-rw-r--r--src/tests/test_variable_tempo.cc375
2 files changed, 388 insertions, 1 deletions
diff --git a/src/main.cc b/src/main.cc
index aba8d4b..9f61f07 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -92,10 +92,20 @@ int main(int argc, char** argv) {
// int melody_id = generate_melody();
// synth_trigger_voice(melody_id, 0.6f, 0.0f);
+ // Music time state for variable tempo
+ static float g_music_time = 0.0f;
+ static float g_tempo_scale = 1.0f; // 1.0 = normal speed
+ static double g_last_physical_time = 0.0;
+
double last_beat_time = 0.0;
int beat_count = 0;
auto update_game_logic = [&](double t) {
+ // Calculate delta time and advance music time at scaled rate
+ const float dt = (float)(t - g_last_physical_time);
+ g_last_physical_time = t;
+ g_music_time += dt * g_tempo_scale;
+
if (t - last_beat_time > (60.0f / g_tracker_score.bpm) / 2.0) { // 8th notes
last_beat_time = t; // Sync to t
@@ -115,7 +125,9 @@ int main(int argc, char** argv) {
*/
++beat_count;
}
- tracker_update((float)t);
+
+ // Pass music_time (not physical time) to tracker
+ tracker_update(g_music_time);
};
#if !defined(STRIP_ALL)
diff --git a/src/tests/test_variable_tempo.cc b/src/tests/test_variable_tempo.cc
new file mode 100644
index 0000000..d366ade
--- /dev/null
+++ b/src/tests/test_variable_tempo.cc
@@ -0,0 +1,375 @@
+// This file is part of the 64k demo project.
+// It tests variable tempo system with music_time scaling.
+// Verifies 2x speed-up and 2x slow-down reset tricks.
+
+#include "audio/mock_audio_backend.h"
+#include "audio/audio.h"
+#include "audio/synth.h"
+#include "audio/tracker.h"
+#include <assert.h>
+#include <stdio.h>
+#include <cmath>
+
+#if !defined(STRIP_ALL)
+
+// Helper: Calculate expected physical time for music_time at constant tempo
+static float calc_physical_time(float music_time, float tempo_scale) {
+ return music_time / tempo_scale;
+}
+
+// Helper: Simulate music time advancement
+static float advance_music_time(float current_music_time, float dt,
+ float tempo_scale) {
+ return current_music_time + (dt * tempo_scale);
+}
+
+void test_basic_tempo_scaling() {
+ printf("Test: Basic tempo scaling (1.0x, 2.0x, 0.5x)...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ // Test 1: Normal tempo (1.0x)
+ {
+ backend.clear_events();
+ float music_time = 0.0f;
+ float tempo_scale = 1.0f;
+
+ // Simulate 1 second of physical time
+ for (int i = 0; i < 10; ++i) {
+ float dt = 0.1f; // 100ms physical steps
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ // After 1 second physical time at 1.0x tempo:
+ // music_time should be ~1.0
+ printf(" 1.0x tempo: music_time = %.3f (expected ~1.0)\n", music_time);
+ assert(std::abs(music_time - 1.0f) < 0.01f);
+ }
+
+ // Test 2: Fast tempo (2.0x)
+ {
+ backend.clear_events();
+ tracker_init(); // Reset tracker
+ float music_time = 0.0f;
+ float tempo_scale = 2.0f;
+
+ // Simulate 1 second of physical time
+ for (int i = 0; i < 10; ++i) {
+ float dt = 0.1f;
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ // After 1 second physical time at 2.0x tempo:
+ // music_time should be ~2.0
+ printf(" 2.0x tempo: music_time = %.3f (expected ~2.0)\n", music_time);
+ assert(std::abs(music_time - 2.0f) < 0.01f);
+ }
+
+ // Test 3: Slow tempo (0.5x)
+ {
+ backend.clear_events();
+ tracker_init();
+ float music_time = 0.0f;
+ float tempo_scale = 0.5f;
+
+ // Simulate 1 second of physical time
+ for (int i = 0; i < 10; ++i) {
+ float dt = 0.1f;
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ // After 1 second physical time at 0.5x tempo:
+ // music_time should be ~0.5
+ printf(" 0.5x tempo: music_time = %.3f (expected ~0.5)\n", music_time);
+ assert(std::abs(music_time - 0.5f) < 0.01f);
+ }
+
+ printf(" ✓ Basic tempo scaling works correctly\n");
+}
+
+void test_2x_speedup_reset_trick() {
+ printf("Test: 2x SPEED-UP reset trick...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ // Scenario: Accelerate to 2.0x, then reset to 1.0x
+ float music_time = 0.0f;
+ float tempo_scale = 1.0f;
+ float physical_time = 0.0f;
+
+ const float dt = 0.1f; // 100ms steps
+
+ // Phase 1: Accelerate from 1.0x to 2.0x over 5 seconds
+ printf(" Phase 1: Accelerating 1.0x → 2.0x\n");
+ for (int i = 0; i < 50; ++i) {
+ physical_time += dt;
+ tempo_scale = 1.0f + (physical_time / 5.0f); // Linear acceleration
+ tempo_scale = fminf(tempo_scale, 2.0f);
+
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
+ music_time);
+ assert(tempo_scale >= 1.99f); // Should be at 2.0x
+
+ // Record state before reset
+ const float music_time_before_reset = music_time;
+ const size_t events_before_reset = backend.get_events().size();
+
+ // Phase 2: RESET - back to 1.0x tempo
+ printf(" Phase 2: RESET to 1.0x tempo\n");
+ tempo_scale = 1.0f;
+
+ // Continue for another 2 seconds
+ for (int i = 0; i < 20; ++i) {
+ physical_time += dt;
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ printf(" After reset + 2s: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
+ music_time);
+
+ // Verify: music_time advanced 2.0 units in 2 seconds at 1.0x tempo
+ const float music_time_delta = music_time - music_time_before_reset;
+ printf(" Music time delta: %.3f (expected ~2.0)\n", music_time_delta);
+ assert(std::abs(music_time_delta - 2.0f) < 0.1f);
+
+ printf(" ✓ 2x speed-up reset trick verified\n");
+}
+
+void test_2x_slowdown_reset_trick() {
+ printf("Test: 2x SLOW-DOWN reset trick...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ // Scenario: Decelerate to 0.5x, then reset to 1.0x
+ float music_time = 0.0f;
+ float tempo_scale = 1.0f;
+ float physical_time = 0.0f;
+
+ const float dt = 0.1f;
+
+ // Phase 1: Decelerate from 1.0x to 0.5x over 5 seconds
+ printf(" Phase 1: Decelerating 1.0x → 0.5x\n");
+ for (int i = 0; i < 50; ++i) {
+ physical_time += dt;
+ tempo_scale = 1.0f - (physical_time / 10.0f); // Linear deceleration
+ tempo_scale = fmaxf(tempo_scale, 0.5f);
+
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
+ music_time);
+ assert(tempo_scale <= 0.51f); // Should be at 0.5x
+
+ // Record state before reset
+ const float music_time_before_reset = music_time;
+
+ // Phase 2: RESET - back to 1.0x tempo
+ printf(" Phase 2: RESET to 1.0x tempo\n");
+ tempo_scale = 1.0f;
+
+ // Continue for another 2 seconds
+ for (int i = 0; i < 20; ++i) {
+ physical_time += dt;
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+ }
+
+ printf(" After reset + 2s: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
+ music_time);
+
+ // Verify: music_time advanced 2.0 units in 2 seconds at 1.0x tempo
+ const float music_time_delta = music_time - music_time_before_reset;
+ printf(" Music time delta: %.3f (expected ~2.0)\n", music_time_delta);
+ assert(std::abs(music_time_delta - 2.0f) < 0.1f);
+
+ printf(" ✓ 2x slow-down reset trick verified\n");
+}
+
+void test_pattern_density_swap() {
+ printf("Test: Pattern density swap at reset points...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ // Simulate: sparse pattern → accelerate → reset + dense pattern
+ float music_time = 0.0f;
+ float tempo_scale = 1.0f;
+
+ // Phase 1: Sparse pattern at normal tempo (first 3 patterns trigger)
+ printf(" Phase 1: Sparse pattern, normal tempo\n");
+ for (float t = 0.0f; t < 3.0f; t += 0.1f) {
+ music_time += 0.1f * tempo_scale;
+ tracker_update(music_time);
+ }
+ const size_t sparse_events = backend.get_events().size();
+ printf(" Events during sparse phase: %zu\n", sparse_events);
+
+ // Phase 2: Accelerate to 2.0x
+ printf(" Phase 2: Accelerating to 2.0x\n");
+ tempo_scale = 2.0f;
+ for (float t = 0.0f; t < 2.0f; t += 0.1f) {
+ music_time += 0.1f * tempo_scale;
+ tracker_update(music_time);
+ }
+ const size_t events_at_2x = backend.get_events().size() - sparse_events;
+ printf(" Additional events during 2.0x: %zu\n", events_at_2x);
+
+ // Phase 3: Reset to 1.0x (in real impl, would switch to denser pattern)
+ printf(" Phase 3: Reset to 1.0x (simulating denser pattern)\n");
+ tempo_scale = 1.0f;
+
+ // At this point, real implementation would trigger a pattern with
+ // 2x more events per beat to maintain perceived density
+
+ const size_t events_before_reset_phase = backend.get_events().size();
+ for (float t = 0.0f; t < 2.0f; t += 0.1f) {
+ music_time += 0.1f * tempo_scale;
+ tracker_update(music_time);
+ }
+ const size_t events_after_reset = backend.get_events().size();
+
+ printf(" Events during reset phase: %zu\n",
+ events_after_reset - events_before_reset_phase);
+
+ // Verify patterns triggered throughout
+ assert(backend.get_events().size() > 0);
+
+ printf(" ✓ Pattern density swap points verified\n");
+}
+
+void test_continuous_acceleration() {
+ printf("Test: Continuous acceleration from 0.5x to 2.0x...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ float music_time = 0.0f;
+ float tempo_scale = 0.5f;
+ float physical_time = 0.0f;
+
+ const float dt = 0.05f; // 50ms steps for smoother curve
+
+ // Accelerate from 0.5x to 2.0x over 10 seconds
+ printf(" Accelerating 0.5x → 2.0x over 10 seconds\n");
+
+ float min_tempo = 0.5f;
+ float max_tempo = 2.0f;
+
+ for (int i = 0; i < 200; ++i) {
+ physical_time += dt;
+ float progress = physical_time / 10.0f; // 0.0 to 1.0
+ tempo_scale = min_tempo + progress * (max_tempo - min_tempo);
+ tempo_scale = fmaxf(min_tempo, fminf(max_tempo, tempo_scale));
+
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+
+ // Log at key points
+ if (i % 50 == 0) {
+ printf(" t=%.1fs: tempo=%.2fx, music_time=%.3f\n", physical_time,
+ tempo_scale, music_time);
+ }
+ }
+
+ printf(" Final: tempo=%.2fx, music_time=%.3f\n", tempo_scale, music_time);
+
+ // Verify tempo reached target
+ assert(tempo_scale >= 1.99f);
+
+ // Verify music_time progressed correctly
+ // Integral of (0.5 + 1.5t/10) from 0 to 10 = 0.5*10 + 1.5*10²/(2*10) = 5 + 7.5 = 12.5
+ const float expected_music_time = 12.5f;
+ printf(" Expected music_time: %.3f, actual: %.3f\n", expected_music_time,
+ music_time);
+ assert(std::abs(music_time - expected_music_time) < 0.5f);
+
+ printf(" ✓ Continuous acceleration verified\n");
+}
+
+void test_oscillating_tempo() {
+ printf("Test: Oscillating tempo (sine wave)...\n");
+
+ MockAudioBackend backend;
+ audio_set_backend(&backend);
+
+ tracker_init();
+ synth_init();
+
+ float music_time = 0.0f;
+ float physical_time = 0.0f;
+
+ const float dt = 0.05f;
+
+ // Oscillate tempo between 0.8x and 1.2x
+ printf(" Oscillating tempo: 0.8x ↔ 1.2x\n");
+
+ for (int i = 0; i < 100; ++i) {
+ physical_time += dt;
+ float tempo_scale = 1.0f + 0.2f * sinf(physical_time * 2.0f);
+
+ music_time += dt * tempo_scale;
+ tracker_update(music_time);
+
+ if (i % 25 == 0) {
+ printf(" t=%.2fs: tempo=%.3fx, music_time=%.3f\n", physical_time,
+ tempo_scale, music_time);
+ }
+ }
+
+ // After oscillation, music_time should be approximately equal to physical_time
+ // (since average tempo is 1.0x)
+ printf(" Final: physical_time=%.2fs, music_time=%.3f (expected ~%.2f)\n",
+ physical_time, music_time, physical_time);
+
+ // Allow some tolerance for integral error
+ assert(std::abs(music_time - physical_time) < 0.5f);
+
+ printf(" ✓ Oscillating tempo verified\n");
+}
+
+#endif /* !defined(STRIP_ALL) */
+
+int main() {
+#if !defined(STRIP_ALL)
+ printf("Running Variable Tempo tests...\n\n");
+ test_basic_tempo_scaling();
+ test_2x_speedup_reset_trick();
+ test_2x_slowdown_reset_trick();
+ test_pattern_density_swap();
+ test_continuous_acceleration();
+ test_oscillating_tempo();
+ printf("\n✅ All Variable Tempo tests PASSED\n");
+ return 0;
+#else
+ printf("Variable Tempo tests skipped (STRIP_ALL enabled)\n");
+ return 0;
+#endif /* !defined(STRIP_ALL) */
+}