0

To explain my situation a little better I'm trying to make an app which will play a ping noise when a button is pressed and then proceed to record and transcribe the user's voice immediately after.

For the ping sound I'm using System Sound Services, to record the audio I'm using AudioToolbox, and to transcribe it I'm using Speech kit.

I believe the crux of my problem lies in the timing of the asynchronous System sound services play function:

    //Button pressed function

    let audiosession = AVAudioSession.sharedInstance()

    let filename = "Ping"
    let ext = "wav"

    if let soundUrl = Bundle.main.url(forResource: filename, withExtension: ext){

        var soundId: SystemSoundID = 0
        AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundId)

        AudioServicesAddSystemSoundCompletion(soundId, nil, nil, {(soundid,_) -> Void in 
        AudioServicesDisposeSystemSoundID(soundid)
        print("Sound played!")}, nil)

        AudioServicesPlaySystemSound(soundId)
    }

    do{
        try audiosession.setCategory(AVAudioSessionCategoryRecord)
        try audiosession.setMode(AVAudioSessionModeMeasurement)
        try audiosession.setActive(true, with: .notifyOthersOnDeactivation)
        print("Changing modes!")

    }catch{
        print("error with audio session")
    }

    recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

    guard let inputNode = audioEngine.inputNode else{
        fatalError("Audio engine has no input node!")
    }

    guard let recognitionRequest = recognitionRequest else{
        fatalError("Unable to create a speech audio buffer recognition request object")
    }

    recognitionRequest.shouldReportPartialResults = true

    recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, delegate: self)

    let recordingFormat = inputNode.outputFormat(forBus: 0)
    inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
        self.recognitionRequest?.append(buffer)
    }

    audioEngine.prepare()

    do{
        try audioEngine.start()
        delegate?.didStartRecording()

    }catch{
        print("audioEngine couldn't start because of an error")
    }

What happens when I run this code is that it records the voice and transcribes it successfully. However the ping is never played. The two(non-error) print statements I have in there fire in the order:

  1. Changing modes!
  2. Sound played!

So to my understanding, the reason the ping sound isn't being played is because by the time it actually completes I've already changed the audio session category from playback to record. Just to verify this is true, I tried removing everything but the sound services ping and it plays the sound as expected.

So my question is what is the best way to bypass the asynchronous nature of the AudioServicesPlaySystemSound call? I've experimented with trying to pass self into the completion function so I could have it trigger a function in my class which then runs the recording chunk. However I haven't been able to figure out how one actually goes about converting self to an UnsafeMutableRawPointer so it can be passed as clientData. Furthermore, even if I DID know how to do that, I'm not sure if it's even a good idea or the intended use of that parameter.

Alternatively, I could probably solve this problem by relying on something like notification center. But once again that just seems like a very clunky way of solving the problem that I'm going to end up regretting later.

Does anyone know what the correct way to handle this type of situation is?

Update:

As per Gruntcake's request, here is my attempt to access self in the completion block.

First I create a userData constant which is an UnsafeMutableRawPointer to self:

    var me = self
    let userData = withUnsafePointer(to: &me) { ptr in
        return unsafeBitCast(ptr, to: UnsafeMutableRawPointer.self)

Next I use that constant in my callback block, and attempt to access self from it:

    AudioServicesAddSystemSoundCompletion(soundId, nil, nil, {(sounded,me) -> Void in 
        AudioServicesDisposeSystemSoundID(sounded)
        let myself = Unmanaged<myclassname>.fromOpaque(me!).takeRetainedValue()
        myself.doOtherStuff()
        print("Sound played!")}, userData)
Bebhead
  • 209
  • 3
  • 14
  • I would love it to be that simple... However the problem I run into is that I can't simply reference self in the closure. Specifically, if I just put self.doOtherStuff() in there I get the following compiler error: 'A C function pointer cannot be formed from a closure that captures context'. The AudioServicesAddSystemSoundCompletion method's final parameter is called 'inClientData' which helpfully says 'Application data to be passed to your callback function when it is invoked' in the documentation. The type is of UnsafeMutableRawPointer, which is why I do that whole userData thing initially – Bebhead Feb 08 '17 at 18:55
  • Your attempt to call doOtherStuff() in the completion block is a correct approach (the only other one is notifications, those are the only two options) What is complicating it in this case is the bridging from Obj-C to Swift that is necessary. Is the last (unaccepted) answer here working for you http://stackoverflow.com/questions/26823405/how-do-i-implement-audioservicessystemsoundcompletionproc-in-swift – Gruntcakes Feb 08 '17 at 18:57
  • @Mungbeans, THANK YOU! That did the trick. I actually stumbled upon that same question earlier when trying to figure this out. But I hadn't even looked at that answer because of the low rating and unaccepted status... If you would like to post your comment as an answer to my question I will accept it. – Bebhead Feb 08 '17 at 19:03

1 Answers1

1

Your attempt to call doOtherStuff() in the completion block is a correct approach (the only other one is notifications, those are the only two options)

What is complicating it in this case is the bridging from Obj-C to Swift that is necessary. Code to do that is:

let myData = unsafeBitCast(self, UnsafeMutablePointer<Void>.self)
AudioServicesAddSystemSoundCompletion(YOUR_SOUND_ID, CFRunLoopGetMain(), kCFRunLoopDefaultMode,{ (mSound, mVoid) in
        let me = unsafeBitCast(mVoid, YOURCURRENTCLASS.self)
        //me it is your current object so if yo have a variable like
        // var someVar you can do
        print(me.someVar)
    }, myData)

Credit: This code was taken from an answer to this question, though it is not the accepted answer:

How do I implement AudioServicesSystemSoundCompletionProc in Swift?

Community
  • 1
  • 1
Gruntcakes
  • 37,738
  • 44
  • 184
  • 378