Igor Kulman

Correctly playing audio in iOS applications

· Igor Kulman

When you look for a way to play audio in your iOS application you usually find code like this

player = try AVAudioPlayer(contentsOf: url)

While this code works and will play the given audio file it does not deal with all the nuances of audio playback.

Imagine the user is playing a podcast or music, do you want the sound in your app to stop that playback? Or play over it?

The key to correct audio playback is understanding AVAudioSession categories. Let’s take it by example

Playing ambient sounds over existing audio

If you want to play an audio file in your application without affecting the existing music or podcast playback that might be going on you should use the .ambient category.

You use this category for example to play sound effects like the sound of a message being sent in a chat application where the sound is not that important and definitely not worth affecting other playback in iOS.

You set the category on the shared AVAudioSession instance

try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)

and make it active

try AVAudioSession.sharedInstance().setActive(true)

All the sounds you now play using AVAudioPlayer will be played as ambient sounds.

Stopping existing audio to play your sound

If the audio file you want to play in your application is important, for example playing a received voice message in a chat application, you can stop existing audio that is playing on iOS.

You can achieve this by setting the category to .playback

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)

Remember, setting a category makes it persist for all the consecutive audio playback until you set it to a different value.

When you play a sound now using AVAudioPlayer it will stop the music or podcast the user might be listening to.

But what if you want the users music or podcast to continue after you play your sound?

Resetting audio category and notifying other applications they can resume

It is a good practice to deactivate the AVAudioSession and send, notification to all the other applications when you are finished playing your sound

try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)

and reset the session back to the ambient category

try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)

Well behaved music or podcast applications should listen for that notification and resume playback when they receive it.

The problem is that not all the applications do it correctly, not even the ones from Apple, so the result will vary depending on the application used.

Ducking existing audio to play your audio

There is an more approach you can use that is something like a hybrid between the two approaches already mentioned. You can set the category to .playback with .duckOthers as an option.

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
try AVAudioSession.sharedInstance().setActive(true)

If you now play your audio file iOS will “duck” the existing audio playback, so making it less loud but not stopping it, and play your sound file over it.

This will make sure the music or podcast the user might be playing is not stopped but at the same time that the audio you play in your application is more or less clearly heard by the user.

Knowing when AVAudioPlayer finished playing

To reset the AVAudioSession back to the ambient category and notify other applications about finished playback you need to know when AVAudioPlayer finished playback of the sound file you gave it to play.

You can easily do this with the AVAudioPlayerDelegate, its delegate method audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) fill fire when audio playback is finished.

I would recommend to also do the same in the audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) delegate method that fires on error, because if you made changes to AVAudioSession before trying to play an audio fail it does not matter if it succeeded or not, you need to do a clean up anyway.

See also