Skip to content

Double-buffer RawSample (single_buffer=False) played with loop=False hangs; playing never clears #11086

Description

@dhalbert

Note: This issue was written by Claude (via Claude Code), not by a human. It was filed at the request of @dhalbert while working on the fixes for #10539 (PR #11085).

@dhalbert says: This is kind of an edge case; no one has reported it as a bug; I discovered it while testing audio PR's.

Summary

A RawSample constructed with single_buffer=False and played with loop=False plays a short burst and then hangs: audio stops, but .playing never becomes False. Observed on raspberrypi; likely affects other ports (needs verification — see below).

sample = audiocore.RawSample(sine_wave, sample_rate=8000, single_buffer=False)
i2s.play(sample, loop=False)
while i2s.playing:   # never exits
    pass

Root cause (raspberrypi)

A double-buffer RawSample's get_buffer (shared-module/audiocore/RawSample.c) returns half the buffer with GET_BUFFER_DONE and alternates halves forever — it never signals true end-of-data.

On rp2 (ports/raspberrypi/audio_dma.c), audio_dma_load_next_block treats GET_BUFFER_DONE && !loop by pointing the DMA channel's chain at itself to stop. With both channels chained to themselves, the ping-pong breaks: channel[0] plays its half and stops, and dma_callback_fun never re-triggers (channels_to_load_mask == 0, filled_count == 1). Audio halts after one half, and playing_in_progress is never cleared. The proper end-of-sample detection (output_length_used == 0) never fires because RawSample always returns nonzero halves.

WaveFile in double-buffer mode is unaffected: it returns GET_BUFFER_MORE_DATA for intermediate reads and a truly-empty GET_BUFFER_DONE at EOF, so playback terminates cleanly.

Why this is tricky (semantics)

Per the RawSample docstring, single_buffer=False (double-buffered) is intended for continuous, live-update playback — updating the buffer while it plays — and its examples all use loop=True. single_buffer=False + loop=False on a static buffer is effectively ill-defined: the sample has no natural end, so "play once and stop" has no clear meaning.

A fix probably needs a decision at the RawSample/shared layer about what this combination should do (e.g. play the whole buffer through once then stop, treat it as looping, or reject the combination), rather than a per-port patch.

TODO

  • Reproduce on other audio ports (espressif, nordic, atmel-samd, mimxrt10xx, stm, zephyr-cp) and note behavior per port.
  • Decide the intended semantics for double-buffer + loop=False.
  • Implement, likely at the RawSample/shared-module layer.

Context

Discovered while fixing the single-buffer loop=False "no sound" / stuck-playing bugs in #10539 (PR #11085). This double-buffer case was deliberately scoped out of that PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions