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

3
我在一个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 12.4.2的iPad Mini 2上。这显然是一台旧设备,但我在模拟器上看到的结果似乎成比例,因此似乎更多地与缓冲区大小和函数本身的行为有关,而不是使用的硬件。我不知道底层发生了什么,但play()似乎可能阻塞直到下一个音频周期的开始,或者类似于那样的东西。
要求较低的缓冲区大小似乎是部分解决方案,但也存在一些潜在的缺点。根据此处的文档,较低的缓冲区大小可能意味着从文件流式传输时需要更多的磁盘访问,并且无论如何,请求可能根本不会得到满足。此外,此处,有人报告了与低缓冲区大小相关的播放问题。考虑到所有这些,我不愿意将其作为解决方案。
这使得play()的执行时间在15-20毫秒范围内,通常意味着在60 FPS下错过一帧。如果我安排事情,只有一个调用play()是在一次中进行,并且仅在偶尔发生,也许它不会被注意到,但这并不理想。
我已经搜索了信息并在其他地方询问了这个问题,但似乎没有多少人在实践中遇到这种行为,或者对他们来说这不是问题。
AVAudioEngine旨在用于实时应用程序,因此如果我正确,AVAudioPlayerNode.play()在相对于缓冲区大小的显着时间内阻塞,这似乎是一个设计问题。我意识到这可能不是很多人正在处理的问题,但我在这里发布是要问是否有人在AVAudioEngine中遇到了这个特定问题,如果有,是否有任何见解,建议或解决方法可以提供。

2
不是答案,但我也发现AVAudioPlayerNode.play()执行起来相当慢。我想知道你是否找到了任何解决方法? 作为Web平台的开发者,我很惊讶这样的本地函数实际上执行起来更慢。 - cocoggu
2
由于尚未收到任何回答,我很快就会发布一个答案,总结我的发现。 - scg
我在 macOS 上也遇到了同样的问题。 - undefined
1个回答

6

我已经彻底调查了这个问题,以下是我的发现。

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

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

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

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

但是,在某些情况下,iOS本身会停止引擎,例如当音频中断发生时。如果您在重新启动引擎之前后继续调用play(),则将抛出NSException异常。如果您所有对play()的调用都在主线程上,那么避免此错误就相当容易,但是多线程会使问题变得复杂,并且可能会在引擎停止后意外地调用play()。虽然可能有解决方法来解决这个问题,但是多线程似乎会引入不必要的复杂性和脆弱性,因此我选择不使用它。

我的当前策略如下。由于前面所述的原因,我不使用多线程。相反,我正在尽一切努力减少对play()的调用次数,包括总体和每帧的调用次数。这包括仅支持立体声音频(出于各种原因,支持单声道和立体声可能会导致对play()的更多调用,这是不希望看到的)。

最后,我也调查了替代方案AVAudioEngine。 OpenAL在iOS上仍然受支持,但已被弃用。使用低级别API(如音频队列服务或音频单元)的自定义实现可能是一种选择,但并不容易。我还研究了一些开源解决方案,但我看过的选项都在底层使用AVAudioEngine本身,因此具有相同的问题,和/或具有其他自身的缺点或限制。当然也有商业选项可用,这可能为一些开发人员提供解决方案。

2
非常感谢您抽出时间回答! - cocoggu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接