Swift iOS Text To Speech not working with “delay”

2020-04-08 14:26发布

问题:

I'm trying to have the iOS text-to-speech synthesizer "say" a list of phrases with a variable delay between the phrases. For example, I may want to it say "Hello", then wait 5 seconds, then "Is anyone there?", then wait 10 seconds, then say "Hello?"...etc.

I've made a simple example below that illustrates what I am trying to do. I know that the speech synthesizer is speaking, additional utterances are added to a queue and spoken in the order they are received.

I've tried many ways to achieve this delay in the loop. Testing delays with a print statement confirms they are working, but they seem to be interfering with the text-speach-functionality which says the first phrase but waits until the for-loop is done before saying the rest. I thought that any of these types of delays would work as I assume the speech synthesizer is event driven.

I'd appreciate some help, or at least an insight it to why it isn't working. Thanks!

Here is the example code: iPhone 6 simulator, Xcode 7.3

import UIKit
import AVFoundation

class ViewController: UIViewController {

    let speechSynthesizer = AVSpeechSynthesizer()
    var phraseArray: [String] = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]

    override func viewDidLoad() {
        super.viewDidLoad()
        for phrase in phraseArray{
            let speechUtterance = AVSpeechUtterance(string: phrase)
            speechSynthesizer.speakUtterance(speechUtterance)

            //"delay()" goes here.  It needs to be a variable length delay.

        }
    }
}

Here are some of the delay methods that I have tried:

  1. Setup the class as a delegate for the speech synthesizer and run a while loop until the synthesizer is finished.

  2. Time based delay: referenceDate = NSDate() while(NSDate().timeIntervalSinceDate(referenceDate) < 0.5) {}

  3. I've tried "delay" solutions from stack, like this one: Swift delay in loop

    func delay(delay:Double, closure:()->()) { dispatch_after( dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)) ), dispatch_get_main_queue(), closure) }

  4. Sleep()

回答1:

How about something like this:

import UIKit
import AVFoundation

func delay(_ delay:Double, closure:@escaping ()->()) {
    let when = DispatchTime.now() + delay
    DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}

class ViewController: UIViewController {

    let speechSynthesizer = AVSpeechSynthesizer()

    override func viewDidLoad() {
        super.viewDidLoad()

        speak([("Hello", 5.0), ("Is there anyone there?", 10.0), ("Hello?", 0.0)])
    }

    func speak(_ phrases: [(phrase: String, wait: Double)]) {
        if let (phrase, wait) = phrases.first {
            let speechUtterance = AVSpeechUtterance(string: phrase)
            speechSynthesizer.speak(speechUtterance)
            let rest = Array(phrases.dropFirst())
            if !rest.isEmpty {
                delay(wait) {
                    self.speak(rest)
                }
            }
        }
    }    
}

Notes:

  • An array of tuples is passed to speak. A tuple pair contains a phrase to speak and a delay to wait before the next phrase is spoken.
  • speak takes the first item from the array, speaks the phrase and passes the rest of the array (if not empty) to speak again after waiting for the delay.
  • delay was written by @matt and comes from here.

Since the last delay does nothing useful, you can turn it around and have the first phrase spoken after a delay.

func speak(_ phrases: [(wait: Double, phrase: String)]) {
    if let (wait, phrase) = phrases.first {
        delay(wait) {
            let speechUtterance = AVSpeechUtterance(string: phrase)
            self.speechSynthesizer.speak(speechUtterance)
            let rest = Array(phrases.dropFirst())
            if !rest.isEmpty {
                self.speak(rest)
            }
        }
    }
}

You would use this one like this:

// Wait 5 seconds before starting...
speak([(5.0, "I'm sorry Dave."), (2.0, "I can't do that.")])