5

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()

Community
  • 1
  • 1
benglish
  • 153
  • 1
  • 4
  • Option 3 is probably the best. You need to make sure that your closure retrieves the next bit of text and speaks it. If you simply submit all of the utterances at once as you are now it won't work – Paulw11 May 17 '16 at 22:47

1 Answers1

5

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.")])
Community
  • 1
  • 1
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • Thank you! I just tested this and it is working as described. Can you explain to me why a blocking loop didn't work? – benglish May 17 '16 at 23:22
  • 1
    You never want to run on the main thread for very long, so blocking with `sleep` is a very bad idea. If you did this, your app UI would freeze the whole time you were speaking a sentence. `delay` works by scheduling the code in `{}` to run on the main thread at a later time. This allows `speak` to return and other threads to run and for your UI to remain responsive. Then after waiting the delay, the closure (the code in `{}`) runs on the main thread and does another little chunk of speech before scheduling the next. – vacawama May 17 '16 at 23:28
  • You got it! Thanks for your prompt response and explanation!. Go Cardinal! – benglish May 18 '16 at 01:12