You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 23:57:03