diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/main.cc | 47 | ||||
| -rw-r--r-- | src/app/test_demo.cc | 7 | ||||
| -rw-r--r-- | src/audio/audio.cc | 29 | ||||
| -rw-r--r-- | src/audio/audio.h | 6 | ||||
| -rw-r--r-- | src/audio/tracker.cc | 10 | ||||
| -rw-r--r-- | src/tests/audio/test_jittered_audio.cc | 5 | ||||
| -rw-r--r-- | src/tests/audio/test_silent_backend.cc | 8 | ||||
| -rw-r--r-- | src/tests/audio/test_wav_dump.cc | 2 |
8 files changed, 92 insertions, 22 deletions
diff --git a/src/app/main.cc b/src/app/main.cc index 90e3015..537da74 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -45,6 +45,8 @@ int main(int argc, char** argv) { int width = 1280; int height = 720; bool dump_wav = false; + float dump_wav_start = -1.0f; + float dump_wav_duration = -1.0f; bool tempo_test_enabled = false; bool headless_mode = false; float headless_duration = 30.0f; @@ -73,6 +75,12 @@ int main(int argc, char** argv) { if (i + 1 < argc && argv[i + 1][0] != '-') { wav_output_file = argv[++i]; } + } else if (strcmp(argv[i], "--dump-wav-start") == 0 && i + 1 < argc) { + dump_wav_start = atof(argv[i + 1]); + ++i; + } else if (strcmp(argv[i], "--dump-wav-duration") == 0 && i + 1 < argc) { + dump_wav_duration = atof(argv[i + 1]); + ++i; } else if (strcmp(argv[i], "--tempo") == 0) { tempo_test_enabled = true; #if defined(DEMO_HEADLESS) @@ -198,8 +206,8 @@ int main(int argc, char** argv) { } #endif /* !defined(STRIP_ALL) */ - // Pre-fill using same pattern as main loop (100ms) - fill_audio_buffer(0.1f, 0.0); + // Pre-fill ring buffer to target lookahead (prevents startup delay) + fill_audio_buffer(audio_get_required_prefill_time(), 0.0); audio_start(); g_last_audio_time = audio_get_playback_time(); // Initialize after start @@ -247,10 +255,29 @@ int main(int argc, char** argv) { #if !defined(STRIP_ALL) // In WAV dump mode, run headless simulation and write audio to file if (dump_wav) { - printf("Running WAV dump simulation...\n"); - + // Determine start and end times + const float start_time = (dump_wav_start >= 0.0f) ? dump_wav_start : 0.0f; const float demo_duration = GetDemoDuration(); - const float max_duration = (demo_duration > 0.0f) ? demo_duration : 60.0f; + float end_time; + if (dump_wav_duration >= 0.0f) { + end_time = start_time + dump_wav_duration; + } else { + end_time = (demo_duration > 0.0f) ? demo_duration : 60.0f; + } + + printf("Running WAV dump simulation (%.1fs - %.1fs)...\n", start_time, + end_time); + + // Seek to start time if needed + if (start_time > 0.0f) { + const double step = 1.0 / 60.0; + for (double t = 0.0; t < start_time; t += step) { + fill_audio_buffer(step, t); + audio_render_silent((float)step); + } + printf("Seeked to %.1fs\n", start_time); + } + const float update_dt = 1.0f / 60.0f; // 60Hz update rate const int frames_per_update = (int)(32000 * update_dt); // ~533 frames const int samples_per_update = frames_per_update * 2; // Stereo @@ -258,8 +285,8 @@ int main(int argc, char** argv) { AudioRingBuffer* ring_buffer = audio_get_ring_buffer(); std::vector<float> chunk_buffer(samples_per_update); - double physical_time = 0.0; - while (physical_time < max_duration) { + double physical_time = start_time; + while (physical_time < end_time) { // Update music time and tracker (using tempo logic from // fill_audio_buffer) fill_audio_buffer(update_dt, physical_time); @@ -278,13 +305,13 @@ int main(int argc, char** argv) { if ((int)physical_time % 1 == 0 && physical_time - update_dt < (int)physical_time) { printf(" Rendering: %.1fs / %.1fs (music: %.1fs, tempo: %.2fx)\r", - physical_time, max_duration, g_music_time, g_tempo_scale); + physical_time, end_time, g_music_time, g_tempo_scale); fflush(stdout); } } - printf("\nWAV dump complete: %.2fs physical, %.2fs music time\n", - physical_time, g_music_time); + printf("\nWAV dump complete: %.2fs (%.2fs - %.2fs), music: %.2fs\n", + physical_time - start_time, start_time, physical_time, g_music_time); #if defined(DEMO_HEADLESS) g_wav_backend_ptr = nullptr; diff --git a/src/app/test_demo.cc b/src/app/test_demo.cc index 39dbcba..5775e74 100644 --- a/src/app/test_demo.cc +++ b/src/app/test_demo.cc @@ -280,14 +280,13 @@ int main(int argc, char** argv) { g_tempo_scale = 1.0f; // No tempo variation } - g_music_time += audio_dt * g_tempo_scale; - g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale); audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); + g_music_time += audio_dt * g_tempo_scale; }; - // Pre-fill using same pattern as main loop (100ms) - fill_audio_buffer(0.1f, 0.0); + // Pre-fill ring buffer to target lookahead (prevents startup delay) + fill_audio_buffer(audio_get_required_prefill_time(), 0.0); audio_start(); g_last_audio_time = audio_get_playback_time(); diff --git a/src/audio/audio.cc b/src/audio/audio.cc index d044b00..ba76a28 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -57,11 +57,35 @@ void audio_init() { g_audio_backend->init(); } +float audio_get_required_prefill_time() { + return (float)RING_BUFFER_LOOKAHEAD_MS / 1000.0f; +} + +bool audio_is_prefilled() { + const int buffered = g_ring_buffer.available_read(); + const float buffered_time = + (float)buffered / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); + const float required = audio_get_required_prefill_time(); + return buffered_time >= (required - 0.001f); // 1ms tolerance +} + void audio_start() { if (g_audio_backend == nullptr) { printf("Cannot start: audio not initialized.\n"); return; } + +#if !defined(STRIP_ALL) + if (!audio_is_prefilled()) { + const int buffered = g_ring_buffer.available_read(); + const float buffered_ms = + (float)buffered / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS) * + 1000.0f; + printf("WARNING: Audio buffer not pre-filled (%.1fms < %.1fms)\n", + buffered_ms, audio_get_required_prefill_time() * 1000.0f); + } +#endif + g_audio_backend->start(); } @@ -73,6 +97,11 @@ void audio_render_ahead(float music_time, float dt, float target_fill) { // Render in small chunks to keep synth time synchronized with tracker // Chunk size: one frame's worth of audio (~16.6ms @ 60fps) + // TODO(timing): CRITICAL BUG - Truncation here may cause 180ms drift over 63 beats + // (int) cast loses fractional samples: 0.333 samples/frame * 2560 frames = 853 samples = 27ms + // But observed drift is 180ms, so this is not the only source (27ms < 180ms) + // NOTE: This is NOT a float vs double precision issue - floats handle <500s times fine + // See also: tracker.cc BPM timing calculation const int chunk_frames = (int)(dt * RING_BUFFER_SAMPLE_RATE); const int chunk_samples = chunk_frames * RING_BUFFER_CHANNELS; diff --git a/src/audio/audio.h b/src/audio/audio.h index 9d521e6..beb994f 100644 --- a/src/audio/audio.h +++ b/src/audio/audio.h @@ -23,6 +23,12 @@ struct SpecHeader { void audio_init(); void audio_start(); // Starts the audio device callback +// Get required pre-fill time (matches ring buffer lookahead) +float audio_get_required_prefill_time(); + +// Check if buffer is sufficiently pre-filled +bool audio_is_prefilled(); + // Ring buffer audio rendering (main thread fills buffer) // target_fill: Target buffer fill time in seconds (default: // RING_BUFFER_LOOKAHEAD_MS/1000) diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 1c0a9b2..37f0683 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -234,6 +234,16 @@ static void trigger_note_event(const TrackerEvent& event, } void tracker_update(float music_time_sec, float dt_music_sec) { + // TODO(timing): CRITICAL BUG - Events trigger ~180ms early over 63 beats @ BPM=90 + // Observed: Beat 63 snare at 41.82s in WAV, should be at 42.00s (180ms drift) + // NOTE: This is NOT a float vs double precision issue - floats handle <500s times fine + // Root cause unknown - suspects: + // 1. Systematic bias in time calculation (not random accumulation) + // 2. Truncation in audio.cc:103 chunk_frames = (int)(dt * sample_rate) + // 3. BPM calculation precision below (unit_duration_sec) + // 4. Mismatch between tracker time and actual sample rendering + // See also: audio.cc sample rendering truncation + // Unit-less timing: 1 unit = 4 beats (by convention) const float BEATS_PER_UNIT = 4.0f; const float unit_duration_sec = diff --git a/src/tests/audio/test_jittered_audio.cc b/src/tests/audio/test_jittered_audio.cc index d8260ec..d7c6a7d 100644 --- a/src/tests/audio/test_jittered_audio.cc +++ b/src/tests/audio/test_jittered_audio.cc @@ -42,11 +42,10 @@ void test_jittered_audio_basic() { float music_time = 0.0f; for (float t = 0.0f; t < total_time; t += dt) { - music_time += dt; // Normal tempo - // Update tracker and fill buffer tracker_update(music_time, dt); audio_render_ahead(music_time, dt); + music_time += dt; // Sleep minimal time to let audio thread run std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -114,7 +113,7 @@ void test_jittered_audio_with_acceleration() { // Update tracker and fill buffer tracker_update(music_time, dt * tempo_scale); - audio_render_ahead(music_time, dt); + (void)audio_render_ahead(music_time, dt); // Sleep minimal time to let audio thread run std::this_thread::sleep_for(std::chrono::milliseconds(1)); diff --git a/src/tests/audio/test_silent_backend.cc b/src/tests/audio/test_silent_backend.cc index 3dc1cd4..cecf72c 100644 --- a/src/tests/audio/test_silent_backend.cc +++ b/src/tests/audio/test_silent_backend.cc @@ -97,7 +97,7 @@ void test_silent_backend_tracking() { assert(backend.get_voice_trigger_count() == 1); // Render audio (calls on_frames_rendered) - audio_render_ahead(0.0f, 0.1f); // Render ~0.1 seconds + (void)audio_render_ahead(0.0f, 0.1f); // Render ~0.1 seconds assert(backend.get_frames_rendered() > 0); // Reset stats @@ -123,7 +123,7 @@ void test_audio_playback_time() { assert(t0 == 0.0f); // Render some audio - audio_render_ahead(0.5f, 0.1f); // Advance music time to 0.5s + (void)audio_render_ahead(0.5f, 0.1f); // Advance music time to 0.5s // Playback time should advance based on frames rendered // Note: audio_get_playback_time() tracks cumulative frames consumed @@ -131,7 +131,7 @@ void test_audio_playback_time() { assert(t1 >= 0.0f); // Should have advanced // Render more - audio_render_ahead(1.0f, 0.5f); + (void)audio_render_ahead(1.0f, 0.5f); float t2 = audio_get_playback_time(); assert(t2 >= t1); // Should continue advancing @@ -152,7 +152,7 @@ void test_audio_buffer_partial_writes() { // Note: With SilentBackend, frames_rendered won't increase because // there's no audio callback consuming from the ring buffer for (int i = 0; i < 10; ++i) { - audio_render_ahead((float)i * 0.1f, 0.1f); + (void)audio_render_ahead((float)i * 0.1f, 0.1f); } // Buffer should have handled multiple writes correctly (no crash) diff --git a/src/tests/audio/test_wav_dump.cc b/src/tests/audio/test_wav_dump.cc index a0f2a4a..ce161a4 100644 --- a/src/tests/audio/test_wav_dump.cc +++ b/src/tests/audio/test_wav_dump.cc @@ -60,10 +60,10 @@ void test_wav_format_matches_live_audio() { for (float t = 0.0f; t < duration; t += update_dt) { // Update audio engine (triggers patterns) fixture.engine().update(music_time, update_dt); - music_time += update_dt; // Render audio ahead audio_render_ahead(music_time, update_dt); + music_time += update_dt; // Read from ring buffer if (ring_buffer != nullptr) { |
