Skip to main content
The RunAnywhere SDK supports multiple voice sources: neural ONNX voices (Piper) and system voices via AVSpeechSynthesizer.

Available Voice Types

Neural Voices (ONNX/Piper)

High-quality neural voices that run on-device:
Voice IDLanguageGenderQualitySize
piper-en-us-amyEnglishFemaleHigh~60 MB
piper-en-us-dannyEnglishMaleHigh~60 MB
piper-en-gb-alanEnglishMaleHigh~60 MB
piper-de-thorstenGermanMaleHigh~60 MB
piper-es-mlsSpanishMixedHigh~60 MB
piper-fr-mlsFrenchMixedHigh~60 MB

System Voices

Built-in iOS/macOS voices via AVSpeechSynthesizer:
Voice IDPlatform
com.apple.ttsbundle.siri_female_en-US_compactiOS/macOS
com.apple.ttsbundle.siri_male_en-US_compactiOS/macOS
com.apple.ttsbundle.Samantha-compactiOS/macOS
com.apple.voice.enhanced.en-US.SamanthamacOS

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

FeatureNeural (Piper)System (AVSpeech)
QualityHigherGood
LatencySlightly higherLower
OfflineYesYes
Size~60 MB eachBuilt-in
CustomizationRate, pitchRate, pitch, volume
LanguagesLimited selectionMany 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

Download and load voices at app startup or in onboarding to avoid delays.
Save user’s preferred voice to UserDefaults and restore on app launch.
Always have a fallback to system voices if neural voices fail to load.