Skip to content

miniaudio: bound the OpenSL ES buffer-queue drain - #19

Merged
Colton127 merged 1 commit into
mainfrom
fix/opensl-drain-timeout
Jul 27, 2026
Merged

miniaudio: bound the OpenSL ES buffer-queue drain#19
Colton127 merged 1 commit into
mainfrom
fix/opensl-drain-timeout

Conversation

@Colton127

@Colton127 Colton127 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Upstream miniaudio waits indefinitely for the OpenSL ES buffer queue to report empty. If the callback queue stalls, ma_device_stop() can block indefinitely; flutter_soloud now calls device stop routinely for idle stopping.

Approach

  • Keeps this as a clearly marked local patch to vendored miniaudio 0.11.25.
  • Uses the deviceType argument for queue, timing, and draining-flag selection, so duplex devices drain capture and playback independently.
  • Bounds polling to twice the configured queue duration, clamped to 200–1000 ms, with a 10 ms interval.
  • Checks GetState() and ends the drain attempt on failure without inspecting queue state.
  • Leaves the selected draining flag set through drain, OpenSL stop, and queue clearing to prevent late callbacks from enqueueing more data; both stop-error paths restore the flag before returning.

Verification

  • git diff --check
  • flutter pub get
  • flutter analyze (passed; six informational lints reported in existing example tests)
  • flutter test (passed)
  • cd example && flutter build apk --debug was attempted, but the build could not proceed because the example configures NDK 27.0.12077973 while dependency jni requires NDK 28.2.13676358. No Android/NDK compile result is claimed.

Maintenance note

The flutter_soloud local patch markers must be preserved when updating vendored miniaudio.

ma_device_drain__opensl() spins in an unbounded for(;;)/ma_sleep(10) loop
until the buffer queue reports empty, with no timeout and no iteration cap.
If the OpenSL callback thread stops servicing the queue -- audioserver
hiccup, route change, device disconnect -- the count never reaches zero and
ma_device_stop() never returns (alnitak#333). Since the audio device is stopped on
every idle timeout to release the audioserver AudioMix partial wakelock,
that is an unrecoverable hang on a routine path.

miniaudio itself disabled the equivalent stop inside ma_device_uninit() for
the same reason; see its "can result in a deadlock" comment there.

The queue can only legitimately hold `periods` buffers of
`periodSizeInFrames`, so the wait is capped at twice that duration, clamped
to [200ms, 1000ms]. Bailing out is safe: both callers in
ma_device_stop__opensl() immediately follow the drain with
SetPlayState(SL_PLAYSTATE_STOPPED) and Clear() on the same queue, so the
worst case is discarding a few milliseconds of tail audio that Clear() was
going to discard anyway.

This is a local patch to vendored miniaudio 0.11.25, marked with a greppable
banner. It should be carried across miniaudio updates, and is worth
reporting upstream: the loop is unbounded for every OpenSL user.

Verified without an NDK (the backend is __ANDROID__-only) by exercising the
added arithmetic against the real ma_device struct (4096x3@48k -> 512ms,
512x3@48k -> 200ms floor, 8192x4@44k -> 1000ms ceiling, 0x0@0 -> no divide
by zero, UINT32_MAX x8 -> no 32-bit overflow), compiling the patched
function verbatim against OpenSL stubs under C99 and C89 with -Wall -Wextra,
and running a stalled-queue harness: the unpatched function loops forever
(killed at 5s), the patched one returns after ~520ms matching the bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Colton127
Colton127 force-pushed the fix/opensl-drain-timeout branch from 99bfa3b to 08d7f79 Compare July 26, 2026 23:24
@Colton127
Colton127 merged commit 50b352c into main Jul 27, 2026
1 check passed
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