fw/services/speaker: drain audio pipeline before stopping#1700
Merged
Conversation
The service stopped the audio driver as soon as the active source had no more samples to generate. The drivers queue up to 64 ms of audio ahead of the DAC (double-buffered DMA/I2S pipe primed with silence plus a circular buffer), so playback was cut off up to 64 ms early. Sounds shorter than the pipeline depth were never heard at all: on sf32lb52, whose audio_stop halts the DMA and cuts amplifier power immediately, a 25 ms speaker_play_tone() produced only the amplifier pop. Once the source is exhausted, keep feeding silence until the pipeline has drained, then stop. Playback transitions through SpeakerStateDraining so clients can observe it. While only padding silence remains, allow same-priority playback to start instead of rejecting it, so rapid re-triggers (e.g. a metronome app) are not dropped. On nrf5/da7212 the change is behavior-neutral: its warm-pause audio_stop keeps the I2S stream draining anyway, so the warm pause merely starts ~80 ms later. Adds a host test suite with a fake audio driver covering the drain and preemption behavior. Signed-off-by: Robert Haist <rhaist@mailbox.org> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jplexer
approved these changes
Jul 14, 2026
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. Found in the wild: testing a metronome app (Pulse) on a real Pebble Time 2, every
speaker_play_tone()click came out as a faint mechanical pop instead of a tone — the speaker was clearly triggered, but no pitch was audible.Root cause: the speaker service stops the audio driver as soon as the active source has no more samples to generate. The drivers queue up to ~64ms of audio ahead of the DAC (double-buffered DMA/I2S pipe primed with silence, plus a circular buffer), so playback is cut off up to 64ms early — and sounds shorter than the pipeline depth are never heard at all. On sf32lb52, whose
audio_stop()halts the DMA and cuts amplifier power immediately, the app's 25ms clicks were killed ~30ms before their first sample reached the DAC, leaving only the amplifier pop. This affects all four sources (tone, note sequence, tracks, PCM stream); stream playback loses its final ~64ms tail.Fix. Once the source is exhausted, keep feeding silence until the pipeline has drained (80ms), then stop. Playback transitions through
SpeakerStateDraining. Semantic change to call out: while only padding silence remains, same-priority playback may now start instead of being rejected, so rapid re-triggers (e.g. a metronome's fast subdivisions) aren't dropped.Per-platform: sf32lb52 (obelix) is the affected driver. nrf5/da7212 (asterix) never had the audible bug — its warm-pause
audio_stop()keeps the I2S stream draining for 1s — and the change is behavior-neutral there (warm pause just starts ~80ms later). QEMU driver: neutral. Non-speaker boards build the stub branch.Testing. New
test_speaker_service.chost suite (fake audio driver): full tone + ≥ pipeline-depth of silence reaches the driver beforeaudio_stop(), same-priority preemption allowed during drain and still blocked during playback. 3 of 4 assertions fail without the fix; full suite passes. Verified end-to-end on qemu_emery with the same app — audibly distinct downbeat/upbeat tones where unfixed firmware plays nothing. Firmware builds: obelix@pvt, asterix, qemu_flint, qemu_emery.Related: #1641 touches the same file (stream session IDs) — one adjacent line in
prv_stop_internal, trivial merge either order; this fix also stops voice-note playback tails from being clipped.