iOS - AVAudioPlayerNode.play() 执行非常慢



我在iOS游戏应用程序中使用AVAudioEngine进行音频处理。我遇到的一个问题是AVAudioPlayerNode.play()需要很长时间才能执行,这在游戏等实时应用程序中可能是一个问题。

play() 只是激活播放器节点 - 您不必每次播放声音时都调用它。因此,不必经常调用它,但必须偶尔调用它,例如最初激活播放器,或者在停用播放器后(在某些情况下会发生)。即使只是偶尔调用,较长的执行时间也可能是一个问题,特别是当您需要一次在多个播放器上调用 play() 时。

play() 的执行时间似乎与 AVAudioSession.ioBufferDuration 的值成正比,您可以使用 AVAudioSession.setPreferredIOBufferDuration() 请求更改该值。以下是我用来测试它的一些代码:

import AVFoundation
import UIKit
class ViewController: UIViewController {
private let engine = AVAudioEngine()
private let player = AVAudioPlayerNode()
private let ioBufferSize = 1024.0 // Or 256.0
override func viewDidLoad() {
super.viewDidLoad()
let audioSession = AVAudioSession.sharedInstance()
try! audioSession.setPreferredIOBufferDuration(ioBufferSize / 44100.0)
try! audioSession.setActive(true)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: nil)
try! engine.start()
print("IO buffer duration: (audioSession.ioBufferDuration)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if player.isPlaying {
player.stop()
} else {
let startTime = CACurrentMediaTime()
player.play()
let endTime = CACurrentMediaTime()
print("(endTime - startTime)")
}
}
}

以下是我使用 1024 缓冲区大小(我相信这是默认值)获得的 play() 的一些示例计时:

0.0218
0.0147
0.0211
0.0160
0.0184
0.0194
0.0129
0.0160

下面是使用缓冲区大小 256 的一些示例计时:

0.0014
0.0029
0.0033
0.0023
0.0030
0.0039
0.0031
0.0032

如上所示,对于 1024 的缓冲区大小,执行时间往往在 15-20 毫秒范围内(大约以 60 FPS 的速度进行整帧)。缓冲区大小为 256 时,它大约是 3 毫秒 - 没有那么糟糕,但当你每帧只有 ~17 毫秒可以使用时,仍然很昂贵。

这是在运行iOS 2的iPad Mini 12.4.2上。这显然是一个旧设备,但我在模拟器上看到的结果似乎同样成比例,因此它似乎更多地与缓冲区大小和函数本身的行为有关,而不是与所使用的硬件有关。我不知道引擎盖下发生了什么,但似乎 play() 可能会阻止直到下一个音频周期开始,或者类似的东西。

请求较小的缓冲区大小似乎是部分解决方案,但存在一些潜在的缺点。根据此处的文档,较低的缓冲区大小可能意味着从文件流式传输时更多的磁盘访问,无论如何,请求可能根本不会得到支持。此外,在这里,有人报告了与低缓冲区大小相关的播放问题。考虑到所有这些,我不愿意将其作为解决方案。

这让我的 play() 执行时间在 15-20 毫秒范围内,这通常意味着 60 FPS 的帧丢失。如果我安排事情,以便一次只调用 play(),并且很少,也许它不会引起注意,但这并不理想。

我已经搜索了信息并在其他地方询问了这个问题,但似乎在实践中没有多少人遇到这种行为,或者这对他们来说不是问题。

AVAudioEngine 旨在用于实时应用程序,因此,如果我是对的,AVAudioPlayerNode.play() 阻塞了与缓冲区大小成比例的大量时间,这似乎是一个设计问题。我意识到这可能不是许多人正在处理的问题,但我在这里发帖是为了询问是否有人在AVAudioEngine上遇到过这个特定问题,如果是这样,是否有任何见解,建议或解决方法任何人都可以提供。

我已经相当彻底地调查了这一点。以下是我的发现。

现在已经在各种设备和iOS版本(包括撰写本文时的最新版本13.2)上测试了该行为,并且还有其他人对其进行了测试,我目前的结论是AVAudioPlayerNode.play()的长执行时间是固有的,并且没有明显的解决方法。正如我在原始帖子中指出的,可以通过请求较低的缓冲区持续时间来减少执行时间,但如前所述,这似乎不是一个可行的解决方案。

我从一个可靠的消息来源听说,在后台线程上调用play()(例如使用Grand Central Dispatch)应该是安全的,事实上,这将是解决问题的一种方法。但是,尽管从技术上讲,在不同的线程上调用play()(或其他与AVAudioEngine相关的函数)可能是安全的,但我怀疑这是否是一个好主意(下面进一步解释)。

据我所知,文档没有说明这一点,但AVAudioEngine会在各种情况下抛出NSException,如果没有特殊处理,将导致 Swift 中的应用程序终止。

导致抛出NSException的事情之一是,如果您在引擎未运行时调用AVAudioPlayerNode.play()。显然,如果您只需要担心自己的代码,则可以采取措施确保不会发生这种情况。

但是,iOS 本身有时会自行停止引擎,例如在发生音频中断时。如果您在此之后和重新启动引擎之前调用play(),则会抛出NSException。如果对play()的所有调用都在主线程上,则避免此错误相当容易,但是多线程使问题复杂化,并且似乎可能会引入引擎停止后意外调用play()的风险。尽管可能有办法解决这个问题,但多线程似乎引入了不希望的复杂性和脆弱性,所以我选择不追求它。

我目前的策略如下。由于前面讨论的原因,我没有使用多线程。相反,我正在尽我所能减少对play()的调用次数,无论是整体还是每帧。这包括,除其他外,仅支持立体声音频(由于各种原因,同时支持单声道和立体声会导致更多调用play(),这是不可取的)。

最后,我还调查了AVAudioEngine的替代品。OpenAL 在 iOS 上仍受支持,但已弃用。使用低级 API(如音频队列服务或音频单元)的自定义实现是可能的,但并非易事。我也看过一些开源解决方案,但我看过的选项本身AVAudioEngine使用的,因此会遇到同样的问题,和/或有自己的其他缺点或局限性。当然,也有可用的商业选项,这可能会为某些开发人员提供解决方案。

最新更新