0

I am trying to pronounce sentences with time intervals, but the problem is that after the synthesizer pronounces it for the first time, loop runs through straight away till the end. The utterance works well, but there are no pauses between.

How can I do that loop switches to next item only after the speech synthesizing task is finished?

EDIT: Maybe, it's possible that loop waits for didFinish each time, and then didFinish tells loop when it can continue?

let speaker = Speaker()
let capitals = ["Canberra is the capital of Australia", "Seoul is the capital of South Korea", "Tokyo is the capital of Japan", "Berlin is the capital of Germany"]

var body: some View {

    Button("Play Sound") {
        playSound()
    }    
}

func playSound() {
        for item in 0..<capitals.count {
            let timer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { timer in
                
                speaker.speak("\(capitals[item])")
                print("I am out")
            }
        }
 }

...

import AVFoundation

class Speaker: NSObject {
    let synth = AVSpeechSynthesizer()

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

    func speak(_ string: String) {
        let utterance = AVSpeechUtterance(string: string)
        utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
        utterance.rate = 0.5
        synth.speak(utterance)
    }
}

extension Speaker: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("all done")
    }
}
MikeMaus
  • 385
  • 3
  • 22
  • 1
    have you looked into Semaphores ? – Mohmmad S Feb 10 '21 at 01:34
  • Didn't know about Semaphores. Seems very very interesting!!! That's a discovery for me. I'll look for it. – MikeMaus Feb 10 '21 at 01:35
  • Another way is to use a recursive function with completion handler called from your `didFinish` function. – koropok Feb 10 '21 at 01:46
  • I wanted more control over pauses, in recursive approach as I understood it's always the same pause time. And I can't do different pauses each one (can I?) – MikeMaus Feb 10 '21 at 01:54

1 Answers1

1

You could always use Combine for this

import Combine

let speaker = Speaker()
let capitals = ["Canberra is the capital of Australia", "Seoul is the capital of South Korea", "Tokyo is the capital of Japan", "Berlin is the capital of Germany"]
var playerCancellable: AnyCancellable? = nil

 Button("Play Sound") {
     playSound()
 }    

func playSound() {
     // Fairly standard timer publisher. The call to .autoconnect() tells the timer to start publishing when subscribed to.
     let timer = Timer.publish(every: 20, on: .main, in: .default)
         .autoconnect()
    
    // Publishers.Zip takes two publishers. 
    // It will only publish when there is a "symmetrical" output. It behaves in a similar manner as `zip` on sequences.
    // So, in this scenario, you will not get the next element of your array until the timer emits another event.
    // In the call to sink, we ignore the first element of the tuple relating to the timer
    playerCancellable = Publishers.Zip(timer, capitals.publisher)
         .sink { _, item in
             speaker.speak(item)
         }
 }

Edit

You mentioned in the comments that you want to be able to variably control the delay between utterances. That's not really something a Timer can be used for. I hacked around a bit because I found it to be an interesting problem and was able to make this work as you describe that you want in the comments:

class Speaker: NSObject {
  let synth = AVSpeechSynthesizer()

  private var timedPhrases: [(phrase: String, delay: TimeInterval)]
  // This is so you don't potentially block the main queue
  private let queue = DispatchQueue(label: "Phrase Queue")
  
  override init() {
    timed = []
    super.init()
    synth.delegate = self
  }
  
  init(_ timedPhrases: [(phrase: String, delay: TimeInterval)]) {
    self.timedPhrases = timedPhrases
    super.init()
    synth.delegate = self
  }
  
  private func speak(_ string: String) {
    let utterance = AVSpeechUtterance(string: string)
    utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
    utterance.rate = 0.5
    synth.speak(utterance)
  }
  
  func speak() {
    guard let first = timed.first else { return }
    speak(first.value)
    timed = Array(timed.dropFirst())
  }
}

extension Speaker: AVSpeechSynthesizerDelegate {
  func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
    if !timed.isEmpty {
      queue.sync {
        Thread.sleep(forTimeInterval: TimeInterval(timed.first!.delay))
        self.speak()
      }
    } else {
      print("all done")
    }
  }
}

let speaker = let speaker = Speaker([
    (phrase: "1", delay: 0),
    (phrase: "2", delay: 3),
    (phrase: "3", delay: 1),
    (phrase: "4", delay: 5),
    (phrase: "5", delay: 10)
])

speaker.speak()

Take this with a huge grain of salt. I don't really consider using Thread.sleep to be a very good practice, but maybe this will give you some ideas on how to approach it. If you want variable timing, a Timer instance is not going to give you that.

Steven0351
  • 439
  • 7
  • 9
  • Can you add more context of what's happening there – Mohmmad S Feb 10 '21 at 01:59
  • Thanks for response! I have a mistake `Expected expression in list of expressions` on the `timer`'s line – MikeMaus Feb 10 '21 at 02:07
  • @MikeMaus, I missed a leading . for the `in: default` part, it should be `in: .default`. I'll update – Steven0351 Feb 10 '21 at 02:24
  • @Steven0351it's also showing something with mutable/ mutating func (asks to switch to - `mutating func`, but then shows error in function call in `Button`) does it compiles for you? – MikeMaus Feb 10 '21 at 02:33
  • 1
    You could make `var playerCancellable: AnyCancellable? = nil` a private `@State` var or something. I ran the code in a playground, I didn't wrap it in a SwiftUI view – Steven0351 Feb 10 '21 at 02:35
  • @Steven0351do you know maybe how to make timer more controllable? So, that I can adjust different time for each pause? – MikeMaus Feb 10 '21 at 03:04
  • 1
    Do you mean like having the Timer fire at different intervals depending on the content to be read? – Steven0351 Feb 10 '21 at 03:17
  • @Steven0351 exactly what I intended! Great usage of array of tuples (discovery for me)! I also discovered how to name the queues from your code. If you'd like to see other questions I asked. Before I saw your response... similar one - https://stackoverflow.com/q/66144518/11419259 ; how highlight speech utterance - https://stackoverflow.com/a/66140583/11419259 . Now I also started to work how I can pause and jump forward/ backward between sentences. – MikeMaus Feb 11 '21 at 04:19