14

I just don't know how to do it...

I search here and at google and people talked about the AVSpeechSynthesizerDelegate but I wasn't able to use it.

I want to run a function exactly when the speech is over.

How can I achieve this? If I must use the delegate, how should I do it?

I tried that way:

func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance) {
    falando = false
    print("FINISHED")
}

This was one of the functions I found on the developer's doc, although the speech was told and nothing was printed.

I tried to put Class A : AVSpeechSynthesizerDelegate so then I would do Speech.delegate = self (Speech is an attribute of A, of type AVSpeechSynthesizer) but it said A does not conform to protocol NSObjectProtocol.

How can I run some function (even a print) as soon as the speech is over?

Thank you!

0

5 Answers 5

35

A does not conform to protocol NSObjectProtocol means that your class must inherit from NSObject, you can read more about it here.

Now I don't know how you've structured your code, but this little example seems to work for me. First a dead simple class that holds the AVSpeechSynthesizer:

class Speaker: NSObject {
    let synth = AVSpeechSynthesizer()

    override init() {
        super.init()
        synth.delegate = self
    }

    func speak(_ string: String) {
        let utterance = AVSpeechUtterance(string: string)
        synth.speakUtterance(utterance)
    }
}

Notice that I set the delegate here (in the init method) and notice that it must inherit from NSObject to keep the compiler happy (very important!)

And then the actual delegate method:

extension Speaker: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("all done")
    }
}

And finally, I can use that class here, like so:

class ViewController: UIViewController {
    let speaker = Speaker()

    @IBAction func buttonTapped(sender: UIButton) {
        speaker.speak("Hello world")
    }
}

Which rewards me with

all done

in my console when the AVSpeechSynthesizer has stopped speaking.

Hope that helps you.

Update

So, time passes and in the comments below @case-silva asked if there was a practical example and @dima-gershman suggested to just use the AVSpeectSynthesizer directly in the ViewController.

To accommodate both, I've made a simple ViewController example here with a UITextField and a UIButton.

The flow is:

  1. You enter some text in the textfield (if not, a default value will be set)
  2. You press the button
  3. The button is disabled and the background color is changed (sorry, it was the best I could come up with :))
  4. Once speech is done, the button is enabled, the textfield is cleared and the background color is changed again.

Here's how it looks

A Simple UIViewController Example

import UIKit
import AVFoundation

class ViewController: UIViewController {

    //MARK: Outlets
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var speakButton: UIButton!

    let synth = AVSpeechSynthesizer()

    override func viewDidLoad() {
        super.viewDidLoad()
        synth.delegate = self
    }

    @IBAction func speakButtonTapped(_ sender: UIButton) {
        //We're ready to start speaking, disable UI while we're speaking
        view.backgroundColor = .darkGray
        speakButton.isEnabled = false
        let inputText = textField.text ?? ""
        let textToSpeak = inputText.isEmpty ? "Please enter some text" : inputText

        let speakUtterance = AVSpeechUtterance(string: textToSpeak)
        synth.speak(speakUtterance)
    }
}

extension ViewController: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        //Speaking is done, enable speech UI for next round
        speakButton.isEnabled = true
        view.backgroundColor = .lightGray
        textField.text = ""
    }
}

Hope that gives you a clue Case.

5
  • @pbodsk This class Speaker works correctly, but I assume if people want to detect when speech is finished - they would like to do something useful about it. So I'd suggest either providing a placeholder for some callback block from Speaker OR just offer to implement AVSpeechSynthesizerDelegate directly in the class that needs to speak (like your ViewController).
    – Dima G
    Commented Feb 3, 2019 at 11:03
  • @DimaGershman I am in a situation right now where I do need to perform a meaningful action in ViewController when the speech from Speaker is finished. Could you possibly give an example of utilizing a callback block from speaker to do so?
    – Case Silva
    Commented May 3, 2019 at 17:33
  • Yep, now with the updated answer you can see an example for how to use AVSpeechSynthesizer and do meaningful action thru the delegate. (thanks @pbodsk for updating it!)
    – Dima G
    Commented May 4, 2019 at 19:18
  • Thank you this is the best explanation of the did finish delegate Commented May 12, 2020 at 22:43
  • for me, my delegate methods are not being called. I am not running any graphical UI.
    – morpheus
    Commented yesterday
