MyApp/SpeechPromptService.swift
3.4 KB · 90 lines · swift · Download from the repository page
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) }}