iOS - AVAudioPlayerNode.play() 执行很慢
iOS - AVAudioPlayerNode.play() execution is very slow
我在 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 毫秒的时间可以使用时,它仍然很昂贵。
这是 iPad Mini 2 运行 iOS 12.4.2。这显然是一台旧设备,但我在模拟器上看到的结果似乎成正比,因此它似乎与缓冲区大小和函数本身的行为有关,而不是与所使用的硬件有关。我不知道引擎盖下发生了什么,但 play() 似乎有可能阻塞到下一个音频周期开始或类似的事情。
请求较小的缓冲区大小似乎是部分解决方案,但存在一些潜在的缺点。根据文档 here, lower buffer sizes can mean more disk access when streaming from a file, and irrespective of that, the request may not be honored at all. Also, ,有人报告了与低缓冲区大小相关的播放问题。考虑到所有这些,我不愿意将其作为解决方案。
这让我的 play() 执行时间在 15-20 毫秒范围内,这通常意味着在 60 FPS 时会丢失帧。如果我安排一次只调用 play() 一次,而且很少调用,也许它不会引人注意,但这并不理想。
我在其他地方搜索过相关信息并询问过这个问题,但似乎在实践中遇到这种行为的人并不多,或者这对他们来说不是问题。
AVAudioEngine 旨在用于实时应用程序,因此如果我是对的 AVAudioPlayerNode.play() 阻塞与缓冲区大小成比例的大量时间,这似乎是一个设计问题。我意识到这可能不是很多人正在处理的问题,但我在这里发帖询问是否有人遇到过 AVAudioEngine 的这个特定问题,如果是,如果有任何见解、建议或解决方法,任何人都可以提供。
我对此进行了相当彻底的调查。这是我的发现。
现在已经在各种设备和 iOS 版本(包括撰写本文时的最新版本 13.2)上测试了行为,并且让其他人也对其进行了测试,我目前的结论是AVAudioPlayerNode.play()
的执行时间长是固有的,并且没有明显的解决方法。正如我最初提到的 post,可以通过请求更短的缓冲持续时间来减少执行时间,但如前所述,这似乎不是一个可行的解决方案。
我从可靠消息来源那里听说,在后台线程上调用 play()
(例如使用 Grand Central Dispatch)应该是安全的,而且这确实是解决问题的一种方法。然而,尽管在不同线程上调用 play()
(或其他 AVAudioEngine
相关函数)在技术上可能是安全的,但我怀疑这是否是一个好主意(下面有进一步的解释)。
据我所知,文档没有说明这一点,但 AVAudioEngine
会在各种情况下抛出 NSException
,如果不进行特殊处理,将导致应用程序终止Swift.
导致抛出 NSException
的其中一个原因是如果您调用 AVAudioPlayerNode.play()
而引擎未处于 运行。显然,如果您只需要担心自己的代码,您可以采取措施确保这种情况不会发生。
但是,iOS 本身有时会自行停止引擎,例如当发生音频中断时。如果您在此之后调用 play()
并在重新启动引擎之前调用,则会抛出 NSException
。如果对 play()
的所有调用都在主线程上,则很容易避免此错误,但多线程使问题复杂化,并且似乎可能会引入在引擎停止后意外调用 play()
的风险.尽管可能有解决此问题的方法,但多线程似乎引入了不希望的复杂性和脆弱性,因此我选择不追求它。
我目前的策略如下。由于前面讨论的原因,我没有使用多线程。相反,我正在尽我所能减少对 play()
的调用次数,包括整体和每帧。这包括仅支持立体声音频(出于各种原因,同时支持单声道和立体声会导致对 play()
的更多调用,这是不希望的)。
最后,我还研究了 AVAudioEngine
的替代方案。 iOS 仍支持 OpenAL,但已弃用。使用音频队列服务或音频单元等低级 API 的自定义实现是可能的,但并非易事。我也看过一些开源解决方案,但我看过的选项在幕后使用 AVAudioEngine
,因此遇到同样的问题,and/or 有其他缺点或局限性。当然也有商业的选择,可以为部分开发者提供解决方案。
我在 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 毫秒的时间可以使用时,它仍然很昂贵。
这是 iPad Mini 2 运行 iOS 12.4.2。这显然是一台旧设备,但我在模拟器上看到的结果似乎成正比,因此它似乎与缓冲区大小和函数本身的行为有关,而不是与所使用的硬件有关。我不知道引擎盖下发生了什么,但 play() 似乎有可能阻塞到下一个音频周期开始或类似的事情。
请求较小的缓冲区大小似乎是部分解决方案,但存在一些潜在的缺点。根据文档 here, lower buffer sizes can mean more disk access when streaming from a file, and irrespective of that, the request may not be honored at all. Also,
这让我的 play() 执行时间在 15-20 毫秒范围内,这通常意味着在 60 FPS 时会丢失帧。如果我安排一次只调用 play() 一次,而且很少调用,也许它不会引人注意,但这并不理想。
我在其他地方搜索过相关信息并询问过这个问题,但似乎在实践中遇到这种行为的人并不多,或者这对他们来说不是问题。
AVAudioEngine 旨在用于实时应用程序,因此如果我是对的 AVAudioPlayerNode.play() 阻塞与缓冲区大小成比例的大量时间,这似乎是一个设计问题。我意识到这可能不是很多人正在处理的问题,但我在这里发帖询问是否有人遇到过 AVAudioEngine 的这个特定问题,如果是,如果有任何见解、建议或解决方法,任何人都可以提供。
我对此进行了相当彻底的调查。这是我的发现。
现在已经在各种设备和 iOS 版本(包括撰写本文时的最新版本 13.2)上测试了行为,并且让其他人也对其进行了测试,我目前的结论是AVAudioPlayerNode.play()
的执行时间长是固有的,并且没有明显的解决方法。正如我最初提到的 post,可以通过请求更短的缓冲持续时间来减少执行时间,但如前所述,这似乎不是一个可行的解决方案。
我从可靠消息来源那里听说,在后台线程上调用 play()
(例如使用 Grand Central Dispatch)应该是安全的,而且这确实是解决问题的一种方法。然而,尽管在不同线程上调用 play()
(或其他 AVAudioEngine
相关函数)在技术上可能是安全的,但我怀疑这是否是一个好主意(下面有进一步的解释)。
据我所知,文档没有说明这一点,但 AVAudioEngine
会在各种情况下抛出 NSException
,如果不进行特殊处理,将导致应用程序终止Swift.
导致抛出 NSException
的其中一个原因是如果您调用 AVAudioPlayerNode.play()
而引擎未处于 运行。显然,如果您只需要担心自己的代码,您可以采取措施确保这种情况不会发生。
但是,iOS 本身有时会自行停止引擎,例如当发生音频中断时。如果您在此之后调用 play()
并在重新启动引擎之前调用,则会抛出 NSException
。如果对 play()
的所有调用都在主线程上,则很容易避免此错误,但多线程使问题复杂化,并且似乎可能会引入在引擎停止后意外调用 play()
的风险.尽管可能有解决此问题的方法,但多线程似乎引入了不希望的复杂性和脆弱性,因此我选择不追求它。
我目前的策略如下。由于前面讨论的原因,我没有使用多线程。相反,我正在尽我所能减少对 play()
的调用次数,包括整体和每帧。这包括仅支持立体声音频(出于各种原因,同时支持单声道和立体声会导致对 play()
的更多调用,这是不希望的)。
最后,我还研究了 AVAudioEngine
的替代方案。 iOS 仍支持 OpenAL,但已弃用。使用音频队列服务或音频单元等低级 API 的自定义实现是可能的,但并非易事。我也看过一些开源解决方案,但我看过的选项在幕后使用 AVAudioEngine
,因此遇到同样的问题,and/or 有其他缺点或局限性。当然也有商业的选择,可以为部分开发者提供解决方案。