Skip to content

wait some ms to pause device when there are no more sounds playing#486

Merged
alnitak merged 7 commits into
mainfrom
manyStops
Jun 20, 2026
Merged

wait some ms to pause device when there are no more sounds playing#486
alnitak merged 7 commits into
mainfrom
manyStops

Conversation

@alnitak

@alnitak alnitak commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Description

On some platforms (notably iOS) the OS can take a short time to fully tear down or hand back the audio session after the last active voice is stopped/paused. If we pause the SoLoud engine immediately, a subsequent play/resume request can arrive while the audio device is still settling, which can cause the OS to keep the Control Center / lock-screen media controls in an inconsistent state or to fail to restart playback cleanly.

To avoid this, we defer the engine pause by ~100 ms. This gives the audio backend and the OS enough time to stabilize, while still using the engine promptly once no voices remain active. It also coalesces rapid stop/pause events so we don't pause/unpause the device repeatedly. The latter happens when stopping many sounds in a short time and new sounds are then started causing a lag when starting to play again.

fixes #485

Type of Change

  • 🛠️ Bug fix (non-breaking change which fixes an issue)

eddyleelin added a commit to eddyleelin/flutter_soloud that referenced this pull request Jun 17, 2026
The auto-pause-when-idle path (Player::setPause / Player::stop /
Player::disposeSound) called soloud.pause() synchronously when the last active
voice went away. On iOS soloud.pause() -> soloud_miniaudio_pause() ->
ma_device_stop() -> AURemoteIO::Stop() does a blocking mach_msg round-trip to
the audio HAL ON THE CALLING THREAD. Those sites are reached from Dart through a
synchronous FFI prologue (so on the Flutter UI thread), so under audio-server
contention the device stop blocks the UI thread for seconds -> App Hang / ANR.

This is the iOS sibling of the Android device-teardown hang already fixed on
this branch, and matches upstream flutter_soloud alnitak#485 (regressed by alnitak#406): the
maintainer confirmed the same UI stall on iPhone, not on Android. Apple confirms
AudioOutputUnitStop is always synchronous and waits on the HAL (rdar://666667
"AudioOutputUnitStop will be stuck in the main thread call"); miniaudio
ma_device_stop blocks on ma_event_wait (alnitak#400).

Fix (same shape as upstream PR alnitak#486 "wait some ms to pause device", hardened):
route setPause/stop/disposeSound through a new Player::pauseEngine() that runs
the device pause OFF the caller thread via a coalesced, settle-delayed worker
(DeferredEnginePause, new header-only device_pause.h). The caller returns
immediately; the pause fires ~150ms later on a worker, only if the engine is
still inited and idle then. Rapid stop/dispose bursts (a screen teardown
disposing N sources) coalesce via a generation counter to a single device pause,
and a stop->play within the settle window cancels the pause entirely (the device
is never needlessly stopped+restarted, which also avoids the alnitak#446
stale-buffer-on-restart glitch).

Hardening over alnitak#486: a generation counter coalesces bursts to one real pause
(vs. a thread per call); a ~150ms settle delay (alnitak#486 committed 2000ms, a debug
leftover); the generation cell lives in a shared_ptr so a worker never touches a
destroyed Player; Player::dispose() cancels pending pauses before deinit; and
soloud_miniaudio_pause() now null-checks gDevicePtr since the deferred worker can
race a teardown that nulled it. disposeAllSound()'s pre-free pause is left
synchronous (it guards the audio thread off filter memory during teardown, a
different purpose, and is not on a hot path).

The deferred primitive (device_pause.h) has a host unit test
(test/native/device_pause_test.cpp, no audio device needed) proving the caller
never blocks even when the pause work is slow, that the pause fires once when
idle, is skipped when a voice resumed, coalesces a burst to one pause, and
honors cancel() — verified red (naive synchronous pause) -> green, ThreadSanitizer
clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alnitak alnitak merged commit ab5748f into main Jun 20, 2026
1 check passed
@alnitak alnitak deleted the manyStops branch June 20, 2026 12:11
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.

perf: Performance issue when having simultaneous soloud.stop's

1 participant