Skip to content
RPRichard PruittTech & Cyber
Navigation
repo://simon-says-duo/MyApp/SpeechPromptService.swift

MyApp/SpeechPromptService.swift

3.4 KB · 90 lines · swift · Download from the repository page

MyApp/SpeechPromptService.swift
import AVFoundationimport Synchronization private final class SpeechAssetSink: Sendable {    private struct State {        var file: AVAudioFile?        var continuation: CheckedContinuation<Void, Error>?    }    private let state: Mutex<State>    let url: URL    init(url: URL, continuation: CheckedContinuation<Void, Error>) {        self.url = url        state = Mutex(State(continuation: continuation))    }    func receive(_ buffer: AVAudioBuffer) {        state.withLock { state in            guard let continuation = state.continuation, let pcm = buffer as? AVAudioPCMBuffer else { return }            do {                if pcm.frameLength == 0 {                    state.file = nil; state.continuation = nil                    continuation.resume()                } else {                    if state.file == nil { state.file = try AVAudioFile(forWriting: url, settings: pcm.format.settings) }                    try state.file?.write(from: pcm)                }            } catch {                state.file = nil; state.continuation = nil                continuation.resume(throwing: error)            }        }    }    func cancel() {        state.withLock { state in            state.file = nil            state.continuation?.resume(throwing: CancellationError())            state.continuation = nil        }    }} @MainActorfinal class SpeechPromptService: NSObject, AVAudioPlayerDelegate {    private let synthesizer = AVSpeechSynthesizer()    private var player: AVAudioPlayer?    private var sink: SpeechAssetSink?    var rate: Float = AVSpeechUtteranceDefaultSpeechRate    var voiceID: String?    private(set) var playbackFinished: Bool?    private(set) var playbackError: String?     func prepare(_ text: String, to url: URL) async throws {        let utterance = AVSpeechUtterance(string: text)        utterance.voice = voiceID.flatMap(AVSpeechSynthesisVoice.init(identifier:)) ?? AVSpeechSynthesisVoice(language: "en-US")        utterance.rate = rate        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in            let sink = SpeechAssetSink(url: url, continuation: continuation)            self.sink = sink            synthesizer.write(utterance) { buffer in sink.receive(buffer) }        }        try Task.checkCancellation()        sink = nil        player = try AVAudioPlayer(contentsOf: url)        player?.delegate = self        playbackFinished = nil; playbackError = nil        guard player?.prepareToPlay() == true else { throw PartyError.message("Speech preparation failed.") }    }    func play() throws {        guard player?.play() == true else { throw PartyError.message("Speech playback failed.") }    }    var isPlaying: Bool { player?.isPlaying == true }    var duration: Double { player?.duration ?? 0 }    func invalidatePreparedAudio() {        stop()        player = nil    }    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {        playbackFinished = flag    }    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {        playbackError = error?.localizedDescription ?? "Speech playback failed."        playbackFinished = false    }    func stop() {        playbackFinished = false        sink?.cancel(); sink = nil        player?.stop()        synthesizer.stopSpeaking(at: .immediate)    }}