From 84f11558d63e14388e658da35f84a174b94c6f86 Mon Sep 17 00:00:00 2001 From: Daniel Grossfeld Date: Sun, 12 Apr 2026 21:53:47 +0200 Subject: [PATCH] fix: voice selection on iOS/Mac using Web Speech API Two bugs prevented the selected voice from being used on iOS and macOS: 1. sayWithVoice matched voices by `name` but the stored setting value is `voiceURI`. On iOS/macOS, these differ (e.g. Siri voices have a URI like `com.apple.voice.compact.en-US.Samantha` vs display name "Samantha"). Fixed by matching on `voiceURI` instead. 2. `getVoices()` was called synchronously. On iOS/Safari, the Web Speech API loads voices asynchronously and `getVoices()` returns an empty array until the `voiceschanged` event fires. Added a `loadVoices()` helper that returns immediately if voices are already available, otherwise waits for the event. Fixes #42, fixes #20 --- src/services/SpeechSynthesis.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/services/SpeechSynthesis.ts b/src/services/SpeechSynthesis.ts index 32bb1f4..5ff2073 100644 --- a/src/services/SpeechSynthesis.ts +++ b/src/services/SpeechSynthesis.ts @@ -42,15 +42,25 @@ export class SpeechSynthesis implements TTSService { return !Platform.isAndroidApp; } - async getVoices(): Promise<{id: string, name: string, languages: string[]}[]> { + private async loadVoices(): Promise { const voices = window.speechSynthesis.getVoices(); + if (voices.length > 0) return voices; + return new Promise(resolve => { + window.speechSynthesis.addEventListener('voiceschanged', () => { + resolve(window.speechSynthesis.getVoices()); + }, {once: true}); + }); + } + + async getVoices(): Promise<{id: string, name: string, languages: string[]}[]> { + const voices = await this.loadVoices(); return voices.map(voice => { return { id: voice.voiceURI, name: voice.name, languages: [voice.lang] }; - }) + }); } async sayWithVoice(text: string, voice: string): Promise { @@ -59,7 +69,8 @@ export class SpeechSynthesis implements TTSService { msg.volume = this.plugin.settings.volume; msg.rate = this.plugin.settings.rate; msg.pitch = this.plugin.settings.pitch; - msg.voice = window.speechSynthesis.getVoices().filter(otherVoice => otherVoice.name === voice)[0]; + const voices = await this.loadVoices(); + msg.voice = voices.filter(otherVoice => otherVoice.voiceURI === voice)[0]; window.speechSynthesis.speak(msg); this.plugin.statusbar.createSpan({text: 'Speaking'}); }