blob: cac5caf658c98a81d4397ac36b60f4298982b675 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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")
}
}
|