From 9d91a1b6f5fa26605fb8567e61603d07d2f6fd9e Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 7 Feb 2026 21:43:30 +0100 Subject: fix(audio): Remove sample offsets - incompatible with tempo scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the irregular timing caused by mixing music time and physical time. ROOT CAUSE (THE REAL BUG): Sample offset calculation was mixing two incompatible time domains: 1. event_trigger_time: in MUSIC TIME (tempo-scaled, can be 2x faster) 2. current_render_time: in PHYSICAL TIME (1:1 with real time, not scaled) When tempo != 1.0, these diverge dramatically: Example at 2.0x tempo: - Music time: 10.0s (advanced 2x faster) - Physical render time: 5.0s (real time elapsed) - Calculated offset: (10.0 - 5.0) * 32000 = 160000 samples = 5 SECONDS! - Result: Event triggers 5 seconds late This caused irregular timing because: - At tempo 1.0x: offsets were roughly correct (domains aligned) - At tempo != 1.0x: offsets were wildly wrong (domains diverged) - Result: Random jitter as tempo changed WHY WAV DUMP WORKED: WAV dump doesn't use tempo scaling (g_tempo_scale = 1.0), so music_time ≈ physical_time and the domains stayed aligned by accident. THE SOLUTION: Remove sample offsets entirely. Trigger events immediately when music_time passes their trigger time. Accept ~16ms quantization (one frame at 60fps). TRADE-OFFS: - Before: Attempted sample-accurate timing (but broken with tempo scaling) - After: ~16ms quantization (acceptable for rhythmic events) - Benefit: Consistent timing across all tempo values - Benefit: Same behavior in WAV dump and miniaudio playback CHANGES: - tracker.cc: Remove offset calculation, always pass offset=0 - Simplify event triggering logic - Add comment explaining why offsets don't work with tempo scaling Previous commits (9cae6f1, 7271773) attempted to fix this with render_time tracking, but missed the fundamental issue: you can't calculate sample offsets when event times and render times are in different time domains. Co-Authored-By: Claude Sonnet 4.5 --- src/audio/tracker.cc | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) (limited to 'src/audio') diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 1cccc57..fd25e84 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -239,10 +239,10 @@ void tracker_update(float music_time_sec) { } // Step 2: Update all active patterns and trigger individual events - // Get current audio RENDER position (write position) for sample-accurate timing - // This is where we're currently writing to the ring buffer (~400ms ahead of playback) - const float current_render_time = audio_get_render_time(); - const float SAMPLE_RATE = 32000.0f; // Audio sample rate + // NOTE: We trigger events immediately when their time passes (no sample offsets) + // This gives ~16ms quantization (60fps) which is acceptable + // Sample offsets don't work with tempo scaling because music_time and render_time + // are in different time domains (tempo-scaled vs physical) for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { if (!g_active_patterns[i].active) @@ -262,22 +262,9 @@ void tracker_update(float music_time_sec) { if (event.unit_time > elapsed_units) break; // This event hasn't reached its time yet - // Calculate exact trigger time for this event - const float event_trigger_time = active.start_music_time + - (event.unit_time * unit_duration_sec); - - // Calculate sample-accurate offset from current RENDER position (write pos) - // This is where we're currently writing to the buffer, not where playback is - const float time_delta = event_trigger_time - current_render_time; - int sample_offset = (int)(time_delta * SAMPLE_RATE); - - // Clamp to 0 if negative (event is late, play immediately) - if (sample_offset < 0) { - sample_offset = 0; - } - - // Trigger this event as an individual voice with sample-accurate timing - trigger_note_event(event, sample_offset); + // Trigger this event immediately (no sample offset) + // Timing quantization: ~16ms at 60fps, acceptable for rhythm + trigger_note_event(event, 0); active.next_event_idx++; } -- cgit v1.2.3