// 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 #include #include #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) */ }