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

如何基于AVAudioEngine实现可动态开关的带阻滤波器滤除录音蜂声

实现方案

首先修正参数笔误:日常提示蜂声不可能为2350kHz(千赫兹,2.35MHz属于超声波范畴),结合阻带范围描述,实际应为23002400Hz(2.32.4kHz),以下实现均按Hz参数处理。

核心原理

  • 使用AVAudioEngine内置的AVAudioUnitEQ实现带阻(陷波)滤波,无需自行编写DSP代码
  • 滤波仅作用于麦克风输入链路,不影响蜂声播放输出
  • 可实时调整滤波参数,完全无需中断录制即可开关滤波

节点链改造

原节点链路:

inputNode(麦克风) → 自定义mixerNode → mainMixerNode → 扬声器
audioPlayer(蜂声) → mainMixerNode → 扬声器

改造后链路(仅麦输入增加滤波环节):

inputNode(麦克风) → EQ滤波单元 → 自定义mixerNode → mainMixerNode → 扬声器
audioPlayer(蜂声) → mainMixerNode → 扬声器

参数配置说明

AVAudioUnitEQ仅需1个滤波频段即可实现需求,参数配置如下:

  • 滤波类型:.bandRes(带阻/陷波)
  • 中心频率:2350Hz
  • 带宽:约0.06倍频程(对应2300~2400Hz的-3dB衰减范围,可根据实际效果微调)
  • 衰减增益:-96dB(完全衰减阻带信号)
  • 初始状态:启用滤波

实时开关滤波

直接修改EQ频段的bypass属性即可,无需断开连接、停止引擎,实时生效无中断:

  • 启用滤波:eqNode.bands.first?.bypass = false
  • 关闭滤波:eqNode.bands.first?.bypass = true

改造后代码

class Recorder {
  enum RecordingState {
    case recording, paused, stopped
  }
  
  private var engine: AVAudioEngine!
  private var mixerNode: AVAudioMixerNode!
  private var eqNode: AVAudioUnitEQ! // 新增:滤波单元
  private var state: RecordingState = .stopped
  private var currentRecordingFile: AVAudioFile? // 新增:持有录制文件引用
  
  private var audioPlayer = AVAudioPlayerNode()
  private var beepFile: AVAudioFile? // 新增:预加载蜂声文件
  
  init() {
    setupSession()
    setupEngine()
    preloadResources()
  }
    
  fileprivate func setupSession() {
      let session = AVAudioSession.sharedInstance()
      try? session.setCategory(.playAndRecord, options: [.mixWithOthers, .defaultToSpeaker])
      try? session.setActive(true, options: .notifyOthersOnDeactivation)
   }
    
    fileprivate func setupEngine() {
      engine = AVAudioEngine()
      mixerNode = AVAudioMixerNode()
      mixerNode.volume = 0 // 避免麦输入回声输出

      // 初始化带阻滤波单元
      eqNode = AVAudioUnitEQ(numberOfBands: 1)
      guard let band = eqNode.bands.first else { return }
      band.filterType = .bandRes
      band.frequency = 2350 // 蜂声中心频率
      band.bandwidth = 0.06 // 对应2300-2400Hz阻带,可微调
      band.gain = -96 // 最大衰减
      band.bypass = false // 初始启用滤波

      // 挂载所有节点
      engine.attach(mixerNode)
      engine.attach(eqNode)
      engine.attach(audioPlayer)
        
      makeConnections()
      engine.prepare()
    }

    fileprivate func preloadResources() {
        // 预加载蜂声文件,避免播放延迟
        guard let path = Bundle.main.path(forResource: "beep.mp3", ofType: nil) else { return }
        let url = URL(fileURLWithPath: path)
        beepFile = try? AVAudioFile(forReading: url)
    }

    fileprivate func makeConnections() {
      let inputNode = engine.inputNode
      let inputFormat = inputNode.outputFormat(forBus: 0)
      
      // 改造麦输入链路:input → EQ → 自定义mixer
      engine.connect(inputNode, to: eqNode, format: inputFormat)
      engine.connect(eqNode, to: mixerNode, format: inputFormat)

      let mainMixerNode = engine.mainMixerNode
      let mixerFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: inputFormat.sampleRate, channels: 1, interleaved: false)
      engine.connect(mixerNode, to: mainMixerNode, format: mixerFormat)
        
      // 蜂声链路:audioPlayer → mainMixer,不受滤波影响
      engine.connect(audioPlayer, to: mainMixerNode, format: nil)
    }
    
    //MARK: Start Recording Function
    func startRecording() throws {
        print("Start Recording!")
      let tapNode: AVAudioNode = mixerNode
      let format = tapNode.outputFormat(forBus: 0)

      let documentURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
      currentRecordingFile = try AVAudioFile(forWriting: documentURL.appendingPathComponent("recording.caf"), settings: format.settings)
      
      tapNode.installTap(onBus: 0, bufferSize: 4096, format: format, block: { [weak self] (buffer, time) in
        guard let file = self?.currentRecordingFile else { return }
        try? file.write(from: buffer)
      })

      try engine.start()
      // 每次录制重新调度蜂声,确保正常播放
      if let beepFile = beepFile {
          audioPlayer.scheduleFile(beepFile, at: nil)
      }
      audioPlayer.play()
        
      // 1秒后自动关闭滤波,无需中断录制
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
          self?.eqNode.bands.first?.bypass = true
      }
      state = .recording
    }
    
    //MARK: Other recording functions
    func resumeRecording() throws {
      try engine.start()
      state = .recording
    }

    func pauseRecording() {
      engine.pause()
      state = .paused
    }

    func stopRecording() {
      mixerNode.removeTap(onBus: 0)
      audioPlayer.stop()
      engine.stop()
      // 重置滤波状态,便于下次录制
      eqNode.bands.first?.bypass = false
      currentRecordingFile = nil
      state = .stopped
    }
}

注意事项

  • 若滤波阻带范围不符合预期,可微调bandwidth参数(单位为倍频程),越大阻带范围越宽
  • 若存在蜂声残留,可适当降低gain值(最低为-96dB)或微调中心频率至实际蜂声频率
  • 代码中已修正原实现的几个问题:蜂声文件预加载、每次录制重新调度蜂声、持有录制文件引用避免写入失败

内容的提问来源于stack exchange,提问作者coder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.30 07:54:03