应用程序被电话iOS中断后音频不会播放
Posted
技术标签:
【中文标题】应用程序被电话iOS中断后音频不会播放【英文标题】:Audio won't play after app interrupted by phone call iOS 【发布时间】:2019-06-05 19:39:58 【问题描述】:我的 SpriteKit 游戏中有一个问题,在应用被电话打断后,使用 playSoundFileNamed(_ soundFile:, waitForCompletion:) 的音频将无法播放。 (我还在我的应用程序中使用了 SKAudioNodes,它们不受影响,但我真的真的很想也能够使用 SKAction playSoundFileNamed。)
这里是一个简化的 SpriteKit 游戏模板中的 gameScene.swift 文件,它重现了该问题。你只需要在项目中添加一个音频文件并称之为“note”
我已将应驻留在 appDelegate 中的代码附加到切换开/关按钮以模拟电话中断。该代码 1)停止 AudioEngine 然后停用 AVAudiosession -(通常在 applicationWillResignActive 中)......和 2)激活 AVAudioSession 然后启动 AudioEngine -(通常在 applicationDidBecomeActive 中)
错误:
AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]:停用正在运行 I/O 的音频会话。在停用音频会话之前,应停止或暂停所有 I/O。
当尝试停用音频会话但仅在至少播放一次声音后才会出现这种情况。 复制:
1) 运行应用程序 2) 关闭和打开引擎几次。不会发生错误。 3) 点击 playSoundFileNamed 按钮 1 次或多次以播放声音。 4)等待声音停止 5) 再等一段时间再确定
6) 点击切换音频引擎按钮以停止音频引擎并停用会话 - 发生错误。
7) 切换引擎几次以查看会话激活、会话停用、会话激活打印在调试区域 - 即没有报告错误。 8) 现在会话处于活动状态并且引擎正在运行,playSoundFileNamed 按钮将不再播放声音。
我做错了什么?
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】:让我在这里为您节省一些时间:playSoundFileNamed 在理论上听起来很棒,您可能会说在您花了 4 年时间开发的应用程序中使用它,直到有一天您意识到它不仅会因中断而完全中断,甚至会使您的应用程序中最关键的中断,即您的 IAP。不要这样做。我仍然不完全确定 SKAudioNode 或 AVPlayer 是否是答案,但这可能取决于您的用例。只是不要这样做。
如果您需要科学证据,请创建一个应用并创建一个 for 循环,在 touchesBegan 中播放任何您想要的声音文件名称,然后查看您的内存使用情况。该方法是一个泄漏的垃圾。
为我们的最终解决方案编辑:
我们发现使用 prepareToPlay() 在内存中预加载适当数量的 AVAudioPlayer 实例是最好的方法。 SwiftySound 音频类使用动态生成器,但动态生成 AVAudioPlayers 会导致动画速度变慢。我们发现拥有最大数量的 AVAudioPlayers 并检查数组中 isPlaying == false 的那些是最简单和最好的;如果一个不可用,你就听不到声音,类似于你在 PSFN 上看到的那样,如果你让它在彼此之上播放很多声音。总的来说,我们还没有找到理想的解决方案,但这对我们来说已经很接近了。
【讨论】:
我正在 Catalina/iOS13 中进行测试,无法在 touchesBegan 内存泄漏中重现您的 playSoundFileNamed。那部分似乎已修复。 如果他们确实在 iOS13 中修复它会很棒,但考虑到他们必须让这个东西正常工作的时间量,我很怀疑。尝试一次播放数百次 SoundFileNamed,您会立即看到内存峰值并且永远不会返回。有趣的是,我们发现如果你给动作附加一个名字,它似乎会正确地释放声音,但你最终会在内存中创建许多声音实例。我们有许多重叠的短声音,并决定使用 AVAudioPlayer 作为解决方案——老派,但似乎相当有效。【参考方案2】:为了响应 Mike Pandolfini 不使用 playSoundFileNamed 的建议,我已将代码转换为仅使用 SKAudioNodes。 (并将错误报告发送给苹果)。 然后我发现其中一些 SKAudioNode 在应用程序中断后也无法播放……我偶然发现了一个修复程序。 当应用程序退出或从后台返回时,您需要告诉每个 SKAudioNode 停止() - 即使它们没有播放。
(我现在没有使用我的第一篇文章中的任何代码来停止音频引擎并停用会话)
然后问题变成了如何快速播放相同的声音,它可能会在其自身上播放。这就是 playSoundFileNamed 的优点。
1) SKAudioNode 修复:
预加载您的 SKAudioNodes,即
let sound = SKAudioNode(fileNamed: "super-20")
在 didMoveToView 中添加它们
sound.autoplayLooped = false
addChild(sound)
添加 willResignActive 通知
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())
现在所有 SKAudioNode 在应用中断后都能可靠播放。
2) 要复制 playSoundFileNamed 播放短而快速的重复声音或可能需要多次播放并因此可能重叠的较长声音的能力,请为每个声音创建/预加载多个属性并像这样使用它们:
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)
【讨论】:
我们尝试使用 SKAudioNodes,但发现了一些可能不适用于您的问题,但为了后代的缘故想强调一下:(1)您必须 stop() 和 play() 才能拥有这些工作正常,但是如果你在很多动作中玩这些,这似乎会造成减速——正如你所说,PSFN 在这方面要好得多; (2) 将这些放在飞行中也会造成减速,尤其是同时; (3)没有“isPlaying”,所以我不能使用一个实例来“在上面”播放某种声音;投射到 AVAudioNode 并使用 isPlaying 是不可靠的。 我们发现使用 prepareToPlay() 在内存中预加载适当数量的 AVAudioPlayer 实例是最好的方法。 SwiftySound 音频类使用动态生成器,但动态生成 AVAudioPlayers 会导致动画速度变慢。我们发现拥有最大数量的 AVAudioPlayers 并检查数组中 isPlaying == false 的那些是最简单和最好的;如果一个不可用,你就听不到声音,类似于你在 PSFN 上看到的那样,如果你让它在彼此之上播放很多声音。总的来说,我们还没有找到理想的解决方案,但这对我们来说已经很接近了。以上是关于应用程序被电话iOS中断后音频不会播放的主要内容,如果未能解决你的问题,请参考以下文章