8

SwiftUI

Create a Speaker class that inherits from NSObject and ObservableObject.

internal class Speaker: NSObject, ObservableObject {
    internal var errorDescription: String? = nil
    private let synthesizer: AVSpeechSynthesizer = AVSpeechSynthesizer()
    @Published var isSpeaking: Bool = false
    @Published var isShowingSpeakingErrorAlert: Bool = false

    override init() {
        super.init()
        self.synthesizer.delegate = self
    }

    internal func speak(_ text: String, language: String) {
        do {
            let utterance = AVSpeechUtterance(string: text)
            utterance.voice = AVSpeechSynthesisVoice(language: language)
            
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            self.synthesizer.speak(utterance)
        } catch let error {
            self.errorDescription = error.localizedDescription
            isShowingSpeakingErrorAlert.toggle()
        }
    }
    
    internal func stop() {
        self.synthesizer.stopSpeaking(at: .immediate)
    }
}

You'll have an error `Cannot assign value to type 'Speaker' to type 'any AvSpeechSynthersizerDelegate)?'

Extend the Speaker class and implement the necessary delegate methods. The error will then go way.

extension Speaker: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
        self.isSpeaking = true
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
        self.isSpeaking = false
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        self.isSpeaking = false
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
    }
}

Add Speaker to the necessary view using the StateObject wrapper.

struct ContentView: View {
    let text: String = "Hello World!"
    @StateObject var speaker: Speaker = Speaker()
    
    var body: some View {
        HStack {
            Text(text)
            Spacer()
            Button(action: {
                if self.speaker.isSpeaking {
                    speaker.stop()
                } else {
                    speaker.speak(text, language: "en-US")
                }
            }) {
                Image(systemName: self.speaker.isSpeaking ? "stop.circle" : "speaker.wave.2.circle")
                    .resizable()
                    .frame(width: 30, height: 30)
            }
            .buttonStyle(BorderlessButtonStyle())
            .alert(isPresented: $speaker.isShowingSpeakingErrorAlert) {
                Alert(title: Text("Pronunciation error", comment: "Pronunciation error alert title."), message: Text(speaker.errorDescription ?? ""))
            }
        }
        .padding()
    }
}
1
  • I'm new to swift, the internal declaration, I got an error when assigning self to the self.synthisizer.delegate, this error went away when I add the declaration for the extension extension Speaker: AVSpeechSynthesizerDelegate part Commented Mar 24 at 11:55
2

The function viewDidAppear is used just as example, same code can be placed anywhere as required:

class MyViewController: UIViewController, AVSpeechSynthesizerDelegate {

    var synth = AVSpeechSynthesizer()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // make sure to set the delegate before "speaking"
        synth.delegate = self

        let utterance = AVSpeechUtterance(string: "Hello world!")
        synth.speak(utterance)
    }

    // will be called when speech did finish
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        // do something useful here ...
    }
}
0

I make app and found this solution for me.

func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
                           willSpeakRangeOfSpeechString characterRange: NSRange,
                           utterance: AVSpeechUtterance) {
        progress = Float(characterRange.location + characterRange.length)
                       / Float(utterance.speechString.count)
        speechCount = Float(utterance.speechString.count)

        self.soundSliderView.setValue(progress, animated: true)
}

You can use when finish "progress value"

func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        if progress >= 0.95 {
            // finis playing with end speaking
            } else {
            // finis playing with stopping
        }
 }
0
// import AVFoundation
import AVFoundation
// Adhere to AVSpeechSynthesizerDelegate
class GetDirectionsViewController: UIViewController, AVSpeechSynthesizerDelegate {
// define a speech session
let speechsynthesizer = AVSpeechSynthesizer()
// set the delegate
speechsynthesizer.delegate = self
// Implement didFinish. 
// This is called when speechsynthesizer completes speech
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("Speech finished")
    }
// Done!

Not the answer you're looking for? Browse other questions tagged or ask your own question.