Search code examples
iosswiftavspeechsynthesizer

How to know when an AVSpeechUtterance has finished, so as to continue app activity?


When an AVSpeechUtterance is speaking, I want to wait until it's finished before doing something else.

There is a property of AVSpeechSynthesizer that's seemingly indicative of when speech is occuring:

isSpeaking

As stupid and simple as this question might sound, I'm wondering how do I use/check this property to wait until speech has concluded before going on?

ALTERNATIVELY:

There's a delegate, that I'm also clueless as to how to use, which has an ability to do something when an utterance has finished:

AVSpeechSynthesizerDelegate

There's an answer, here, that says to use this. But that doesn't help me because I don't know how to use a Delegate.

UPDATE:

This is how I setup my speaking class:

import AVFoundation

class CanSpeak: NSObject, AVSpeechSynthesizerDelegate {

    let voices = AVSpeechSynthesisVoice.speechVoices()
    let voiceSynth = AVSpeechSynthesizer()
    var voiceToUse: AVSpeechSynthesisVoice?

    override init(){
        voiceToUse = AVSpeechSynthesisVoice.speechVoices().filter({ $0.name == "Karen" }).first
    }

    func sayThis(_ phrase: String){
        let utterance = AVSpeechUtterance(string: phrase)
        utterance.voice = voiceToUse
        utterance.rate = 0.5
        voiceSynth.speak(utterance)
    }
}

UPDATE 2: Wrongheaded workaround...

using the above mentioned isSpeaking property, in the gameScene:

voice.sayThis(targetsToSay)

    let initialPause = SKAction.wait(forDuration: 1.0)
    let holdWhileSpeaking = SKAction.run {
        while self.voice.voiceSynth.isSpeaking {print("STILL SPEAKING!")}
    }
    let pauseAfterSpeaking = SKAction.wait(forDuration: 0.5)
    let doneSpeaking = SKAction.run {print("TIME TO GET ON WITH IT!!!")}

run(SKAction.sequence(
    [   initialPause,
        holdWhileSpeaking,
        pauseAfterSpeaking,
        doneSpeaking
    ]))

Solution

  • Delegate pattern is one of the most common used design pattern in object-oriented programming, it's not as hard as it seems. For your case, you can simply let your class (a game scene) to be a delegate of the CanSpeak class.

    protocol CanSpeakDelegate {
       func speechDidFinish()
    }
    

    Next set AVSpeechSynthesizerDelegate to your CanSpeak class, declare CanSpeakDelegate and then use AVSpeechSynthesizerDelegate delegate function.

    class CanSpeak: NSObject, AVSpeechSynthesizerDelegate {
    
       let voices = AVSpeechSynthesisVoice.speechVoices()
       let voiceSynth = AVSpeechSynthesizer()
       var voiceToUse: AVSpeechSynthesisVoice?
    
       var delegate: CanSpeakDelegate!
    
       override init(){
          voiceToUse = AVSpeechSynthesisVoice.speechVoices().filter({ $0.name == "Karen" }).first
          self.voiceSynth.delegate = self
       }
    
       func sayThis(_ phrase: String){
          let utterance = AVSpeechUtterance(string: phrase)
          utterance.voice = voiceToUse
          utterance.rate = 0.5
          voiceSynth.speak(utterance)
       }
    
       func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
          self.delegate.speechDidFinish()
       }
    }
    

    Lastly in your game scene class, simply conform to CanSpeakDelegate and set it as the delegate of your CanSpeak class.

    class GameScene: NSObject, CanSpeakDelegate {
    
       let canSpeak = CanSpeak()
    
       override init() {
          self.canSpeak.delegate = self
       }
    
       // This function will be called every time a speech finishes
       func speechDidFinish() {
          // Do something
       }
    }