应用程序被电话打断后无法播放 iOS



我的SpriteKit游戏中遇到了一个问题,使用playSoundFileNamed(_ soundFile:, waitForCompletion:)的音频在应用程序被电话打断后将无法播放。(我也在我的应用程序中使用SKAudioNodes,这些节点不受影响,但我真的非常希望能够使用SKAction playSoundFileNameed。

这是来自精简的SpriteKit游戏模板的gameScene.swift文件,该文件重现了该问题。您只需要将音频文件添加到项目中并将其称为"note">

我已将应驻留在 appDelegate 中的代码附加到切换开/关按钮以模拟电话呼叫中断。该代码 1) 停止音频引擎然后停用 AVAudioSession - (通常在应用程序中WillResignActive) ...2)激活AVAudioSession然后启动音频引擎 - (通常在应用程序中DidBecomActive)

错误:

AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]:停用具有运行 I/O 的音频会话。在停用音频会话之前,应停止或暂停所有 I/O。

尝试停用音频会话时会发生这种情况,但仅在至少播放一次声音后。 要复制:

1) 运行应用 2) 关闭和打开发动机几次。不会发生任何错误。 3) 点击播放声音文件命名按钮 1 次或更多次以播放声音。 4)等待声音停止 5)再等一会儿确定

6) 点击切换音频引擎按钮以停止音频引擎并停用会话 - 发生错误。

7) 打开引擎几次,以查看会话已激活、会话已停用、会话激活打印在调试区域中 - 即未报告任何错误。 8)现在,在会话处于活动状态并且引擎运行时,播放声音文件命名按钮将不再播放声音。

我做错了什么?

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

让我在这里为您节省一些时间:playSoundFileNamed在理论上听起来很棒,以至于您可能会说在花费4年时间开发的应用程序中使用它,直到有一天您意识到它不仅在中断时完全崩溃,而且甚至在最关键的中断中使您的应用程序崩溃,您的IAP。别这样。我仍然不完全确定SKAudioNode还是AVPlayer是答案,但这可能取决于您的用例。只是不要这样做。

如果您需要科学证据,请创建一个应用程序并创建一个 for 循环,该循环可以播放 SoundFileNamed 任何您想要的触摸开始,并查看您的内存使用情况会发生什么。该方法是一块漏水的垃圾。

为我们的最终解决方案编辑:

我们发现使用 prepareToPlay() 在内存中预加载适当数量的 AVAudioPlayer 实例是最好的方法。SwiftySound音频类使用动态生成器,但是动态制作AVAudioPlayer会导致动画速度变慢。我们发现拥有最大数量的 AVAudioPlayer 并检查数组中 isPlay == false 是最简单和最好的;如果没有一个,你就不会得到声音,类似于你可能看到的PSFN,如果你让它在彼此之上播放很多声音。总的来说,我们还没有找到理想的解决方案,但这对我们来说很接近。

为了回应Mike Pandolfini不使用playSoundFileName的建议,我已经将我的代码转换为仅使用SKAudioNodes。 (并将错误报告发送给苹果)。 然后我发现其中一些 SKAudioNode 在应用程序中断后也无法播放......我偶然发现了一个修复程序。 你需要告诉每个SKAudioNode在应用程序退出或从后台返回时停止() - 即使他们没有播放。

(我现在没有在我的第一篇文章中使用任何停止音频引擎并停用会话的代码)

然后,问题就变成了如何在可能播放自己的地方快速播放相同的声音。这就是playSoundFileNamed的优点。

1) SKAudioNode修复:

预加载您的 SKAudio 节点,即

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

在didMoveToView中添加它们

sound.autoplayLooped = false
addChild(sound)

添加将辞职活动通知

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

然后创建选择器的函数,停止所有音频节点播放:

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

所有SKAudio节点现在都可以在应用程序中断后可靠地播放。

2) 要复制 playSoundFileName 播放短的快速重复声音或较长的声音的能力,这些声音可能需要多次播放,因此可能会重叠,请为每个声音创建/预加载 1 个以上的属性,并像这样使用它们:

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

在 didMoveToView 中

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

创建播放函数

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

要播放声音,请将该特定声音的数组及其计数器传递给播放函数。

playFastSound(from: soundArray, with: &soundCounter)

最新更新