Flush the final partial speaker buffer - #325
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an audio playback edge case in Speaker_Class::spk_task where the final, partially-filled mix buffer could be dropped when a WAV ends naturally mid-buffer and no subsequent request is published in time, causing audible cutoffs/gaps.
Changes:
- Track whether a channel ended via natural exhaustion and should flush a partial buffer (
flush_partial). - Promote the amount of already-mixed audio into
data_lengthon the early-continuepath so the partial buffer is still transmitted. - Preserve the existing “discard immediately” behavior for explicit stop-marker requests.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| { // nothing to do; a writer caught mid-publish raises the bit itself. | ||
| ch_info->diff = 0; | ||
| ch_info->index = 0; | ||
| if (flush_partial && data_length < idx) { data_length = idx; } |
There was a problem hiding this comment.
An out-of-bounds read cannot actually occur here: dma_buf_len is rounded down to an even value when the task starts, and sound_buf32 is zeroed with memset at the top of every round, so with an odd data_length the packing loop's final v2 reads an in-bounds, zero-initialized element.
That said, an odd count did become newly reachable through this flush path, and it leaves a trailing half-word in the transmitted bytes (on the HW v1 layout that replaces the final real sample with the zero half). Rounding the flushed count up to even avoids both concerns at the cost of one silent padding sample, so I adopted that in 94dffd8.
Problem
When a wav reaches its natural end partway through the speaker task's working
buffer and no follow-up request has been published, the channel loop exits
through an early
continuebefore the samples already mixed into the workingbuffer are counted into
data_length. Everything generated in that finalround — up to one working buffer — is silently dropped instead of being sent
to I2S.
For a single
playRaw()call this cuts off the tail of the sound. Anapplication that streams audio as consecutive short
playRaw()chunks canlose the tail of every chunk whenever publishing the next request loses the
race against the speaker task, which turns into periodic gaps.
Fix
The task now tracks whether it reached the exit path through natural
exhaustion of the request. In that case the number of samples generated so
far is promoted into
data_length(with max semantics, so other channelsmixed into the same buffer are unaffected) before the loop moves on, and the
partial buffer is transmitted as usual.
Explicit stop requests (
stop()markers) keep the existing behavior ofdiscarding immediately: cutting the sound short is the point of a stop, so
nothing is flushed on that path.