Available Voice Types
Neural Voices (ONNX/Piper)
High-quality neural voices that run on-device:| Voice ID | Language | Gender | Quality | Size |
|---|---|---|---|---|
piper-en-us-amy | English | Female | High | ~60 MB |
piper-en-us-danny | English | Male | High | ~60 MB |
piper-en-gb-alan | English | Male | High | ~60 MB |
piper-de-thorsten | German | Male | High | ~60 MB |
piper-es-mls | Spanish | Mixed | High | ~60 MB |
piper-fr-mls | French | Mixed | High | ~60 MB |
System Voices
Built-in iOS/macOS voices via AVSpeechSynthesizer:| Voice ID | Platform |
|---|---|
com.apple.ttsbundle.siri_female_en-US_compact | iOS/macOS |
com.apple.ttsbundle.siri_male_en-US_compact | iOS/macOS |
com.apple.ttsbundle.Samantha-compact | iOS/macOS |
com.apple.voice.enhanced.en-US.Samantha | macOS |
Loading Voices
Load a Neural Voice
try await RunAnywhere.loadTTSVoice("piper-en-us-amy")
List Available Voices
let voices = await RunAnywhere.availableTTSVoices
for voice in voices {
print(voice)
}
Check Current Voice
if let currentVoice = await RunAnywhere.currentTTSVoiceId {
print("Current voice: \(currentVoice)")
}
Unload Voice
try await RunAnywhere.unloadTTSVoice()
Voice Selection
By Language
func loadVoiceForLanguage(_ language: String) async throws {
let voiceMap: [String: String] = [
"en": "piper-en-us-amy",
"de": "piper-de-thorsten",
"es": "piper-es-mls",
"fr": "piper-fr-mls"
]
if let voiceId = voiceMap[language] {
try await RunAnywhere.loadTTSVoice(voiceId)
} else {
// Fall back to system voice
try await RunAnywhere.loadTTSVoice("com.apple.ttsbundle.siri_female_en-US_compact")
}
}
Voice Picker UI
struct VoicePickerView: View {
@State private var selectedVoice = "piper-en-us-amy"
@State private var availableVoices: [String] = []
@State private var isLoading = false
let voiceNames: [String: String] = [
"piper-en-us-amy": "Amy (English, Female)",
"piper-en-us-danny": "Danny (English, Male)",
"piper-en-gb-alan": "Alan (British, Male)",
"piper-de-thorsten": "Thorsten (German, Male)",
"piper-es-mls": "Spanish Neural",
"piper-fr-mls": "French Neural"
]
var body: some View {
VStack(spacing: 20) {
Text("Select Voice")
.font(.headline)
Picker("Voice", selection: $selectedVoice) {
ForEach(availableVoices, id: \.self) { voice in
Text(voiceNames[voice] ?? voice)
.tag(voice)
}
}
.pickerStyle(.wheel)
Button(action: loadSelectedVoice) {
HStack {
if isLoading {
ProgressView()
.scaleEffect(0.8)
}
Text(isLoading ? "Loading..." : "Apply")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(isLoading)
Button("Preview") {
Task {
try? await RunAnywhere.speak("Hello! This is a preview of the selected voice.")
}
}
.disabled(isLoading)
}
.padding()
.task {
availableVoices = await RunAnywhere.availableTTSVoices
}
}
func loadSelectedVoice() {
isLoading = true
Task {
do {
try await RunAnywhere.loadTTSVoice(selectedVoice)
} catch {
print("Failed to load voice: \(error)")
}
await MainActor.run {
isLoading = false
}
}
}
}
Voice Comparison
Neural vs System Voices
| Feature | Neural (Piper) | System (AVSpeech) |
|---|---|---|
| Quality | Higher | Good |
| Latency | Slightly higher | Lower |
| Offline | Yes | Yes |
| Size | ~60 MB each | Built-in |
| Customization | Rate, pitch | Rate, pitch, volume |
| Languages | Limited selection | Many languages |
Quality Tiers
enum VoiceQuality {
case high // Neural voices
case standard // Enhanced system voices
case compact // Compact system voices
}
func recommendVoice(for quality: VoiceQuality, language: String) -> String {
switch quality {
case .high:
return "piper-\(language)-*" // Neural voice
case .standard:
return "com.apple.voice.enhanced.\(language)-*"
case .compact:
return "com.apple.ttsbundle.*-compact"
}
}
Custom Voice Configuration
Per-Voice Settings
struct VoiceSettings {
let voiceId: String
var rate: Float = 1.0
var pitch: Float = 1.0
var volume: Float = 1.0
}
class VoiceManager {
private var settings: [String: VoiceSettings] = [
"piper-en-us-amy": VoiceSettings(voiceId: "piper-en-us-amy", rate: 0.95, pitch: 1.0),
"piper-en-us-danny": VoiceSettings(voiceId: "piper-en-us-danny", rate: 1.0, pitch: 0.95)
]
func speak(_ text: String) async throws {
let currentVoice = await RunAnywhere.currentTTSVoiceId ?? "piper-en-us-amy"
let voiceSettings = settings[currentVoice] ?? VoiceSettings(voiceId: currentVoice)
try await RunAnywhere.speak(
text,
options: TTSOptions(
rate: voiceSettings.rate,
pitch: voiceSettings.pitch,
volume: voiceSettings.volume
)
)
}
}
Downloading Voices
Neural voices need to be downloaded before first use:// Check if voice is downloaded
let models = try await RunAnywhere.availableModels()
let voiceModel = models.first { $0.id == "piper-en-us-amy" }
if let model = voiceModel, !model.isDownloaded {
// Download the voice
let task = try await Download.shared.downloadModel(model)
for await progress in task.progress {
print("Downloading: \(Int(progress.overallProgress * 100))%")
}
}
// Now load the voice
try await RunAnywhere.loadTTSVoice("piper-en-us-amy")
Multi-Voice Support
Switch between voices for different speakers:class MultiVoiceNarrator {
let voiceAssignments: [String: String] = [
"narrator": "piper-en-us-amy",
"character1": "piper-en-us-danny",
"character2": "piper-en-gb-alan"
]
func speak(as role: String, text: String) async throws {
guard let voiceId = voiceAssignments[role] else { return }
let currentVoice = await RunAnywhere.currentTTSVoiceId
// Load voice if different
if currentVoice != voiceId {
try await RunAnywhere.loadTTSVoice(voiceId)
}
try await RunAnywhere.speak(text)
}
}
// Usage
let narrator = MultiVoiceNarrator()
try await narrator.speak(as: "narrator", text: "Once upon a time...")
try await narrator.speak(as: "character1", text: "Hello, friend!")
try await narrator.speak(as: "character2", text: "Greetings!")
Best Practices
Preload voices
Preload voices
Download and load voices at app startup or in onboarding to avoid delays.
Cache voice selection
Cache voice selection
Save user’s preferred voice to UserDefaults and restore on app launch.
Fallback strategy
Fallback strategy
Always have a fallback to system voices if neural voices fail to load.
synthesize()
Basic synthesis →
Streaming TTS
Stream long text →