播放 AVAudioPlayerNode 时 AVAudioEngine inputNode 的格式发生变化
AVAudioEngine inputNode's format changes when playing an AVAudioPlayerNode
我将从一个简单的 "playground" 视图控制器开始 class 我已经制作了它来演示我的问题:
class AudioEnginePlaygroundViewController: UIViewController {
private var audioEngine: AVAudioEngine!
private var micTapped = false
override func viewDidLoad() {
super.viewDidLoad()
configureAudioSession()
audioEngine = AVAudioEngine()
}
@IBAction func toggleMicTap(_ sender: Any) {
guard let mic = audioEngine.inputNode else {
return
}
if micTapped {
mic.removeTap(onBus: 0)
micTapped = false
return
}
stopAudioPlayback()
let micFormat = mic.inputFormat(forBus: 0)
print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
print("in tap completion")
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
micTapped = true
startEngine()
}
@IBAction func playAudioFile(_ sender: Any) {
stopAudioPlayback()
let playerNode = AVAudioPlayerNode()
let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
let audioFile = readableAudioFileFrom(url: audioUrl)
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
startEngine()
playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
playerNode.play()
}
// MARK: Internal Methods
private func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
try AVAudioSession.sharedInstance().setActive(true)
} catch { }
}
private func readableAudioFileFrom(url: URL) -> AVAudioFile {
var audioFile: AVAudioFile!
do {
try audioFile = AVAudioFile(forReading: url)
} catch { }
return audioFile
}
private func startEngine() {
guard !audioEngine.isRunning else {
return
}
do {
try audioEngine.start()
} catch { }
}
private func stopAudioPlayback() {
audioEngine.stop()
audioEngine.reset()
}
}
上面的 VC 有一个 AVAudioEngine 实例和两个 UIButton 动作:一个播放在硬编码 url 中找到的音频文件,另一个切换 installation/removal点击引擎的 inputNode。
我的目标是让实时麦克风敲击和音频文件播放同时工作,但彼此完全互斥。也就是说,无论麦克风的当前状态如何,我都希望能够触发播放,反之亦然。如果我在触发音频文件播放之前安装水龙头,一切都会完全按预期工作。但是,如果我先播放音频文件,然后尝试安装水龙头,我会遇到以下崩溃:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAEGraphNode.mm:810:CreateRecordingTap: (IsFormatSampleRateAndChannelCountValid(format))]
这让我通过 installTap 调用上方的日志语句检查了麦克风格式的数据。果然,当我在播放前安装 tap 时,我得到了 44100.0 的预期采样率和 1 的通道数。但是当我先播放音频文件并 然后 安装麦克风 tap 时,我的日志显示采样率为 0,通道数为 2,这给了我上面显示的错误。
我已经尝试修改 AVAudioEngine 的 start/reset 流程,尝试了我的 AVAudioSession 的不同 category/mode 组合(参见我的 configureAudioSession 方法),并手动尝试像这样创建 tap 格式:
let micFormat = mic.inputFormat(forBus: 0)
var trueFormat: AVAudioFormat!
if micFormat.sampleRate == 0 {
trueFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)
} else {
trueFormat = micFormat
}
print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: trueFormat) { (buffer, when) in
print("in tap completion")
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
这给了我一个相似但不同的错误:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAudioIONodeImpl.mm:896:SetOutputFormat: (IsFormatSampleRateAndChannelCountValid(hwFormat))]
我看不出麦克风的格式数据会根据是否播放 AVAudioPlayerNode 而变化的任何原因。
经过一番搜索,我发现了问题所在。问题在于音频引擎的 inputNode 单例。来自文档:
The audio engine creates a singleton on demand when inputNode is first accessed. To receive input, connect another audio node from the output of the input audio node, or create a recording tap on it.
加上对我遇到的格式问题的引用:
Check the input format of input node (specifically, the hardware format) for a non-zero sample rate and channel count to see if input is enabled.
在我的 playground class 中,触发音频文件播放的流程在创建 "active chain" 之前从不访问引擎的 inputNode:
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
如果您希望引擎在内部为输入配置自身,您似乎必须在启动之前访问 AVAudioEngine 的 inputNode。即使停止()和重置()引擎也不会导致访问 inputNode 以重新配置引擎。 (我怀疑通过 disconnectNode 调用手动中断活动链将允许内部重新配置,但我还不确定)。
所以代码方面的修复很简单:只需在实例化后立即访问引擎的输入节点,以便为音频输入配置引擎。这是完整的 class,文件播放和麦克风敲击一起工作:
import UIKit
class AudioEnginePlaygroundViewController: UIViewController {
private var audioEngine: AVAudioEngine!
private var mic: AVAudioInputNode!
private var micTapped = false
override func viewDidLoad() {
super.viewDidLoad()
configureAudioSession()
audioEngine = AVAudioEngine()
mic = audioEngine.inputNode!
}
@IBAction func toggleMicTap(_ sender: Any) {
if micTapped {
mic.removeTap(onBus: 0)
micTapped = false
return
}
let micFormat = mic.inputFormat(forBus: 0)
mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
micTapped = true
startEngine()
}
@IBAction func playAudioFile(_ sender: Any) {
stopAudioPlayback()
let playerNode = AVAudioPlayerNode()
let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
let audioFile = readableAudioFileFrom(url: audioUrl)
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
startEngine()
playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
playerNode.play()
}
// MARK: Internal Methods
private func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
try AVAudioSession.sharedInstance().setActive(true)
} catch { }
}
private func readableAudioFileFrom(url: URL) -> AVAudioFile {
var audioFile: AVAudioFile!
do {
try audioFile = AVAudioFile(forReading: url)
} catch { }
return audioFile
}
private func startEngine() {
guard !audioEngine.isRunning else {
return
}
do {
try audioEngine.start()
} catch { }
}
private func stopAudioPlayback() {
audioEngine.stop()
audioEngine.reset()
}
}
我将从一个简单的 "playground" 视图控制器开始 class 我已经制作了它来演示我的问题:
class AudioEnginePlaygroundViewController: UIViewController {
private var audioEngine: AVAudioEngine!
private var micTapped = false
override func viewDidLoad() {
super.viewDidLoad()
configureAudioSession()
audioEngine = AVAudioEngine()
}
@IBAction func toggleMicTap(_ sender: Any) {
guard let mic = audioEngine.inputNode else {
return
}
if micTapped {
mic.removeTap(onBus: 0)
micTapped = false
return
}
stopAudioPlayback()
let micFormat = mic.inputFormat(forBus: 0)
print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
print("in tap completion")
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
micTapped = true
startEngine()
}
@IBAction func playAudioFile(_ sender: Any) {
stopAudioPlayback()
let playerNode = AVAudioPlayerNode()
let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
let audioFile = readableAudioFileFrom(url: audioUrl)
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
startEngine()
playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
playerNode.play()
}
// MARK: Internal Methods
private func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
try AVAudioSession.sharedInstance().setActive(true)
} catch { }
}
private func readableAudioFileFrom(url: URL) -> AVAudioFile {
var audioFile: AVAudioFile!
do {
try audioFile = AVAudioFile(forReading: url)
} catch { }
return audioFile
}
private func startEngine() {
guard !audioEngine.isRunning else {
return
}
do {
try audioEngine.start()
} catch { }
}
private func stopAudioPlayback() {
audioEngine.stop()
audioEngine.reset()
}
}
上面的 VC 有一个 AVAudioEngine 实例和两个 UIButton 动作:一个播放在硬编码 url 中找到的音频文件,另一个切换 installation/removal点击引擎的 inputNode。
我的目标是让实时麦克风敲击和音频文件播放同时工作,但彼此完全互斥。也就是说,无论麦克风的当前状态如何,我都希望能够触发播放,反之亦然。如果我在触发音频文件播放之前安装水龙头,一切都会完全按预期工作。但是,如果我先播放音频文件,然后尝试安装水龙头,我会遇到以下崩溃:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAEGraphNode.mm:810:CreateRecordingTap: (IsFormatSampleRateAndChannelCountValid(format))]
这让我通过 installTap 调用上方的日志语句检查了麦克风格式的数据。果然,当我在播放前安装 tap 时,我得到了 44100.0 的预期采样率和 1 的通道数。但是当我先播放音频文件并 然后 安装麦克风 tap 时,我的日志显示采样率为 0,通道数为 2,这给了我上面显示的错误。
我已经尝试修改 AVAudioEngine 的 start/reset 流程,尝试了我的 AVAudioSession 的不同 category/mode 组合(参见我的 configureAudioSession 方法),并手动尝试像这样创建 tap 格式:
let micFormat = mic.inputFormat(forBus: 0)
var trueFormat: AVAudioFormat!
if micFormat.sampleRate == 0 {
trueFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)
} else {
trueFormat = micFormat
}
print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: trueFormat) { (buffer, when) in
print("in tap completion")
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
这给了我一个相似但不同的错误:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAudioIONodeImpl.mm:896:SetOutputFormat: (IsFormatSampleRateAndChannelCountValid(hwFormat))]
我看不出麦克风的格式数据会根据是否播放 AVAudioPlayerNode 而变化的任何原因。
经过一番搜索,我发现了问题所在。问题在于音频引擎的 inputNode 单例。来自文档:
The audio engine creates a singleton on demand when inputNode is first accessed. To receive input, connect another audio node from the output of the input audio node, or create a recording tap on it.
加上对我遇到的格式问题的引用:
Check the input format of input node (specifically, the hardware format) for a non-zero sample rate and channel count to see if input is enabled.
在我的 playground class 中,触发音频文件播放的流程在创建 "active chain" 之前从不访问引擎的 inputNode:
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
如果您希望引擎在内部为输入配置自身,您似乎必须在启动之前访问 AVAudioEngine 的 inputNode。即使停止()和重置()引擎也不会导致访问 inputNode 以重新配置引擎。 (我怀疑通过 disconnectNode 调用手动中断活动链将允许内部重新配置,但我还不确定)。
所以代码方面的修复很简单:只需在实例化后立即访问引擎的输入节点,以便为音频输入配置引擎。这是完整的 class,文件播放和麦克风敲击一起工作:
import UIKit
class AudioEnginePlaygroundViewController: UIViewController {
private var audioEngine: AVAudioEngine!
private var mic: AVAudioInputNode!
private var micTapped = false
override func viewDidLoad() {
super.viewDidLoad()
configureAudioSession()
audioEngine = AVAudioEngine()
mic = audioEngine.inputNode!
}
@IBAction func toggleMicTap(_ sender: Any) {
if micTapped {
mic.removeTap(onBus: 0)
micTapped = false
return
}
let micFormat = mic.inputFormat(forBus: 0)
mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}
micTapped = true
startEngine()
}
@IBAction func playAudioFile(_ sender: Any) {
stopAudioPlayback()
let playerNode = AVAudioPlayerNode()
let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
let audioFile = readableAudioFileFrom(url: audioUrl)
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
startEngine()
playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
playerNode.play()
}
// MARK: Internal Methods
private func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
try AVAudioSession.sharedInstance().setActive(true)
} catch { }
}
private func readableAudioFileFrom(url: URL) -> AVAudioFile {
var audioFile: AVAudioFile!
do {
try audioFile = AVAudioFile(forReading: url)
} catch { }
return audioFile
}
private func startEngine() {
guard !audioEngine.isRunning else {
return
}
do {
try audioEngine.start()
} catch { }
}
private func stopAudioPlayback() {
audioEngine.stop()
audioEngine.reset()
}
}