diff --git a/src/plugins/tts/AbstractTTSEngine.js b/src/plugins/tts/AbstractTTSEngine.js index 596036998..46a0a373f 100644 --- a/src/plugins/tts/AbstractTTSEngine.js +++ b/src/plugins/tts/AbstractTTSEngine.js @@ -142,6 +142,10 @@ export default class AbstractTTSEngine { // MS Edge fires voices changed randomly very often this.events.off('voiceschanged', this.updateBestVoice); this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI); + // if the current book has a language set, store the selected voice with the book language as a suffix + if (this.opts.bookLanguage) { + localStorage.setItem(`BRtts-voice-${this.opts.bookLanguage}`, this.voice.voiceURI); + } if (this.activeSound) this.activeSound.setVoice(this.voice); } @@ -221,10 +225,12 @@ export default class AbstractTTSEngine { // user languages that match the book language const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage)); - // Try to find voices that intersect these two sets - return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) || + // First try to find the last chosen voice from localStorage for the current book language + return AbstractTTSEngine.getMatchingStoredVoice(bookLangVoices, bookLanguage) + // Try to find voices that intersect these two sets + || AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) // no user languages match the books; let's return the best voice for the book language - (bookLangVoices.find(v => v.default) || bookLangVoices[0]) + || (bookLangVoices.find(v => v.default) || bookLangVoices[0]) // No voices match the book language? let's find a voice in the user's language // and ignore book lang || AbstractTTSEngine.getMatchingVoice(userLanguages, voices) @@ -232,6 +238,19 @@ export default class AbstractTTSEngine { || (voices.find(v => v.default) || voices[0]); } + /** + * @private + * Get the voice last selected by the user for the book language from localStorage. + * Returns undefined if no voice is stored or found. + * @param {SpeechSynthesisVoice[]} voices browser voices to choose from + * @param {ISO6391} bookLanguage book language to look for + * @return {SpeechSynthesisVoice | undefined} + */ + static getMatchingStoredVoice(voices, bookLanguage) { + const storedVoice = localStorage.getItem(`BRtts-voice-${bookLanguage}`); + return (storedVoice ? voices.find(v => v.voiceURI === storedVoice) : undefined); + } + /** * @private * Get the best voice that matches one of the BCP47 languages (order by preference) diff --git a/tests/jest/plugins/tts/AbstractTTSEngine.test.js b/tests/jest/plugins/tts/AbstractTTSEngine.test.js index 2eee936d3..4c26c745c 100644 --- a/tests/jest/plugins/tts/AbstractTTSEngine.test.js +++ b/tests/jest/plugins/tts/AbstractTTSEngine.test.js @@ -111,6 +111,26 @@ for (const dummyVoice of [dummyVoiceHyphens, dummyVoiceUnderscores]) { expect(getBestBookVoice(voices, 'en', ['en-CA', 'en'])).toBe(voices[0]); }); + + test('choose stored language from localStorage', () => { + const voices = [ + dummyVoice({lang: "en-US", voiceURI: "English US", default: true}), + dummyVoice({lang: "en-GB", voiceURI: "English GB"}), + dummyVoice({lang: "en-CA", voiceURI: "English CA"}), + ]; + class DummyEngine extends AbstractTTSEngine { + getVoices() { return voices; } + } + const ttsEngine = new DummyEngine({...DUMMY_TTS_ENGINE_OPTS, bookLanguage: 'en'}); + // simulates setting default voice on tts startup + ttsEngine.updateBestVoice(); + // simulates user choosing a voice that matches the bookLanguage + // voice will be stored in localStorage + ttsEngine.setVoice(voices[2].voiceURI); + + // expecting the voice to be selected by getMatchingStoredVoice and returned as best voice + expect(getBestBookVoice(voices, 'en', [])).toBe(voices[2]); + }); }); }