Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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