4

我在 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 的这个特定问题,如果是,是否有任何见解、建议或解决方法,任何人都可以提供。

4

2 回答 2

5

我对此进行了相当彻底的调查。这是我的发现。

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

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

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

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

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

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

最后,我还研究了AVAudioEngine. iOS 上仍支持 OpenAL,但已弃用。使用音频队列服务或音频单元等低级 API 的自定义实现是可能的,但并非易事。我还查看了一些开源解决方案,但我查看的选项AVAudioEngine在引擎盖下使用,因此遇到相同的问题,和/或自身有其他缺点或限制。当然也有可用的商业选项,这可能会为一些开发人员提供解决方案。

于 2019-11-01T18:58:24.117 回答
2

我发现了以下内容:当您第一次播放一个AVAudioPlayerNode实例时,它似乎需要自行初始化。鉴于您自己的上述研究,我会冒险猜测这段时间用于分配与 相关的内存IOBufferDuration,这可以解释为什么选择较小的IOBufferDuration大小会导致明显较低的延迟。根据我自己的经验,当我的游戏开始时,当声音被 -ed 时会有视觉抖动.play(),但是在游戏循环通过我的所有AVAudioPlayerNode实例并返回到第一个重复使用它之后,我根本没有注意到任何视觉故障。

因此,合乎逻辑的解决方案似乎是AVAudioPlayerNode在运行任何时间关键代码(例如在游戏期间)之前为每个实例运行以下代码:

audioNode.volume = 0
audioNode.play()

为我工作。如果您尝试它,请让我知道它是否有效。CACurrentMediaTime()此外,通过实施此更新来查看您的结果也会很有趣。

于 2021-03-04T09:26:30.980 回答