Skip to content

Fix non-looping audio playback across espressif, nordic, raspberrypi, zephyr-cp#11085

Open
dhalbert wants to merge 4 commits into
adafruit:mainfrom
dhalbert:dhalbert/fix-i2s-loop-false-10539
Open

Fix non-looping audio playback across espressif, nordic, raspberrypi, zephyr-cp#11085
dhalbert wants to merge 4 commits into
adafruit:mainfrom
dhalbert:dhalbert/fix-i2s-loop-false-10539

Conversation

@dhalbert

@dhalbert dhalbert commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

🤖 Generated with Claude Code

Playing a RawSample with loop=False misbehaved across several ports. The common cause: audiosample_get_buffer returns the final chunk of data together with GET_BUFFER_DONE, and a single-buffer RawSample (the default) returns its entire buffer with GET_BUFFER_DONE on the very first call. Ports that acted on DONE before copying that buffer dropped the audio entirely.

Per-port changes

  • espressif (I2S)common-hal/audiobusio/__init__.c
    • Defer stopping via a new last_buffer flag so the final GET_BUFFER_DONE buffer is copied out instead of dropped (single-buffer loop=False was completely silent).
    • Silence any unfilled remainder of the DMA buffer so a sample that ends mid-buffer (or a zero-length sample) doesn't play stale data.
    • Fix the preload cap, which compared a byte count against a frame count and left the DMA only ~¼ primed — this split playback into a short tone, a gap, then the rest.
  • nordic (I2S)common-hal/audiobusio/I2SOut.c — same last_buffer deferral so the final buffer is played; existing hold_value tail fill unchanged.
  • raspberrypiaudio_dma.c — a single-buffer, non-looping sample plays entirely in hardware via DMA chaining with no completion interrupt, so playing never cleared. audio_dma_get_playing now detects the drained channel and stops. (Different symptom: the audio played once, but playing stayed True forever.)
  • zephyr-cp (I2S)common-hal/audiobusio/I2SOut.c — copy the returned buffer before handling GET_BUFFER_DONE; adds a native_sim regression test (tests/test_audiobusio.py) asserting the sine wave reaches the I2S output.

Testing

  • espressif (ESP32-S3), nordic, and raspberrypi fixes verified on hardware with the issue's repro (single tone plays once; playing clears).
  • zephyr-cp change is covered by a new native_sim test but has not been run locally (no build env on hand) — please let CI exercise it.

Out of scope / follow-ups

dhalbert and others added 4 commits July 1, 2026 13:51
audiobusio.I2SOut dropped the final buffer that audiosample_get_buffer
returns together with GET_BUFFER_DONE, breaking before it was copied. A
single-buffer RawSample played with loop=False returns its entire buffer
with GET_BUFFER_DONE on the first call, so nothing was ever output.

- Add a last_buffer flag so the final buffer is copied out before
  stopping, instead of breaking before the copy.
- Silence any unfilled remainder of the DMA buffer so a sample that ends
  mid-buffer (or a zero-length sample) does not play stale data.
- Fix the preload cap, which compared a byte count against a frame count
  and left the DMA only ~1/4 primed, splitting playback into a short
  tone, a gap, then the rest.

Addresses adafruit#10539.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
audiobusio.I2SOut dropped the final buffer that audiosample_get_buffer
returns together with GET_BUFFER_DONE, breaking before it was copied. A
single-buffer RawSample played with loop=False returns its entire buffer
with GET_BUFFER_DONE on the first call, so nothing was ever output.

Add a last_buffer flag so the final buffer is copied out before stopping,
instead of breaking before the copy. The existing hold_value tail fill and
external stopping/paused handling are unchanged.

Addresses adafruit#10539.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single-buffer sample plays entirely in hardware via DMA chaining with no
completion interrupt (the interrupt is only enabled for the multi-buffer
path). With loop=False, channel[0] chains to itself and stops after one
pass, but nothing ever ran audio_dma_stop, so playing_in_progress stayed
true forever and I2SOut.playing (and AudioOut/PWMAudioOut) never became
false.

Detect this in audio_dma_get_playing: when a single-buffer, non-looping,
non-paused sample's DMA channel has drained, stop and report not playing.

Addresses adafruit#10539.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fill_buffer set stopping and returned on GET_BUFFER_DONE with loop=False
before copying the returned data. A single-buffer RawSample returns its
entire buffer with GET_BUFFER_DONE on the first call, so the block was
filled with silence and nothing was heard.

Copy the returned buffer first, then handle GET_BUFFER_DONE (drain and
stop). Add a native_sim regression test that plays a single-buffer,
non-looping sample and asserts the sine wave reaches the I2S output.

Addresses adafruit#10539.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

audiocore.RawSample doesn't work with loop=False on esp32* boards

1 participant