From b464969cd1f5dd4dceb996ad8410e2695ab477c4 Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 15 Feb 2026 14:45:15 +0100 Subject: docs: document audio WAV drift bug investigation Root cause: audio_render_ahead() over-renders by 366ms per 10s, causing progressive timing drift in WAV files. Events appear early in viewer. Findings: - Renders 11,733 extra frames over 40s (331,533 vs 319,800 expected) - Ring buffer accumulates excess audio (~19 frames/iteration) - WAV dump reads exact 533 frames but renders ~552 frames per call - Results in -180ms drift at 60 beats visible in timeline viewer Debug changes: - Added render tracking to audio.cc to measure actual vs expected - Added drift printf to tracker.cc for kick/snare timing analysis - Added WAV sample rate detection to timeline viewer See doc/AUDIO_WAV_DRIFT_BUG.md for complete analysis and proposed fixes. Co-Authored-By: Claude Sonnet 4.5 --- tools/timeline_editor/timeline-playback.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'tools') diff --git a/tools/timeline_editor/timeline-playback.js b/tools/timeline_editor/timeline-playback.js index 1bcdcd0..bfeb75a 100644 --- a/tools/timeline_editor/timeline-playback.js +++ b/tools/timeline_editor/timeline-playback.js @@ -67,17 +67,36 @@ export class PlaybackController { async loadAudioFile(file) { try { const arrayBuffer = await file.arrayBuffer(); + + // Detect original WAV sample rate before decoding + const dataView = new DataView(arrayBuffer); + let originalSampleRate = 32000; // Default assumption + + // Parse WAV header to get original sample rate + // "RIFF" at 0, "WAVE" at 8, "fmt " at 12, sample rate at 24 + if (dataView.getUint32(0, false) === 0x52494646 && // "RIFF" + dataView.getUint32(8, false) === 0x57415645) { // "WAVE" + originalSampleRate = dataView.getUint32(24, true); // Little-endian + console.log(`Detected WAV sample rate: ${originalSampleRate}Hz`); + } + if (!this.state.audioContext) { this.state.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } + this.state.audioBuffer = await this.state.audioContext.decodeAudioData(arrayBuffer); this.state.audioDuration = this.state.audioBuffer.duration; + this.state.originalSampleRate = originalSampleRate; + this.state.resampleRatio = this.state.audioContext.sampleRate / originalSampleRate; + + console.log(`AudioContext rate: ${this.state.audioContext.sampleRate}Hz, resample ratio: ${this.state.resampleRatio.toFixed(3)}x`); + this.renderWaveform(); this.dom.playbackControls.style.display = 'flex'; this.dom.playbackIndicator.style.display = 'block'; this.dom.clearAudioBtn.disabled = false; this.dom.replayBtn.disabled = false; - this.showMessage(`Audio loaded: ${this.state.audioDuration.toFixed(2)}s`, 'success'); + this.showMessage(`Audio loaded: ${this.state.audioDuration.toFixed(2)}s @ ${originalSampleRate}Hz`, 'success'); this.renderCallback('audioLoaded'); } catch (err) { this.showMessage(`Error loading audio: ${err.message}`, 'error'); -- cgit v1.2.3