AudioKit v5同一输入节点同时使用AmplitudeTap与NodeRecorder报错
问题根因
CoreAudio 底层存在限制:单个 AVAudioNode 的同一音频总线最多仅支持挂载 1 个 Tap 回调。你当前代码中 NodeRecorder 内部会自动给绑定节点添加 Tap,AmplitudeTap 本身就是 Tap 实现,二者同时绑定到同一个 engine.input 节点,第二个尝试添加 Tap 的操作就会触发 nullptr == Tap() 的断言异常。
解决方案
核心思路是将两个 Tap 分别挂载到不同的音频节点,利用音频链路数据透传的特性,不会影响录制结果和振幅检测的准确性。推荐两种实现方案:
方案一:最小改动适配
新增一个仅用于振幅检测的透明中转节点,不和原始录制逻辑冲突,也不会影响输出消音的效果:
func setup() { if mixer == nil { mixer = Mixer() } guard let input = engine.input else { return false } if recorder == nil { do { // Recorder 仍然绑定原始输入节点 recorder = try NodeRecorder(node: input) } catch let err { print("Error: \(err)") } } if silencer == nil { let silencer = Fader(input, gain: 0.0) self.silencer = silencer } if !mixer.hasInput(silencer) { mixer.addInput(silencer) } engine.output = mixer // 新增振幅检测专用中转节点,增益保持1确保检测数据准确 let amplitudeMonitor = Fader(input, gain: 1.0) // 加入混音链路确保有数据流经过,输出增益设为0避免回声 mixer.addInput(amplitudeMonitor) amplitudeMonitor.volume = 0.0 // AmplitudeTap 绑定中转节点,不再抢占input的Tap资源 tracker = AmplitudeTap(amplitudeMonitor) { amplitude in DispatchQueue.main.async { self.update(amplitude) } } }
方案二:通用分发架构(适合后续需要扩展输入处理逻辑的场景)
新增专门的输入分发 Mixer,所有输入相关的处理都从分发节点分支,避免后续新增 Tap 再次出现冲突:
// 新增全局属性:输入分发混合器 private var inputSplitter: Mixer? func setup() { if mixer == nil { mixer = Mixer() } guard let input = engine.input else { return false } // 初始化分发器,所有输入数据先进入分发器 if inputSplitter == nil { inputSplitter = Mixer(input) } guard let inputSplitter = inputSplitter else { return false } if recorder == nil { do { // 录制器绑定分发器 recorder = try NodeRecorder(node: inputSplitter) } catch let err { print("Error: \(err)") } } if silencer == nil { // 消音链路从分发器分支 let silencer = Fader(inputSplitter, gain: 0.0) self.silencer = silencer } if !mixer.hasInput(silencer) { mixer.addInput(silencer) } engine.output = mixer // 振幅检测链路从分发器独立分支 let amplitudeMonitor = Fader(inputSplitter, gain: 1.0) mixer.addInput(amplitudeMonitor) amplitudeMonitor.volume = 0.0 tracker = AmplitudeTap(amplitudeMonitor) { amplitude in DispatchQueue.main.async { self.update(amplitude) } } }
额外优化建议
调整启停方法的操作顺序,避免出现资源释放异常:
- 启动顺序:先启动引擎,再启动 Tap,最后启动录制
- 停止顺序:先停止录制,再停止 Tap,最后停止引擎
修改后启停代码示例:
func start() { do { try engine.start() tracker.start() NodeRecorder.removeTempFiles() try recorder?.record() } catch let err { print(err) } } func stop() -> String { recorder?.stop() tracker.stop() engine.stop() if let file = recorder?.audioFile { print("playing recorded file") player.file = file player.play() } return recorder?.audioFile.url.path ?? "" }
内容的提问来源于stack exchange,提问作者Nazarii Kahaniak
相关产品推荐
相关产品推荐

