---
title: Audio Context
description: How to manage audio context and interruptions
---

`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](https://pub.dev/packages/audio_session).

This page provides a guide on how to use `audio_session` to manage the audio context.

<Info>
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.
</Info>

## Setup

First, add `audio_session` to your `pubspec.yaml`:

```yaml
dependencies:
  audio_session: ^0.2.2
```

## Basic Usage

Initialize 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.

```dart
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:

```dart
await session.setActive(true);
final handle = await SoLoud.instance.play(sound);
```

## Handling Interruptions

You need to listen to interruption events to pause, resume, or duck your audio.

```dart
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;
      }
    }
  });
}
```

## Output-Device Lifecycle

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 across `deinit()` and a later `init()`.

<Info>
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)`.
</Info>

`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()`.

### Changing the Output Device

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):

```dart
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.

<Info>
On the web only the default output device is returned.
</Info>

### Automatic Device Start Failures

`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:

```dart
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.

<Info>
  **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`:

  ```dart
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)
}
  ```
</Info>
