Skip to content

Configurable audio-device idle timeout and off-UI-thread lifecycle - #17

Merged
Colton127 merged 5 commits into
mainfrom
lifecycle-revamp
Jul 24, 2026
Merged

Configurable audio-device idle timeout and off-UI-thread lifecycle#17
Colton127 merged 5 commits into
mainfrom
lifecycle-revamp

Conversation

@Colton127

Copy link
Copy Markdown
Owner

No description provided.

alnitak and others added 5 commits July 20, 2026 16:10
I'd like to propose adding my game to the README's example apps/games using flutter_soloud.
Add SUMOJI game to the list of apps
Generalize the fixed ~500 ms deferred idle-pause into a configurable
output-device lifecycle coordinator and move every blocking backend call
off the UI thread.

Behavior change:
- The default 500 ms idle timeout now applies to Android too. Previously
  the Android device was kept running while idle (grouped with Web),
  holding the audioserver AudioMix partial wakelock indefinitely. It now
  stops after the idle timeout like the other native platforms. The old
  keep-alive behavior is available opt-in via
  setAudioDeviceIdleTimeout(null). Web still never idle-stops (avoids the
  stale-buffer glitch, alnitak#446); the deferred/coalesced timeout keeps rapid
  stop->play from cycling the device on Android.

New API:
- setAudioDeviceIdleTimeout(Duration?): null keeps the device alive
  indefinitely (device-level replacement for a silent keep-alive sound),
  Duration.zero stops as soon as idle, positive keeps it running that long
  after going idle. Default 500 ms. Persists across deinit()/init().
- startAudioDevice() / stopAudioDevice(force): explicit prewarm and
  stop-without-mutating-voices (force stops during active playback).
- getAudioDeviceState(): cheap synchronous read of the miniaudio device
  state (AudioDeviceState enum).

Threading / lifecycle:
- Run init/deinit, changeDevice, and device start/stop off the UI isolate
  (worker isolates on FFI); fixes the startup ANR (alnitak#481). deinitAsync()
  added; deinit() kept for synchronous contexts.
- Single persistent scheduler thread handles deferred idle-stop and
  asynchronous resume via generation-based request coalescing so no
  ma_device_start()/stop() ever blocks the caller.
- Serialize all real device operations behind one mutex; make mInited
  atomic; make init all-or-nothing (roll back the backend on any failure).
- Order native callback teardown before Dart closes its NativeCallables to
  avoid use-after-free; preserve request order across init/dispose worker
  races with an atomic shutdown flag + lifecycle generation.

Interruption handling:
- Route OS interruptions through a dedicated interruption callback that
  stops the device without pausing/mutating voices; on interruption-end
  restart only when active playback requires it or keep-alive is
  configured. iOS reactivates AVAudioSession before restarting the unit.

Correctness:
- play/play3d/textToSpeech/busPlayOnEngine create the voice paused, then
  validate the handle (isValidVoiceHandle) before registering it and
  requesting device startup, fixing the old newHandle != 0 check that
  could register an error code as a handle. changeDevice now returns the
  real result and supports the default device (-1).

Tests: add audio_device_idle_timeout and audio_device_lifecycle_races
covering timeout policy, prewarm, interruption recovery, and scheduler
race/coalescing behavior.
@Colton127
Colton127 merged commit e32264e into main Jul 24, 2026
1 check failed
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.

3 participants