swiftfrequencyavaudioenginelowpass-filterhighpass-filter

Low Pass filter + sample rate conversion using Avaudioengine iOS


We are working on a project which allows us to record some sounds from a microphone with a 5k Hz sample rate with some Low-Pass filter & HighPass filter.

What we are using

We are using AvaudioEngine for this purpose.

We are using AVAudioConverter for downgrading the sample rate.

We are using AVAudioUnitEQ for the LowPass & HighPass filter.

Code

let bus = 0
let inputNode = engine.inputNode

let equalizer = AVAudioUnitEQ(numberOfBands: 2)

equalizer.bands[0].filterType = .lowPass
equalizer.bands[0].frequency = 3000
equalizer.bands[0].bypass = false

equalizer.bands[1].filterType = .highPass
equalizer.bands[1].frequency = 1000
equalizer.bands[1].bypass = false
engine.attach(equalizer) //Attach equalizer

// Connect nodes
engine.connect(inputNode, to: equalizer, format: inputNode.inputFormat(forBus: 0))
engine.connect(equalizer, to: engine.mainMixerNode, format: inputNode.inputFormat(forBus: 0))
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: inputNode.inputFormat(forBus: 0))

let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                            sampleRate: 5000,
                                            channels: 1,
                                            interleaved: false)!

// Converter to downgrade sample rate
guard let converter: AVAudioConverter = AVAudioConverter(from: inputNode.inputFormat(forBus: 0), to: outputFormat) else {
           print("Can't convert in to this format")
           return
       }

engine.mainMixerNode.installTap(onBus: bus, bufferSize: 2688, format: engine.mainMixerNode.outputFormat(forBus: 0)) { (buffer, time) in
           
     var newBufferAvailable = true
           
     let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in
           if newBufferAvailable {
                outStatus.pointee = .haveData
                newBufferAvailable = false
                return buffer
           } else {
                outStatus.pointee = .noDataNow
                return nil
           }
     }
           
           
     let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(outputFormat.sampleRate) * buffer.frameLength / AVAudioFrameCount(buffer.format.sampleRate))!

           var error: NSError?
           let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback)
           assert(status != .error)

           
           if status == .haveData {
             // Process with converted buffer
           }
            
       }
       
       engine.prepare()
       
       do {
           try engine.start()
       } catch {
           print("Can't start the engine: \(error)")
       }

Issue

low-pass and high-pass filters are not working.

Alternate Approach

To check code is working or not, we have added a reverb effect instead of lowpass filter. Reverb effect(Using AVAudioUnitReverb) works with same code.

Can anyone help me where are we doing wrong in applying lowpass filter?


Solution

  • I think the main problem with this code was that the AVAudioConverter was being created before calling engine.prepare() which can and will change the mainMixerNode output format. Aside from that, there was a redundant connection of mainMixerNode to outputNode, along with a probably incorrect format - mainMixerNode is documented to be automatically created and connected to the output node "on demand". The tap also did not need a format.

    let bus = 0
    let inputNode = engine.inputNode
    
    let equalizer = AVAudioUnitEQ(numberOfBands: 2)
    
    equalizer.bands[0].filterType = .lowPass
    equalizer.bands[0].frequency = 3000
    equalizer.bands[0].bypass = false
    
    equalizer.bands[1].filterType = .highPass
    equalizer.bands[1].frequency = 1000
    equalizer.bands[1].bypass = false
    engine.attach(equalizer) //Attach equalizer
    
    // Connect nodes
    engine.connect(inputNode, to: equalizer, format: inputNode.inputFormat(forBus: 0))
    engine.connect(equalizer, to: engine.mainMixerNode, format: inputNode.inputFormat(forBus: 0))
    
    // call before creating converter because this changes the mainMixer's output format
    engine.prepare()
    
    let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                     sampleRate: 5000,
                                     channels: 1,
                                     interleaved: false)!
    
    // Downsampling converter
    guard let converter: AVAudioConverter = AVAudioConverter(from: engine.mainMixerNode.outputFormat(forBus: 0), to: outputFormat) else {
        print("Can't convert in to this format")
        return
    }
    
    engine.mainMixerNode.installTap(onBus: bus, bufferSize: 2688, format: nil) { (buffer, time) in
        var newBufferAvailable = true
        
        let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in
            if newBufferAvailable {
                outStatus.pointee = .haveData
                newBufferAvailable = false
                return buffer
            } else {
                outStatus.pointee = .noDataNow
                return nil
            }
        }
        
        
        let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(outputFormat.sampleRate) * buffer.frameLength / AVAudioFrameCount(buffer.format.sampleRate))!
        
        var error: NSError?
        let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback)
        assert(status != .error)
        
        
        if status == .haveData {
            // Process with converted buffer
        }
    }
    
    do {
        try engine.start()
    } catch {
        print("Can't start the engine: \(error)")
    }