Keep audio-play while sleeping - ios

I have an iOS app using AVFoundation and playing audio contents.
Once launched I want it to keep playing, even in the background, until I stop it.
But now as soon as the device goes to sleep or I put the app in the background, it stops playing. How can I fix this and get the behavior I wish.
Here is the kind of code I am using in the app:
var soundSession:AVAudioSession!, audioPlayer: AVAudioPlayer?
.....
soundSession = AVAudioSession.sharedInstance()
.....
do {try soundSession.setCategory(AVAudioSession.Category.playback)
try soundSession.setActive(true)
if audioPlayer == nil {
audioPlayer = try AVAudioPlayer(contentsOf: url,
fileTypeHint: fileTypHint)
guard let _ = audioPlayer else {return}
audioPlayer!.delegate = self
}
audioPlayer?.play()
} catch {
print("Error in \(#function)\n" + error.localizedDescription)
}

Related

Audio glitching on Swift loop

I'm working on an iOS app that has ambient music playing in the background in a -1 (infinite) loop. However, if the user clicks a button on the layout, the audio glitches randomly. Sometimes it doesn't at all.
Here is my code:
if let bundle = Bundle.main.path(forResource: "ambient_audio", ofType: "mp3") {
let backgroundMusic = NSURL(fileURLWithPath: bundle)
do {
audioPlayer = try AVAudioPlayer(contentsOf:backgroundMusic as URL)
guard let audioPlayer = audioPlayer else { return }
audioPlayer.numberOfLoops = -1 // for infinite times
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
}
It is a relatively small issue, sometimes it doesn't occur at all. But when it does, it is annoying.
I have tried many solutions, but none have worked (E.G. background threads).

How to play custom sound while app is in background AVAudioSession?

My iOS app receives notification to refresh its state from our main service. Right now, we are fetching the latest state and we play a continues sound when there is updated data. Our devices are locked in guided mode to stop them from turning off.
We are making changes such that the device can go to sleep after xx minutes of inactivity. However, we are noticing that the sound doesn't play when the app is in background and it always seems to fail on AVAudioSession.sharedInstance().setActive(true). Interestingly, if put breakpoints and run it through the debugger, it works fine but when running normally, it fails with this error:
Error Domain=NSOSStatusErrorDomain Code=561015905
We have the "Audio, AirPlay, and PnP" enabled under background modes under capabilities. Here is the code for playing the sound:
func playSound(shouldPlay: Bool) {
guard let url = Bundle.main.url(forResource: "sms_alert_circles", withExtension: "caf") else { return }
let audioSession = AVAudioSession.sharedInstance()
do {
try self.audioSession.setCategory(.playback, mode: .default, options: .mixWithOthers)
try self.audioSession.setActive(true)
/* The following line is required for the player to work on iOS 11. Change the file type accordingly*/
self.player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.caf.rawValue)
guard let player = self.player else { return }
if shouldPlay == true {
player.volume = 1.0
player.numberOfLoops = 1
player.play()
} else {
player.stop()
try self.audioSession.setActive(false)
}
} catch let error {
print("Error playing sounds")
print(error.localizedDescription)
}
}
I am hoping someone can point out the issue.

AVAudioPlayer not responding in the background

i'm playing audio in the background when my app receive voip notification.
But AVAudioPlayer acting weird and sometimes play the audio and sometimes not without any error.
thats how i play:
func playAudio() {
// Set the sound file name & extension
let alertSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("testAudio", ofType: "wav")!)
// Play the sound
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: alertSound)
audioPlayer.delegate = self
if (audioPlayer.prepareToPlay()) {
audioPlayer.play()
}
else {NSLog("there is error")}
} catch {
NSLog("there is \(error)")
}
}
I initialize AVAudioSession in my AppDelegate application function:
do
{
audioPlayer = AVAudioPlayer()
audioSession = AVAudioSession()
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, withOptions: [AVAudioSessionCategoryOptions.MixWithOthers])
try audioSession.setActive(true)
try audioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.Speaker)
}
catch {
NSLog("there is \(error)")
}
and of course var audioPlayer: AVAudioPlayer! as a class verb
Its working for the first minute or two and then AVAudioPlayer stop responding without any error.
I even try to use AVAudioPlayerDelegate to receive events, but nothing happen.
can someone explain this behavior? is it a bug in ios? or i'm doing somthing wrong?
tested on Ios 9.2, iphone 6 device, xcode 7.2.
Thanks.

Background Music Restarting

I am programming a game which switches between SKScenes. When I start playing the background music from the start screen, switch to a new screen and back to the start screen the music starts playing again.
I can't find out how to run the music once and not restarting again and again. I could make a workaround with storing a bool in NSUserDefaults to check if the music is running but that seems like an awful workaround. There must be a more simple solution. I have attempted with static but that doesn't work.
Create a new empty swift file and add this code into it:
var backgroundMusicPlayer: AVAudioPlayer!
var musicOnPlayScene = Bool()
func playBackgroundMusic(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
print("Could not find file: \(filename)")
return
}
var error: NSError? = nil
do {
backgroundMusicPlayer = try AVAudioPlayer(contentsOfURL: url!)
} catch let error1 as NSError {
error = error1
backgroundMusicPlayer = nil
}
if backgroundMusicPlayer == nil {
print("Could not create audio player: \(error!)")
return
}
backgroundMusicPlayer.numberOfLoops = -1
backgroundMusicPlayer.prepareToPlay()
backgroundMusicPlayer.play()
}
Now play your background music into start scene this way:
playBackgroundMusic("yourSong.mp3")
and when you switch a scene add this code into next scene's didMoveToView:
backgroundMusicPlayer.play()
This will resume your background music when you switch any scene.
Hope it will help.

How can I allow background music to continue playing while my app still plays its sounds while using Swift

I created an app and I am attempting to allow the user to continue to listen to their music while playing my game, but whenever they hit "play" and the ingame sounds occur it will stop the background music. I am developing on iOS using Swift. Here is a piece of the code that initiates the ingame sounds.
func playSpawnedDot() {
var alertSound: NSURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("spawnDot", ofType: "mp3")!)!
var error:NSError?
audioPlayer = AVAudioPlayer(contentsOfURL: alertSound, error: &error)
audioPlayer.prepareToPlay()
if volumeBool {
audioPlayer.play()
}
}
You need to set the AVAudioSession category, with one of the following value: https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVAudioSession_ClassReference/index.html (AVAudioSession Class Reference).
The default value is set to AVAudioSessionCategorySoloAmbient. As you can read :
[...] using this category implies that your app’s audio is nonmixable—activating your session will interrupt any other audio sessions which are also nonmixable. To allow mixing, use the AVAudioSessionCategoryAmbient category instead.
You have to change the category, before you play your sound. To do so :
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient, error: nil)
AVAudioSession.sharedInstance().setActive(true, error: nil)
You don't need to call those line each time you play the sound. You might want to do it only once.
Swift 4 version:
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient)
try? AVAudioSession.sharedInstance().setActive(true)
This is how I do it in Swift 3.0
var songPlayer : AVAudioPlayer?
func SetUpSound() {
if let path = Bundle.main.path(forResource: "TestSound", ofType: "wav") {
let filePath = NSURL(fileURLWithPath:path)
songPlayer = try! AVAudioPlayer.init(contentsOf: filePath as URL)
songPlayer?.numberOfLoops = -1 //logic for infinite loop
songPlayer?.prepareToPlay()
songPlayer?.play()
}
let audioSession = AVAudioSession.sharedInstance()
try!audioSession.setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.duckOthers) //Causes audio from other sessions to be ducked (reduced in volume) while audio from this session plays
}
You can see more of AVAudioSessionCategoryOptions here: https://developer.apple.com/reference/avfoundation/avaudiosessioncategoryoptions
Here's what I am using for Swift 2.0:
let sess = AVAudioSession.sharedInstance()
if sess.otherAudioPlaying {
_ = try? sess.setCategory(AVAudioSessionCategoryAmbient, withOptions: .DuckOthers)
_ = try? sess.setActive(true, withOptions: [])
}
Please note that you can replace .DuckOthers with [] if you don't want to lower background music and instead play on top to it.
Since they can't seem to make up their minds from version to version. Here it is in Swift 5.0
do{
try AVAudioSession.sharedInstance().setCategory(.ambient)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch {
NSLog(error.localizedDescription)
}
lchamp's solution worked perfectly for me, adapted for Objective-C:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
**
Updated for Swift 3.0
**
The name of the sound I am playing is shatter.wav
func shatterSound() {
if let soundURL = Bundle.main.url(forResource: "shatter", withExtension: "wav") {
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &mySound)
AudioServicesPlaySystemSound(mySound);
}
}
Then where ever you want to play the sound call
shatterSound()
If you want to play an alert sound:
public func playSound(withFileName fileName: String) {
if let soundUrl = Bundle.main.url(forResource: fileName, withExtension: "wav") {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient, with:[.duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
var soundId: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundId)
AudioServicesAddSystemSoundCompletion(soundId, nil, nil, { (soundId, clientData) -> Void in
AudioServicesDisposeSystemSoundID(soundId)
do {
// This is to unduck others, make other playing sounds go back up in volume
try AVAudioSession.sharedInstance().setActive(false)
} catch {
DDLogWarn("Failed to set AVAudioSession to inactive. error=\(error)")
}
}, nil)
AudioServicesPlaySystemSound(soundId)
} catch {
DDLogWarn("Failed to create audio player. soundUrl=\(soundUrl) error=\(error)")
}
} else {
DDLogWarn("Sound file not found in app bundle. fileName=\(fileName)")
}
}
And if you want to play music:
import AVFoundation
var audioPlayer:AVAudioPlayer?
public func playSound(withFileName fileName: String) {
if let soundUrl = Bundle.main.url(forResource: fileName, withExtension: "wav") {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient, with:[.duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
let player = try AVAudioPlayer(contentsOf: soundUrl)
player.delegate = self
player.prepareToPlay()
DDLogInfo("Playing sound. soundUrl=\(soundUrl)")
player.play()
// ensure the player does not get deleted while playing the sound
self.audioPlayer = player
} catch {
DDLogWarn("Failed to create audio player. soundUrl=\(soundUrl) error=\(error)")
}
} else {
DDLogWarn("Sound file not found in app bundle. fileName=\(fileName)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
self.audioPlayer?.stop()
do {
// This is to unduck others, make other playing sounds go back up in volume
try AVAudioSession.sharedInstance().setActive(false)
} catch {
DDLogWarn("Failed to set AVAudioSession inactive. error=\(error)")
}
}
For Swift (Objective-C like this too)
you can use this link for best answer and if you don't have any time for watching 10 minutes the best action is that you just copy below code in your AppDelegate in didFinishLaunchingWithOptions and then select your project's target then go to Capabilities and at last in Background modes check on Audio, AirPlay and Picture in Picture
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayback)
}
catch {
}
}
I didn't think that just setting the AVAudioSession to AVAudioSessionCategoryOptions.duckOthers would work, but it did. Here is my full code to help the rookies like myself.
Swift 4 - Full Example:
var audioPlayer = AVAudioPlayer()
func playSound(sound: String){
let path = Bundle.main.path(forResource: sound, ofType: nil)!
let url = URL(fileURLWithPath: path)
let audioSession = AVAudioSession.sharedInstance()
try!audioSession.setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.duckOthers)
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.play()
} catch {
print("couldn't load the file")
}
}
I still need to figure out setActive (I was looking at this blog post) but the audio stops ducking when I leave the app, so it works for my app.
Swift 5 Version based on lchamp`s answer.
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
try? AVAudioSession.sharedInstance().setActive(true)
This works perfectly for me on iOS 14.4 and Swift 5. It's a bit different answer from others by using the .duckOthers option (and you can also mix directly with sound if you'd like with .mixWithOthers), but it works perfectly while music plays. It will lower the volume of the music, play the "beep" sound, and then raise the music volume back up to normal. It also captures error data using Google Firebase Crashlytics if there is an issue, and tries to raise the volume to normal even on an error.
This code will also work perfectly on the first, and all other, plays of your sound without it stopping the music ever.
func playSound() {
do {
if let path = Bundle.main.path(forResource: "beep", ofType: "mp3") {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .duckOthers)
try AVAudioSession.sharedInstance().setActive(true)
let filePath = NSURL(fileURLWithPath:path)
songPlayer = try AVAudioPlayer.init(contentsOf: filePath as URL)
songPlayer?.numberOfLoops = 0
songPlayer?.prepareToPlay()
songPlayer?.play()
try AVAudioSession.sharedInstance().setActive(false)
}
} catch (let error) {
try? AVAudioSession.sharedInstance().setActive(false)
Crashlytics.crashlytics().setCustomValue(error.localizedDescription, forKey: "audio_playback_error")
}
}
An important thing the others answers do not have is that you need to call false with the .notifyOthers flag when deactivating:
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
The reason for this is that other apps playing music in the background will know when to turn their audio back on when you deactivate yours. Otherwise your background music won't turn back on if you turned off your session.

Resources