Audio won't play after app interrupted by phone call iOS

拥有回忆 提交于 2020-01-22 17:10:31

问题


I have a problem in my SpriteKit game where audio using playSoundFileNamed(_ soundFile:, waitForCompletion:) will not play after the app is interrupted by a phone call. (I also use SKAudioNodes in my app which aren't affected but I really really really want to be able to use the SKAction playSoundFileNamed as well.)

Here's the gameScene.swift file from a stripped down SpriteKit game template which reproduces the problem. You just need to add an audio file to the project and call it "note"

I've attached the code that should reside in appDelegate to a toggle on/off button to simulate the phone call interruption. That code 1) Stops AudioEngine then deactivates AVAudioSession - (normally in applicationWillResignActive) ... and 2) Activates AVAudioSession then Starts AudioEngine - (normally in applicationDidBecomeActive)

The error:

AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.

This occurs when attempting to deactivate the audio session but only after a sound has been played at least once. to reproduce:

1) Run the app 2) toggle the engine off and on a few times. No error will occur. 3) Tap the playSoundFileNamed button 1 or more times to play the sound. 4) Wait for sound to stop 5) Wait some more to be sure

6) Tap Toggle Audio Engine button to stop the audioEngine and deactivate session - the error occurs.

7) Toggle the engine on and of a few times to see session activated, session deactivated, session activated printed in debug area - i.e. no errors reported. 8) Now with session active and engine running, playSoundFileNamed button will not play the sound anymore.

What am I doing wrong?

import SpriteKit
import AVFoundation

class GameScene: SKScene {
var toggleAudioButton: SKLabelNode?
var playSoundFileButton: SKLabelNode?
var engineIsRunning = true

override func didMove(to view: SKView) {
toggleAudioButton = SKLabelNode(text: "toggle Audio Engine")
toggleAudioButton?.position = CGPoint(x:20, y:100)
toggleAudioButton?.name = "toggleAudioEngine"
toggleAudioButton?.fontSize = 80
addChild(toggleAudioButton!)

playSoundFileButton = SKLabelNode(text: "playSoundFileNamed")
playSoundFileButton?.position = CGPoint(x: (toggleAudioButton?.frame.midX)!, y:    (toggleAudioButton?.frame.midY)!-240)
playSoundFileButton?.name = "playSoundFileNamed"
playSoundFileButton?.fontSize = 80
addChild(playSoundFileButton!)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
  let  location = touch.location(in: self)
  let  nodes = self.nodes(at: location)

  for spriteNode in nodes {
    if spriteNode.name == "toggleAudioEngine" {
      if engineIsRunning { // 1 stop engine, 2 deactivate session

        scene?.audioEngine.stop() // 1
        toggleAudioButton!.text = "engine is paused"
        engineIsRunning = !engineIsRunning
        do{
          // this is the line that fails when hit anytime after the playSoundFileButton has played a sound
          try AVAudioSession.sharedInstance().setActive(false) // 2
          print("session deactivated")
        }
        catch{
          print("DEACTIVATE SESSION FAILED")
        }
      }
      else { // 1 activate session/ 2 start engine
        do{
          try AVAudioSession.sharedInstance().setActive(true) // 1
          print("session activated")
        }
        catch{
          print("couldn't setActive = true")
        }
        do {
          try scene?.audioEngine.start() // 2
          toggleAudioButton!.text = "engine is running"
          engineIsRunning = !engineIsRunning
        }
        catch {
          //
        }
      }
    }

    if spriteNode.name == "playSoundFileNamed" {
      self.run(SKAction.playSoundFileNamed("note", waitForCompletion: false))
    }
   }
  }
 }
}

回答1:


Let me save you some time here: playSoundFileNamed sounds wonderful in theory, so wonderful that you might say use it in an app you spent 4 years developing until one day you realize it’s not just totally broken on interruptions but will even crash your app in the most critical of interruptions, your IAP. Don’t do it. I’m still not entirely sure whether SKAudioNode or AVPlayer is the answer, but it may depend on your use case. Just don’t do it.

If you need scientific evidence, create an app and create a for loop that playSoundFileNamed whatever you want in touchesBegan, and see what happens to your memory usage. The method is a leaky piece of garbage.

EDITED FOR OUR FINAL SOLUTION:

We found having a proper number of preloaded instances of AVAudioPlayer in memory with prepareToPlay() was the best method. The SwiftySound audio class uses an on-the-fly generator, but making AVAudioPlayers on the fly created slowdown in animation. We found having a max number of AVAudioPlayers and checking an array for those where isPlaying == false was simplest and best; if one isn't available you don't get sound, similar to what you likely saw with PSFN if you had it playing lots of sounds on top of each other. Overall, we have not found an ideal solution, but this was close for us.




回答2:


In response to Mike Pandolfini’s advice not to use playSoundFileNamed I’ve converted my code to only use SKAudioNodes. (and sent the bug report to apple). I then found that some of these SKAudioNodes don’t play after app interruption either … and I’ve stumbled across a fix. You need to tell each SKAudioNode to stop() as the app resigns to, or returns from the background - even if they’re not playing.

(I'm now not using any of the code in my first post which stops the audio engine and deactivates the session)

The problem then became how to play the same sound rapidly where it possibly plays over itself. That was what was so good about playSoundFileNamed.

1) The SKAudioNode fix:

Preload your SKAudioNodes i.e.

let sound = SKAudioNode(fileNamed: "super-20")

In didMoveToView add them

sound.autoplayLooped = false
addChild(sound)

Add a willResignActive notification

notificationCenter.addObserver(self, selector:#selector(willResignActive), name:UIApplication.willResignActiveNotification, object: nil)

Then create the selector’s function which stops all audioNodes playing:

@objc func willResignActive() {
  for node in self.children {
    if NSStringFromClass(type(of: node)) == “SKAudioNode" {
      node.run(SKAction.stop())
    }
  }
}

All SKAudioNodes now play reliably after app interrupt.

2) To replicate playSoundFileNamed’s ability to play the short rapid repeating sounds or longer sounds that may need to play more than once and therefore could overlap, create/preload more than 1 property for each sound and use them like this:

let sound1 = SKAudioNode(fileNamed: "super-20")
let sound2 = SKAudioNode(fileNamed: "super-20")
let sound3 = SKAudioNode(fileNamed: "super-20")
let sound4 = SKAudioNode(fileNamed: "super-20")

var soundArray: [SKAudioNode] = []
var soundCounter: Int = 0

in didMoveToView

soundArray = [sound1, sound2, sound3, sound4]
for sound in soundArray {
  sound.autoplayLooped = false
  addChild(sound)
}

Create a play function

func playFastSound(from array:[SKAudioNode], with counter:inout Int) {
counter += 1
if counter > array.count-1 {
  counter = 0
}
  array[counter].run(SKAction.play())
}

To play a sound pass that particular sound's array and its counter to the play function.

playFastSound(from: soundArray, with: &soundCounter)


来源:https://stackoverflow.com/questions/56466959/audio-wont-play-after-app-interrupted-by-phone-call-ios

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!