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

时间:2019-09-29 17:31:15

标签: ios avaudioengine avaudioplayernode

我正在将AVAudioEngine用于iOS游戏应用程序中的音频。我遇到的一个问题是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)")
        }
    }
}

以下是我使用play大小为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的特定问题,如果有,是否有人可以提供任何见解,建议或解决方法。 / p>

2 个答案:

答案 0 :(得分:2)

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

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

audioNode.volume = 0
audioNode.play()

为我工作。如果您尝试一下,请告诉我它是否有效。此外,看到您实施此更新后的 CACurrentMediaTime() 结果也会很有趣。

答案 1 :(得分:1)

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

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

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

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

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

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

我目前的策略如下。由于前面讨论的原因,我没有使用多线程。取而代之的是,我正在尽一切努力减少总体和每帧调用play()的次数。除其他外,这包括仅支持立体声音频(出于各种原因,同时支持单声道和立体声会导致对play()的更多呼叫,这是不希望的)。

最后,我还研究了AVAudioEngine的替代方法。 iOS仍支持OpenAL,但已弃用。使用低级API(例如音频队列服务或音频单元)的自定义实现是可能的,但并非易事。我也研究了一些开源解决方案,但是我所研究的选项本身是在内部使用AVAudioEngine,因此会遇到相同的问题,并且/或者它们自身也有其他缺点或局限。当然,也有商业选项可用,可能为某些开发人员提供解决方案。

相关问题