Skip to content

WAVStream: drain queued I2S audio by wall-clock instead of a fixed ibuf sleep - #282

Merged
ThomasFarstrike merged 2 commits into
MicroPythonOS:mainfrom
bitcoin3us:fix/esp32-wav-timing
Sep 3, 2026
Merged

ThomasFarstrike merged 2 commits into
MicroPythonOS:mainfrom
bitcoin3us:fix/esp32-wav-timing

Conversation

@bitcoin3us

Copy link
Copy Markdown
Contributor

Problem

On ESP32, WAVStream.play() sleeps a fixed ibuf / bytes_per_second after the last I2S write before firing on_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:

clip overshoot end-to-end before after
2 s 8 kHz mono +2048 ms 4185 ms 2219 ms
3 s 22.05 kHz mono +743 ms 3870 ms 3215 ms
3 s 22.05 kHz stereo +371 ms 3486 ms 3202 ms

(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 + codec on_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_samples over 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.I2S buffering behavior ever changes: the drain is derived from observed pacing, not from a driver-internal constant.

Verification

  • Raw machine.I2S consumption 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.
  • Fix verified end-to-end on the ESP32-S3 device by loading the patched module (numbers above), including a repeat run through the real AudioManager → output on_open/on_close path.
  • New unit tests for compute_drain_ms; full tests/test_stream_wav.py and tests/test_audiomanager.py pass on the desktop build.
  • ruff clean.

Merge checklist

  • tests/test_stream_wav.py passes
  • tests/test_audiomanager.py passes
  • CHANGELOG entry under "Future release"
  • Verified on ESP32 hardware (Waveshare ESP32-S3-Touch-LCD-3.5, ES8311)

🤖 Generated with Claude Code

…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>
@ThomasFarstrike
ThomasFarstrike merged commit 6a9de77 into MicroPythonOS:main Sep 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants