Skip to content

fw/services/speaker: drain audio pipeline before stopping#1700

Merged
jplexer merged 1 commit into
coredevices:mainfrom
rhaist:fix-speaker-pipeline-drain
Jul 14, 2026
Merged

fw/services/speaker: drain audio pipeline before stopping#1700
jplexer merged 1 commit into
coredevices:mainfrom
rhaist:fix-speaker-pipeline-drain

Conversation

@rhaist

@rhaist rhaist commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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.c host suite (fake audio driver): full tone + ≥ pipeline-depth of silence reaches the driver before audio_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.

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>
@rhaist
rhaist requested review from gmarull and jplexer as code owners July 14, 2026 09:39
@jplexer
jplexer merged commit 74312ef into coredevices:main Jul 14, 2026
42 checks passed
@rhaist
rhaist deleted the fix-speaker-pipeline-drain branch July 14, 2026 13:53
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