diff options
Diffstat (limited to 'DayByDay/SpeechSynthesizer.swift')
| -rw-r--r-- | DayByDay/SpeechSynthesizer.swift | 52 |
1 files changed, 52 insertions, 0 deletions
diff --git a/DayByDay/SpeechSynthesizer.swift b/DayByDay/SpeechSynthesizer.swift new file mode 100644 index 0000000..cac5caf --- /dev/null +++ b/DayByDay/SpeechSynthesizer.swift @@ -0,0 +1,52 @@ +// +// SpeechSynthesizer.swift +// DayByDay +// + +import AVFoundation + +/// Wrapper around AVSpeechSynthesizer that selects the highest-quality +/// en-US neural voice available on the device. Falls back gracefully if +/// premium/enhanced voices haven't been downloaded yet. +final class SpeechSynthesizer { + static let shared = SpeechSynthesizer() + + private let synthesizer = AVSpeechSynthesizer() + private let voice: AVSpeechSynthesisVoice? + + private init() { + self.voice = Self.bestAvailableVoice() + } + + func speak(_ text: String) { + if synthesizer.isSpeaking { + synthesizer.stopSpeaking(at: .immediate) + } + + let utterance = AVSpeechUtterance(string: text) + utterance.voice = voice + utterance.rate = 0.4 + utterance.pitchMultiplier = 1.0 + utterance.preUtteranceDelay = 0 + synthesizer.speak(utterance) + } + + /// Picks the best en-US voice on the device. + /// Preference order: premium > enhanced > default quality. + /// Skips novelty voices so the child always hears a natural-sounding voice. + private static func bestAvailableVoice() -> AVSpeechSynthesisVoice? { + let candidates = AVSpeechSynthesisVoice.speechVoices().filter { voice in + voice.language.hasPrefix("en-US") + && !voice.voiceTraits.contains(.isNoveltyVoice) + } + + if let premium = candidates.first(where: { $0.quality == .premium }) { + return premium + } + if let enhanced = candidates.first(where: { $0.quality == .enhanced }) { + return enhanced + } + // Fall back to the best default-quality voice. + return candidates.first ?? AVSpeechSynthesisVoice(language: "en-US") + } +} |
