flutter_soloud handles the playing of audio, but does not handle configuration of the OS level audio sessions and their contexts. If you wish to change the audio session away from OS defaults (for example, to always play over other apps or ignore silent mode), we recommend audio_session.
This page provides a guide on how to use audio_session to manage the audio context.
Please, look at the lib/audio_context/audio_context.dart example for a simple implementation to handle audio interruptions and the use of audio_service also for background audio.
First, add audio_session to your pubspec.yaml:
dependencies:
audio_session: ^0.2.2Initialize and configure audio_session in your app. A good place to do this is in the initState of your widget and should be done before any audio is played.
import 'package:audio_session/audio_session.dart';
// ...
late final AudioSession session;
@override
void initState() {
super.initState();
AudioSession.instance.then((audioSession) async {
session = audioSession;
await session.configure(
const AudioSessionConfiguration(
androidWillPauseWhenDucked: true,
androidAudioAttributes: AndroidAudioAttributes(
usage: AndroidAudioUsage.media,
contentType: AndroidAudioContentType.music,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransientMayDuck,
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.none,
),
);
_handleInterruptions(session);
});
}When you want to start to play audio, you need to activate the audio session:
await session.setActive(true);
final handle = await SoLoud.instance.play(sound);You need to listen to interruption events to pause, resume, or duck your audio.
void _handleInterruptions(AudioSession audioSession) {
audioSession.becomingNoisyEventStream.listen((_) {
// The user unplugged headphones, so we should pause.
SoLoud.instance.setPause(soundHandle, true);
});
audioSession.interruptionEventStream.listen((event) {
if (event.begin) {
switch (event.type) {
case AudioInterruptionType.duck:
// Another app started playing audio and we should duck.
SoLoud.instance.fadeGlobalVolume(0.1, const Duration(milliseconds: 300));
break;
case AudioInterruptionType.pause:
case AudioInterruptionType.unknown:
// Another app started playing audio and we should pause.
SoLoud.instance.setPause(soundHandle, true);
break;
}
} else {
switch (event.type) {
case AudioInterruptionType.duck:
// The interruption ended and we should unduck.
SoLoud.instance.fadeGlobalVolume(1, const Duration(milliseconds: 300));
break;
case AudioInterruptionType.pause:
// The interruption ended and we should resume.
SoLoud.instance.setPause(soundHandle, false);
break;
case AudioInterruptionType.unknown:
// The interruption ended but we should not resume.
break;
}
}
});
}Voice state and output-device state are separate. Pausing a voice preserves its SoundHandle and its SoLoud state. Device lifecycle operations only stop, start, or prewarm the platform audio output; they do not maintain a second copy of voice volume, pan, speed, fades, looping, seek position, or other properties.
play() and play3d() are synchronous and return a SoundHandle immediately. An unpaused voice starts the output device after the voice has been created successfully. Creating a paused voice does not start the device; unpausing it later does.
When no unpaused voices remain, the output device follows the configured idle timeout:
setAudioDeviceIdleTimeout(Duration.zero)stops it as soon as possible.- A positive duration keeps it running for that grace period (default is 500 ms).
setAudioDeviceIdleTimeout(null)keeps it running indefinitely, including acrossdeinit()and a laterinit().
Android stops the audio output device when idle (when no voices are actively playing), releasing the audioserver AudioMix partial wakelock. If your app requires the output device to stay open indefinitely on Android, call SoLoud.instance.setAudioDeviceIdleTimeout(null).
startAudioDevice() temporarily starts or prewarms the output and completes after startup finishes. It does not enable permanent keep-alive; if the engine is still idle, the configured timeout begins again. Loaded sounds, active voices, and filter states are all preserved across device stop and start, so playback resumes seamlessly.
stopAudioDevice() is an idle-only conditional stop by default, so it succeeds without interrupting active playback. Use stopAudioDevice(force: true) only when the output must be stopped while voices remain active; their voice state is not changed.
getAudioDeviceState() is a cheap synchronous read of the actual current backend state as an AudioDeviceState enum: uninitialized, stopped, started, starting, or stopping. It is safe to call even before init().
Use listPlaybackDevices() to enumerate the OS playback devices and changeDevice() to switch the output to one of them (or back to the system default when called without an argument):
final devices = SoLoud.instance.listPlaybackDevices();
await SoLoud.instance.changeDevice(newDevice: devices[1]);changeDevice() returns a Future because device enumeration and replacement can block; it runs off the UI isolate, so it won't freeze the app (or trigger an ANR on Android). Await it to know when the swap has finished. The replacement device is only started when the old one was running or the idle policy requires it, so a device stopped via stopAudioDevice() or the idle timeout stays stopped across the swap.
On the web only the default output device is returned.
play(), play3d(), setPause(), pauseSwitch(), speechText(), playClocked(), play3dClocked() and playScheduled() request the output-device start on a background scheduler instead of blocking for it. This keeps them synchronous and non-blocking, but it also means they cannot throw when that start fails โ they return before it has been attempted. SoLoudAudioDeviceFailedToStartCppException is therefore no longer thrown by these methods.
Instead, subscribe to SoLoud.instance.audioDeviceStartFailures to be notified when an automatic start fails (after the backend has already rebuilt the device and retried). Voice state is untouched, so recovery is usually just an explicit start:
SoLoud.instance.audioDeviceStartFailures.listen((_) async {
try {
await SoLoud.instance.startAudioDevice();
} on SoLoudAudioDeviceFailedToStartCppException {
// Still unavailable: tell the user, or back off and retry later.
}
});startAudioDevice() and changeDevice() are the exceptions: they await their device operation and report failures directly to their caller, so they do not emit on this stream.
iOS and Google Ads
If you are using the google_mobile_ads plugin on iOS, you might encounter issues with audio playback. To solve this, you can configure the audio_session adding this line in AppDelegate.swift:
import GoogleMobileAds
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Add this
MobileAds.shared.audioVideoManager.isAudioSessionApplicationManaged = true
...
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}