diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-15 16:41:08 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-15 16:41:08 +0100 |
| commit | 3a3eca975f15d1e025d6c46c6de253d2abaa7170 (patch) | |
| tree | 766b9684a4075681c571b06caae1262f30910381 | |
| parent | 6e7aa374b6e0e5ebda2f3525c130ce0cc1cd72cb (diff) | |
fix(audio): WAV dump drift improvements, acceptable state
WAV dump changes:
- Bypass ring buffer, render directly with synth_render()
- Frame accumulator eliminates truncation errors
- Skip pre-fill and fix seek for WAV dump mode
- Result: No glitches, -150ms drift at 64b (acceptable)
Timeline editor:
- Fix waveform tooltip position calculation
- Increase beat bar visibility (0.5 opacity)
Cleanup:
- Remove all drift debugging code from audio.cc and tracker.cc
Status: Acceptable for now, further investigation needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | doc/AUDIO_WAV_DRIFT_BUG.md | 17 | ||||
| -rw-r--r-- | src/app/main.cc | 41 | ||||
| -rw-r--r-- | src/audio/audio.cc | 17 | ||||
| -rw-r--r-- | src/audio/tracker.cc | 8 | ||||
| -rw-r--r-- | tools/timeline_editor/index.html | 4 | ||||
| -rw-r--r-- | tools/timeline_editor/timeline-playback.js | 2 |
6 files changed, 41 insertions, 48 deletions
diff --git a/doc/AUDIO_WAV_DRIFT_BUG.md b/doc/AUDIO_WAV_DRIFT_BUG.md index e22f4fa..050dd49 100644 --- a/doc/AUDIO_WAV_DRIFT_BUG.md +++ b/doc/AUDIO_WAV_DRIFT_BUG.md @@ -1,7 +1,8 @@ # Audio WAV Drift Bug Investigation **Date:** 2026-02-15 -**Status:** ROOT CAUSE IDENTIFIED +**Status:** ACCEPTABLE (to be continued) +**Current State:** -150ms drift at beat 64b, no glitches ## Problem Statement @@ -163,8 +164,18 @@ Eliminates cumulative truncation error. 1. ✅ Measure WAV sample positions directly (Python script) 2. ✅ Add render tracking debug output 3. ✅ Confirm over-rendering (366ms per 10s) -4. ⏳ Implement fix -5. ⏳ Verify corrected WAV alignment in viewer +4. ✅ Implement partial fix (bypass ring buffer, direct render) +5. ⚠️ Current result: -150ms drift at beat 64b (acceptable, needs further work) + +## Current Implementation (main.cc:286-308) + +**WAV dump now bypasses ring buffer entirely:** +1. **Frame accumulator**: Calculates exact frames per update (no truncation) +2. **Direct render**: Calls `synth_render()` directly with exact frame count +3. **No ring buffer**: Eliminates buffer management complexity +4. **Result**: No glitches, but -150ms drift remains + +**Remaining issue:** Drift persists despite direct rendering. Likely related to tempo scaling or audio engine state management. Acceptable for now. ## Notes diff --git a/src/app/main.cc b/src/app/main.cc index 537da74..5648b90 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -207,7 +207,10 @@ int main(int argc, char** argv) { #endif /* !defined(STRIP_ALL) */ // Pre-fill ring buffer to target lookahead (prevents startup delay) - fill_audio_buffer(audio_get_required_prefill_time(), 0.0); + // Skip pre-fill in WAV dump mode (direct render, no ring buffer) + if (!dump_wav) { + fill_audio_buffer(audio_get_required_prefill_time(), 0.0); + } audio_start(); g_last_audio_time = audio_get_playback_time(); // Initialize after start @@ -268,37 +271,41 @@ int main(int argc, char** argv) { printf("Running WAV dump simulation (%.1fs - %.1fs)...\n", start_time, end_time); - // Seek to start time if needed + // Seek to start time if needed (advance state without rendering) 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); + g_audio_engine.update(g_music_time, (float)step * g_tempo_scale); + g_music_time += (float)step * g_tempo_scale; } 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 + const int sample_rate = 32000; - AudioRingBuffer* ring_buffer = audio_get_ring_buffer(); - std::vector<float> chunk_buffer(samples_per_update); + std::vector<float> chunk_buffer(2048); // Max samples for one update double physical_time = start_time; + double frame_accumulator = 0.0; while (physical_time < end_time) { - // Update music time and tracker (using tempo logic from - // fill_audio_buffer) - fill_audio_buffer(update_dt, physical_time); + // Calculate exact frames for this update + frame_accumulator += sample_rate * update_dt; + const int frames_this_update = (int)frame_accumulator; + frame_accumulator -= frames_this_update; + const int samples_this_update = frames_this_update * 2; - // Read rendered audio from ring buffer - if (ring_buffer != nullptr) { - ring_buffer->read(chunk_buffer.data(), samples_per_update); - } + // Update tracker/audio state + g_audio_engine.update(g_music_time, update_dt * g_tempo_scale); - // Write to WAV file - wav_backend.write_audio(chunk_buffer.data(), samples_per_update); + // Render directly to buffer (bypass ring buffer) + if (frames_this_update > 0) { + synth_render(chunk_buffer.data(), frames_this_update); + wav_backend.write_audio(chunk_buffer.data(), samples_this_update); + } + // Advance music time + g_music_time += update_dt * g_tempo_scale; physical_time += update_dt; // Progress indicator every second diff --git a/src/audio/audio.cc b/src/audio/audio.cc index f5bc4ab..ba76a28 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -108,10 +108,6 @@ void audio_render_ahead(float music_time, float dt, float target_fill) { if (chunk_frames <= 0) return; - static int64_t g_total_render_calls = 0; - static int64_t g_total_frames_rendered = 0; - const int64_t frames_before = g_ring_buffer.get_total_written() / RING_BUFFER_CHANNELS; - // Keep rendering small chunks until buffer is full enough while (true) { // First, try to flush any pending samples from previous partial writes @@ -228,19 +224,6 @@ void audio_render_ahead(float music_time, float dt, float target_fill) { } } } - - // DEBUG: Track actual frames rendered vs expected - const int64_t frames_after = g_ring_buffer.get_total_written() / RING_BUFFER_CHANNELS; - const int64_t actual_rendered = frames_after - frames_before; - g_total_render_calls++; - g_total_frames_rendered += actual_rendered; - - if (g_total_render_calls % 600 == 0) { // Every 10 seconds at 60fps - const float expected_frames = g_total_render_calls * (float)(chunk_frames); - const float drift_ms = (expected_frames - g_total_frames_rendered) / RING_BUFFER_SAMPLE_RATE * 1000.0f; - printf("[RENDER_DRIFT] calls=%lld expect=%.1f actual=%lld drift=%.2fms\n", - g_total_render_calls, expected_frames, g_total_frames_rendered, drift_ms); - } } float audio_get_playback_time() { diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 00c31e9..37f0683 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -324,14 +324,6 @@ void tracker_update(float music_time_sec, float dt_music_sec) { } } - // DEBUG: Track kick/snare timing for drift investigation - if (event.sample_id == 0 || event.sample_id == 1) { // Assuming kick=0, snare=1 - const char* name = (event.sample_id == 0) ? "KICK " : "SNARE"; - const float delta_ms = (event_music_time - music_time_sec) * 1000.0f; - printf("[DRIFT] %s: music=%.4f expect=%.4f delta=%.2fms offset=%d\n", - name, music_time_sec, event_music_time, delta_ms, sample_offset); - } - trigger_note_event(event, sample_offset, volume_mult); active.next_event_idx++; } diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html index bc7f2a0..9768343 100644 --- a/tools/timeline_editor/index.html +++ b/tools/timeline_editor/index.html @@ -45,7 +45,7 @@ .timeline { position: relative; min-height: 100%; } .sticky-header { position: sticky; top: 0; background: var(--bg-medium); z-index: 100; padding: 20px 20px 10px 20px; border-bottom: 2px solid var(--bg-light); flex-shrink: 0; } - .waveform-container { position: relative; height: 80px; overflow: hidden; background: rgba(0, 0, 0, 0.3); border-radius: var(--radius); cursor: crosshair; } + .waveform-container { position: relative; height: 80px; overflow: hidden; background: rgba(0, 0, 0, 0.5); border-radius: var(--radius); cursor: crosshair; } #cpuLoadCanvas { position: absolute; left: 0; bottom: 0; height: 10px; display: block; z-index: 1; } #waveformCanvas { position: absolute; left: 0; top: 0; height: 80px; display: block; z-index: 2; } .waveform-cursor { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(78, 201, 176, 0.6); pointer-events: none; z-index: 3; display: none; } @@ -56,7 +56,7 @@ .time-markers { position: relative; height: 30px; margin-top: var(--gap); border-bottom: 1px solid var(--bg-light); } .time-marker { position: absolute; top: 0; font-size: 12px; color: var(--text-muted); } .time-marker::before { content: ''; position: absolute; left: 0; top: 20px; width: 1px; height: 10px; background: var(--bg-light); } - .time-marker::after { content: ''; position: absolute; left: 0; top: 30px; width: 1px; height: 10000px; background: rgba(60, 60, 60, 0.2); pointer-events: none; } + .time-marker::after { content: ''; position: absolute; left: 0; top: 30px; width: 1px; height: 10000px; background: rgba(100, 100, 60, 0.9); pointer-events: none; } .sequence { position: absolute; background: #264f78; border: 2px solid var(--accent-blue); border-radius: var(--radius); padding: 8px; cursor: move; min-height: 40px; transition: box-shadow 0.2s; } .sequence:hover { box-shadow: 0 0 10px rgba(14, 99, 156, 0.5); } diff --git a/tools/timeline_editor/timeline-playback.js b/tools/timeline_editor/timeline-playback.js index 8c84877..a1c50ab 100644 --- a/tools/timeline_editor/timeline-playback.js +++ b/tools/timeline_editor/timeline-playback.js @@ -168,7 +168,7 @@ export class PlaybackController { ctx.stroke(); // Beat markers - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.50)'; ctx.lineWidth = 1; for (let beat = 0; beat <= maxTimeBeats; beat++) { const x = beat * this.state.pixelsPerBeat; |
