iOS - AVAudioPlayerNode.play() 执行速度很慢

标签 ios avaudioengine avaudioplayernode

我在 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() 似乎可能会阻塞直到下一个音频周期开始或类似的事情。

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

这让我的 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 ,因此遇到了相同的问题,和/或有其他缺点或自身的限制。当然,也有可用的商业选项,这可能为一些开发人员提供解决方案。

关于iOS - AVAudioPlayerNode.play() 执行速度很慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58157605/

相关文章:

ios - 如何制作这个网格布局

带音量包络的 iOS 音频采样器

ios - Swift:如何在中断时停止调用 scheduleBuffer 完成处理程序?

ios11 - AVAudioPlayerNode 调度缓冲区和 iOS11 中的音频路由更改

ios - NSFetchedResultsController 一个实体用于部分,另一个实体用于行

ios - 当我按下按钮时打印用户坐标 - Swift

ios - 避免在从 `UITableView` 进行后台更新时跳入 `NSFetchedResultsController`

swift - 带有 AVAudioConverterInputBlock 的 AVAudioConverter 在处理后会出现断断续续的音频

ios - 将单声道音频数据传递给 AVAudioEnvironmentNode

ios - 从特定时间开始将 AVAudioFile 读入缓冲区