Don of the Day

Don of the Day


Adventures in software development with Xamarin and the Web

Software developer, building things with code in sunny Scottsdale, AZ.

Share


Twitter


AVAudioSession and Ducking

Don FitzsimmonsDon Fitzsimmons

I often rely on the kindness of other developers to write helpful articles on the use of various APIs or frameworks. You know, Google something like "AVAudioSession ducking not working". Most often, you come to a series of Stack Overflow posts, which is great, but the holy grail of Google searching for a very specific problem is a blog post that addresses it directly.

And so, while trying to figure out an issue with the iOS audio API, I came across several unanswered Stackoverflow posts and a few blog posts that didn't quite address the issue I was having. I was on my own. I did figure out the issue and now I'm sharing my solution with others in case some poor soul ends up in the same spot.

The Problem

In iOS, your app has some requirement to play voice prompts over background audio. So, iTunes is playing music and your app needs to call out certain stages of a workout. You need the background audio to "duck" to a lower volume while your app's audio plays a voice for a few seconds, then the background audio should "duck" back up to it's previous level.

It turns out that Apple provides this capability and sure enough, it's an option on the AVAudioSession called .DuckOthers. What Apple doesn't say much about is how to use it. I quickly learned that in order to duck your background audio while your app's audio is playing, the only way to un-duck the audio is to set your AVAudioSession to the inactive state immediately after your audio is done playing, otherwise your background audio will stay ducked at that lower level once your app's audio is done.

Okay, so I wrote two helper methods in my app to handle activating and deactivating the AVAudioSession before and after my AVAudioPlayer plays the sounds. That code looked like this (this code is C# because I'm using Xamarin, but you can translate this to Swift or Objective-C pretty easily):

private void ActivateAudioSession()
 {
     var session = AVAudioSession.SharedInstance();
     session.SetCategory(AVAudioSessionCategory.Playback,AVAudioSessionCategoryOptions.DuckOthers);
     session.SetActive(true);
 }

 private void DeactivateAudioSession()
 {
     var session = AVAudioSession.SharedInstance();
     session.SetActive(false);
 }

This looked like a good solution and it seemed to work. But there's a catch.

The Other Problem

So my two methods above worked great at first. Before creating my AVAudioPlayer and playing my sound file, I would call ActivateAudioSession and on the player's complete event, I would call DeactivateAudioSession. But once I tested this, I found that every call to DeactivateAudioSession would case the UI on my app to stutter and drop frames. It was really strange. Then I realized that for whatever reason, setting the AVAudioSession.SetActive(false) takes .5 seconds. That may not sound like much, but when your app is a timer and the UI is blocked for .5 seconds, it causes all kinds of mahem.

So the problem is that the deactivating an AVAudioSession while background music is playing, will block your UI for .5 seconds. The remedy to this is to call AVAudioSession.SetActive(false) on a separate thread so you don't block the UI thread. My modified DeactivateAudioSession look like this:

private void DeactivateAudioSession()
 {
  new System.Threading.Thread(new System.Threading.ThreadStart(() =>
  {
      var session = AVAudioSession.SharedInstance();
      session.SetActive(false);
  })).Start();
}

Using Objective-C or Swift, you would just create a new NSThread (I think) instead of the System.Threading.Thread in C#. Once I deactivated the AVAudioSession on a separate thread, my UI remained responsive and no longer dropped frames. All was right with the world.

I hope this helps someone desperately Google searching for an answer to this problem.

Software developer, building things with code in sunny Scottsdale, AZ.

Comments