scg*_*scg 3 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)")
}
}
}
Run Code Online (Sandbox Code Playgroud)
以下是我使用 1024 缓冲区大小(我认为这是默认值)获得的 play() 的一些示例计时:
0.0218
0.0147
0.0211
0.0160
0.0184
0.0194
0.0129
0.0160
Run Code Online (Sandbox Code Playgroud)
以下是使用 256 缓冲区大小的一些示例时序:
0.0014
0.0029
0.0033
0.0023
0.0030
0.0039
0.0031
0.0032
Run Code Online (Sandbox Code Playgroud)
正如您在上面看到的,对于 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 的这个特定问题,如果是的话,是否有任何人可以提供任何见解、建议或解决方法。
我对此进行了相当彻底的调查。这是我的发现。
现在已经在各种设备和 iOS 版本(包括撰写本文时的最新版本 13.2)上测试了该行为,并让其他人也对其进行了测试,我目前的结论是,较长的执行时间是固有的,AVAudioPlayerNode.play()并且没有明显的解决方法。正如我在原来的帖子中所指出的,可以通过请求较短的缓冲区持续时间来减少执行时间,但正如前面所讨论的,这似乎不是一个可行的解决方案。
我从可靠的消息来源听说调用play()后台线程(例如使用 Grand Central Dispatch)应该是安全的,实际上这将是解决问题的一种方法。然而,尽管从技术上来说在不同线程上调用play()(或其他AVAudioEngine相关函数)可能是安全的,但我对这是否是一个好主意持怀疑态度(下面进一步解释)。
据我所知,文档并没有说明这一点,但在各种情况下AVAudioEngine都会抛出NSException's ,如果没有特殊处理,这将导致 Swift 中的应用程序终止。
NSException导致抛出异常的原因之一是,如果您AVAudioPlayerNode.play()在引擎未运行时调用。显然,如果您只需要担心自己的代码,则可以采取措施确保不会发生这种情况。
然而,iOS 本身有时会自行停止引擎,例如当发生音频中断时。如果您play()在此之后并在重新启动引擎之前调用,NSException则会抛出异常。如果所有调用都在主线程上,则很容易避免此错误play(),但多线程使问题变得复杂,并且似乎可能会带来play()引擎停止后意外调用的风险。尽管可能有办法解决这个问题,但多线程似乎会带来不良的复杂性和脆弱性,因此我选择不追求它。
我目前的策略如下。由于前面讨论的原因,我没有使用多线程。相反,我正在尽一切努力减少对 的调用次数play(),无论是总体调用次数还是每帧调用次数。除其他外,这包括仅支持立体声音频(出于各种原因,支持单声道和立体声可能会导致更多的呼叫play(),这是不希望的)。
最后,我还研究了AVAudioEngine. OpenAL 在 iOS 上仍然受支持,但已被弃用。使用低级 API(例如音频队列服务或音频单元)的自定义实现是可能的,但并不简单。我还研究了一些开源解决方案,但我研究的选项AVAudioEngine本身是在幕后使用的,因此会遇到相同的问题,和/或有其他缺点或自身的局限性。当然,也有可用的商业选项,这可能为一些开发人员提供解决方案。
| 归档时间: |
|
| 查看次数: |
964 次 |
| 最近记录: |