samp17
samp17

Reputation: 577

swift AVAudioEngine and AVAudioSinkNode sampleRate convert

I have been having issues with this for a while now, and have written the following swift file that can be run on as the main view controller file for an app. Upon execution, it will play a short blast of a 1kHz sine wave. It will simultaneously record from the input of the audio interface.

Currently I have plugged in the output to the input for testing. But this could just as well be the computers built in speakers and built in mic (just check the volume in the system settings before running the app, as it will play automatically)

I cannot get this to give me an accurate result:

import UIKit
import AVFoundation

var globalSampleRate = 48000


class ViewController: UIViewController {
    var micBuffer:[Float] = Array(repeating:0, count:10000)
    var referenceBuffer:[Float] = Array(repeating:0, count:10000)
    var running:Bool = false
    var engine = AVAudioEngine()

    override func viewDidLoad() {
        super.viewDidLoad()

        let syncQueue = DispatchQueue(label:"Audio Engine")
        syncQueue.sync{
            initializeAudioEngine()
            while running == true {
            }
            engine.stop()
            writetoFile(buff: micBuffer, name: "Mic Input")
            writetoFile(buff: referenceBuffer, name: "Reference")

        }
    }

    func initializeAudioEngine(){

        var micBufferPosition:Int = 0
        var refBufferPosition:Int = 0
        let frequency:Float = 1000.0
        let amplitude:Float = 1.0
        let signal = { (time: Float) -> Float in
            return amplitude * sin(2.0 * Float.pi * frequency * time)
        }

        let deltaTime = 1.0 / Float(globalSampleRate)
        var time: Float = 0

        let micSinkNode = AVAudioSinkNode() { (timeStamp, frames, audioBufferList) ->
          OSStatus in

            let ptr = audioBufferList.pointee.mBuffers.mData?.assumingMemoryBound(to: Float.self)
            var monoSamples = [Float]()
            monoSamples.append(contentsOf: UnsafeBufferPointer(start: ptr, count: Int(frames)))
            for frame in 0..<frames {
              self.micBuffer[micBufferPosition + Int(frame)] = monoSamples[Int(frame)]
            }
            micBufferPosition += Int(frames)

            if micBufferPosition > 8000 {
                self.running = false
            }

            return noErr
        }


        let srcNode = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
            let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList)
            for frame in 0..<Int(frameCount) {
                let value = signal(time)
                time += deltaTime
                for buffer in ablPointer {
                    let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
                    buf[frame] = value
                    self.referenceBuffer[refBufferPosition + frame] = value
                }

            }
            refBufferPosition += Int(frameCount)
            return noErr
        }

        let inputFormat = engine.inputNode.inputFormat(forBus: 0)
        let outputFormat = engine.outputNode.outputFormat(forBus: 0)
        let nativeFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32,
            sampleRate: Double(globalSampleRate),
            channels: 1,
            interleaved: false)

        let formatMixer  = AVAudioMixerNode()
        engine.attach(formatMixer)
        engine.attach(micSinkNode)
        engine.attach(srcNode)
        //engine.connect(engine.inputNode, to: micSinkNode, format: inputFormat)
        engine.connect(engine.inputNode, to: formatMixer, format: inputFormat)
        engine.connect(formatMixer, to: micSinkNode, format: nativeFormat)
        engine.connect(srcNode, to: engine.mainMixerNode, format: nativeFormat)
        engine.connect(engine.mainMixerNode, to: engine.outputNode, format: outputFormat)
        print("micSinkNode Format is \(micSinkNode.inputFormat(forBus: 0))")
        print("inputNode Format is \(engine.inputNode.inputFormat(forBus: 0))")
        print("outputNode Format is \(engine.outputNode.outputFormat(forBus: 0))")
        print("formatMixer Format is \(formatMixer.outputFormat(forBus: 0))")

        engine.prepare()
        running = true
        do {
            try engine.start()
        } catch {
            print("Error")
        }
    }

}


func writetoFile(buff:[Float], name:String){

    let outputFormatSettings = [
        AVFormatIDKey:kAudioFormatLinearPCM,
        AVLinearPCMBitDepthKey:32,
        AVLinearPCMIsFloatKey: true,
        AVLinearPCMIsBigEndianKey: true,
        AVSampleRateKey: globalSampleRate,
        AVNumberOfChannelsKey: 1
        ] as [String : Any]

    let fileName = name
    let DocumentDirURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)


    let url = DocumentDirURL.appendingPathComponent(fileName).appendingPathExtension("wav")
    print("FilePath: \(url.path)")

    let audioFile = try? AVAudioFile(forWriting: url, settings: outputFormatSettings, commonFormat: AVAudioCommonFormat.pcmFormatFloat32, interleaved: false)

    let bufferFormat = AVAudioFormat(settings: outputFormatSettings)

    let outputBuffer = AVAudioPCMBuffer(pcmFormat: bufferFormat!, frameCapacity: AVAudioFrameCount(buff.count))

    for i in 0..<buff.count {
        outputBuffer?.floatChannelData!.pointee[i] = Float(( buff[i] ))
    }
    outputBuffer!.frameLength = AVAudioFrameCount( buff.count )

    do{
        try audioFile?.write(from: outputBuffer!)

    } catch let error as NSError {
        print("error:", error.localizedDescription)
    }
}

If I run this app, the console will print out the url of the two wavs created (one being the generated sine wave and the other being the recorded mic input). If I inspect these in a daw I get the following. You can see that the two sine waves do not stay in sync. This leads me to believe the sample rates are different, however the formats printed to the console show me that they are not different.

Originally the inputNode was direct to the micSinkNode, however I have inserted an AVAudioMixerNode to try and convert the format prior to using AVAudioSinkNode.

The aim is to be able to use any sampleRate hardware running using its own settings, and to save the samples to the apps preferred 'native settings'. (ie the app will number crunch at 48kHz. I would like to be able to use 96k hardware, and different channel counts).

Can anyone suggest why this isn't working how it should?

enter image description here

Upvotes: 3

Views: 1362

Answers (1)

akuz
akuz

Reputation: 637

AVAudioSinkNode does not support format conversions.

It must handle the data in the hardware input format.

Reference: talk from WWDC 2019, around 3 min 55 sec here: https://developer.apple.com/videos/play/wwdc2019/510/

P.S. I am currently struggling to set a non-default sample rate on the device (which I assume is supported by the hardware, because I pick it from the list of available sample rates), but even in this case of non-default sample rate, the sink stops working completely (no crash, just doesn't get called).

Upvotes: 0

Related Questions