Search code examples
iosswiftavaudioengineacr

How to use the data of a PCM buffer obtained by using the AVAudioEngine outside of the audio thread?


I'm using the AVAudioEngine in iOS to obtain audio from the microphone and writing it to buffer using an Input Node and its function installTap.
Inside the tapBlock of the installTap function, where it is supposed to be the place to read and/or manipulate the PCM buffer, I need to call a C library function, this function processes the PCM buffer data, it computes an audio fingerprint, this function also needs to read a file that is the database of the pre computed audio fingerprints in order to look for a possible match.

The problem is that apparently (correct me if i'm wrong), you cannot make any file I/O calls inside this block because this code is being run in another thread, and the file pointer that I passed to the C side of things is always null or garbage, this is not happening outside of this function, (in the main thread side of things) the pointer works, and C can read the database file.

How can I manipulate the PCM buffer in the main thread so I can make file I/O calls and being able to compute the match that I need in the C side of things?

What am I doing wrong?

Any other alternatives? Thanks.

import Foundation
import AVFoundation

let audioEngine = AVAudioEngine()

class AudioEngineTest: NSObject {
   func setupAudioEngine() {
       let input = audioEngine.inputNode

    let inputFormat = input.outputFormat(forBus: 0)
    let inputNode = audioEngine.inputNode;
    
    //Convert received buffer to required format
     let recordingFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: Double(44100), channels: 2, interleaved: false)
     let formatConverter =  AVAudioConverter(from:inputFormat, to: recordingFormat!)
     
     let pcmBuffer = AVAudioPCMBuffer(pcmFormat: recordingFormat!, frameCapacity: AVAudioFrameCount(recordingFormat!.sampleRate * 4.0))
     var error: NSError? = nil
                             
    
                
    inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(2048), format: inputFormat)
    {
        (buffer, time) in
         
      
        let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
          outStatus.pointee = AVAudioConverterInputStatus.haveData
          return buffer
        }
        
        formatConverter?.convert(to: pcmBuffer!, error: &error, withInputFrom: inputBlock)
                

        if error != nil {
          print(error!.localizedDescription)
        }
        

        //Calling the function from the C library, passing it the buffer and the pointer to the db file: dbFilePathForC an UnsafeMutablePointer<Int8>

       creatingFingerprintAndLookingForMatch(pcmbuffer, dbFilePathForC)

      //In this scope, the pointer dbFilePathFoC is either null or garbage, so the C side of things cannot read the database file, outside of this scope, the same pointer works and C can read the file, but I cannot read the PCM buffer because it only exists inside this scope of this closure of installTap, called the tapBlock 
    }  
    
      try! audioEngine.start()
    
    
   }


}

Block of code to obtain the pointer to the database file

 let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    

   let dbPath = documentsPath+"/mydb.db"
     do {
        let text = try String(contentsOfFile: dbPath)
      
        //converting dbPath to a pointer to be use in C
        let cstringForDB = (dbPath as NSString).utf8String
        let dbFilePathForC = UnsafeMutablePointer<Int8>(mutating: cstringForDB!);
       

    } catch  {
        print("error cannot read the db file")
    }

Solution

  • I/O calls are allowed on any thread. The problem is in your C-string conversion to UnsafeMutablePointer<Int8> (it's called unsafe for a good reason). You're doing this on a stack let "variable" which will vanish once your PCM Audio non main thread finishes. Hence you end up with a dangling pointer pointing at some random memory. I suspect you don't seem to experience the same problem on main thread because it's always there throughout app lifetime and hitting a dangling pointer on its stack is less probable (but still definitely possible). The solution is to get your UnsafeMutablePointer<Int8> (courtesy of yossan) like this:

    func makeCString(from str: String) -> UnsafeMutablePointer<Int8> {
        let count = str.utf8CString.count 
        let result: UnsafeMutableBufferPointer<Int8> = UnsafeMutableBufferPointer<Int8>.allocate(capacity: count)
        _ = result.initialize(from: str.utf8CString)
        return result.baseAddress!
    }
    

    By doing so you allocate space on the heap for the C-string which is shared between all threads in a safe manner (as long it's a memory read only).