WAVStream: opt-in warm output so back-to-back clips don't click (ES8311) - #301
bitcoin3us wants to merge 5 commits into
Conversation
On ES8311 boards every clip started with an audible click. Not the files, and not fixable by reordering the codec unmute around each play: WAVStream re-created the MCLK PWM and machine.I2S for every clip and deinit'd both plus re-muted the DAC in its finally block, so every press had clock and mute transitions the codec turns into a click. Add a warm output, opt-in per board via AudioManager.Output(warm_ms=N): after a clip ends the MCLK/I2S keep running and the codec stays unmuted for N ms, and the next clip with the same rate/format/pins reuses them with no restart and no re-unmute. A one-shot machine.Timer(-1) releases the output (on_close, then deinit) after N ms idle; WAVStream. release_warm() releases it explicitly, and the I2S recorder releases it before taking the peripheral. For warm outputs the codec is unmuted only after the fresh clocks have been primed with silence. warm_ms=0 (the default) keeps the previous behaviour exactly, including on_open running before machine.I2S() binds the pins, which boards that mux pins between SD and I2S in those hooks (SQUiXL) rely on. Enabled at 30 s on the Waveshare ESP32-S3-Touch-LCD-3.5 speaker; confirmed by ear on that board that presses after the first are silent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
machine.Timer(-1) is not available on the ESP32 port (ValueError), so the idle release never fired there and a warm output stayed warm until released explicitly. Try the virtual timer first and fall back to hardware timer 3 (the LVGL task handler owns 0), and recreate the timer object on every arm: a deinit'd ESP32 hardware timer is not reliably re-armed by init(), which broke the cancel/re-arm cycle between back-to-back clips. Verified on the Waveshare ESP32-S3-Touch-LCD-3.5: second clip reuses the warm clocks, the timer re-arms, the output is released 29.3 s after the last clip, and the next clip re-warms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
CI note: the one red job is the macOS unit-test runner failing |
Indeed, that test is flaky on the (slow?) CI macOS machine(s). I've committed a fix attempt. |
…lick-pr # Conflicts: # CHANGELOG.md
|
Thanks — brought the branch up to date with |
|
Since it adds complexity, I'm a bit hesitant to merge as-is. As I understand it, it doesn't really solve the problem, as there will still be an audible click at the start of the first clip, as well as when the user waits more than 30 seconds to play the next clip. Many of these audio DAC have a section in the datasheet with something like "click-free start" or "glitch-free startup" which describes how to start playing audio without clicks. I haven't studied the ES8311 but perhaps that could offer an even better solution? BTW, I also fixed another test failure so feel free to re-sync this branch :-) |
…lick-pr # Conflicts: # CHANGELOG.md
|
Fair pushback — you're right that it's a mitigation, not a cure: the first clip after boot and the first clip after the 30 s idle still get the transition. (Branch re-synced with On the datasheet route: I looked at what our driver actually does around a clip versus what the ES8311 documents as the pop-free sequence, and there's a real gap.
So I think you're right that there's a better fix: implement the power-sequenced open/close in the ES8311 driver ( Proposal: park this PR, I test the datasheet sequence on the 3.5 by ear, and report back with numbers/recording. If it works, I'll rework this into that (much smaller) change; if the click survives the proper sequence, that's evidence the transition is in the I2S peripheral start itself and the warm output is the pragmatic answer. Sound OK? |
|
Bench results (Waveshare 3.5 with its speaker, ES8311; loud test clip = 150 ms digital silence then a 2 s near-full-scale 660/1320 Hz tone so any click is the hardware transition, not the file; volume 100; codec registers sampled mid-tone to confirm DAC powered + un-muted on every play; judged by ear):
So the datasheet's click-free start doesn't help here: the pop isn't in the codec's mute/power path, it's in the MCLK/I2S start itself (or the amp reacting to it), and only clock continuity removes it. Set C also answers the residual-click worry: with the silence-primed un-mute the first clip after a release was clean too, so the remaining cost of the warm approach is the clocks running for the idle window, not an audible artefact. Caveats: one board, one listener, one clip. If you'd still rather avoid the complexity, the one cheaper idea the data leaves open is applying the same silence-primed un-mute to the non-warm path (start clocks → prime with silence → un-mute) and testing whether that alone is enough; I'm happy to bench that as a follow-up, but on this evidence I'd merge the warm output as-is. |
Problem
On ES8311 boards (Waveshare ESP32-S3-Touch-LCD-3.5) every WAV clip started with an audible click, which makes a soundboard-style app unusable. It is not the files (they start at zero) and it is not fixable by reordering the codec unmute around each play (tried: unmute after I2S init plus a silence prime; still clicked). The cause is structural:
WAVStreamre-created the MCLK PWM andmachine.I2Sfor every clip and in itsfinallyblock deinit'd both and re-muted the DAC, so every press produced clock and mute transitions that the codec turns into a click.Fix
A warm output, opt-in per board via
AudioManager.Output(warm_ms=N):Nms. The next clip with the same rate, format and pins reuses them: no clock restart, no re-unmute, nothing to click.machine.Timer(-1)releases the output afterNms idle (on_close, thendeinit); the timer is cancelled while a clip plays and re-armed when it finishes.WAVStream.release_warm()releases it explicitly (an app can call it when leaving), and the I2S recorder releases it before taking the peripheral.warm_ms=0, the default, keeps the existing behaviour byte-for-byte, includingon_openrunning beforemachine.I2S()binds the pins, which boards that mux pins between SD and I2S in those hooks (SQUiXL) rely on. Only boards that opt in see any change.Verification
tests/test_stream_wav.py(30 tests, 7 new: release order, no-op release, release despiteon_closefailure, idle timer skipping while a clip plays and releasing when idle, one-shot timer period,warm_msdefault) andtests/test_audiomanager.py(32 tests, 2 new) pass on the desktop build.Notes
🤖 Generated with Claude Code
With thanks to the scientists and engineers who did the hard, unglamorous work that got us here.