WAVStream: drain queued I2S audio by wall-clock instead of a fixed ibuf sleep - #282
Merged
ThomasFarstrike merged 2 commits intoSep 3, 2026
Merged
Conversation
…uf sleep After the last I2S write, WAVStream on ESP32 slept a fixed ibuf/bytes_per_second before firing on_complete. The write loop already paces against DMA drain, so nearly nothing is left queued at that point, and the estimate overshoots by the full ibuf duration: +2048 ms for an 8 kHz mono clip, +743 ms at 22.05 kHz mono, +371 ms at 22.05 kHz stereo. Every clip appeared to run long (a 2 s 8 kHz clip took 4.2 s end to end) and back-to-back playback (playlists, repeat) gained the same dead gap between clips. Track the duration of audio queued (progress samples over the original rate, which also covers ADPCM and upsampled playback) and the wall-clock time spent queueing it, then sleep only the difference. Measured on a Waveshare ESP32-S3-Touch-LCD-3.5 (ES8311 speaker output), end-to-end player start to on_complete: 3 s 22.05 kHz mono 3870 ms -> 3215 ms 3 s 22.05 kHz stereo 3486 ms -> 3202 ms 2 s 8 kHz mono 4185 ms -> 2219 ms The remaining ~200 ms is player-thread startup and codec on_open, not drain error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On ESP32,
WAVStream.play()sleeps a fixedibuf / bytes_per_secondafter the last I2S write before firingon_complete. But the blocking write loop already paces against DMA drain (measured: writes complete in almost exactly the clip duration), so nearly nothing is left queued when the loop exits — and the fixed estimate overshoots by the full ibuf duration, worst at low byte rates:(Measured on a Waveshare ESP32-S3-Touch-LCD-3.5 through an ES8311 "Speaker" output, player start →
on_complete; the residual ~200 ms is player-thread startup + codecon_open, not drain error.)Every clip appears to run long, and anything sequencing off
on_complete(playlists, repeat chaining, sound-then-action flows) inherits the same dead gap between clips.Fix
Track the duration of the audio actually queued (
_progress_samplesover the original rate — correct for ADPCM and for upsampled playback, since upsampling scales data and rate by the same factor) and the wall-clock time spent queueing it, then sleep only the difference:compute_drain_ms(played_ms, elapsed_ms). If playback ran long (stalls), the drain is clamped to zero rather than sleeping a negative/absurd amount.This also self-adapts if the underlying
machine.I2Sbuffering behavior ever changes: the drain is derived from observed pacing, not from a driver-internal constant.Verification
machine.I2Sconsumption rates were measured first on hardware to rule out clock/slot-config bugs: mono and stereo at 22050 Hz and mono at 8000 Hz all drain within ~1% of nominal, deterministically. The wall-clock discrepancies were entirely the drain sleep.AudioManager→ outputon_open/on_closepath.compute_drain_ms; fulltests/test_stream_wav.pyandtests/test_audiomanager.pypass on the desktop build.ruffclean.Merge checklist
tests/test_stream_wav.pypassestests/test_audiomanager.pypasses🤖 Generated with Claude Code