From 54d4c65854289de9d454198d7575d3db824cf0f1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 12 Oct 2019 23:04:32 -0400 Subject: Rename audioGetFromSources's createAudioObject argument to download --- ext/bg/js/audio.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ext/bg/js/audio.js') diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 9e0ae67c..1a626d42 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -163,7 +163,7 @@ async function audioInject(definition, fields, sources, optionsContext) { audioSourceDefinition = definition.expressions[0]; } - const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, false); + const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, true); if (url !== null) { const filename = audioBuildFilename(audioSourceDefinition); if (filename !== null) { -- cgit v1.2.3 From 69b28571bdf8fb4c13198223e8a5668cf490840c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 12 Oct 2019 23:09:58 -0400 Subject: audioBuildUrl => audioGetUrl and simplify --- ext/bg/js/api.js | 2 +- ext/bg/js/audio.js | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) (limited to 'ext/bg/js/audio.js') diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 94a70c34..9fefadca 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -176,7 +176,7 @@ apiCommandExec.handlers = { }; async function apiAudioGetUrl(definition, source, optionsContext) { - return audioBuildUrl(definition, source, optionsContext); + return audioGetUrl(definition, source, optionsContext); } async function apiInjectScreenshot(definition, fields, screenshot) { diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 1a626d42..9508abf0 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -93,20 +93,14 @@ const audioUrlBuilders = { } }; -async function audioBuildUrl(definition, mode, optionsContext, cache={}) { - const cacheKey = `${mode}:${definition.expression}`; - if (cache.hasOwnProperty(cacheKey)) { - return Promise.resolve(cache[cacheKey]); - } - +async function audioGetUrl(definition, mode, optionsContext, download) { if (audioUrlBuilders.hasOwnProperty(mode)) { const handler = audioUrlBuilders[mode]; - return handler(definition, optionsContext).then( - (url) => { - cache[cacheKey] = url; - return url; - }, - () => null); + try { + return await handler(definition, optionsContext, download); + } catch (e) { + // NOP + } } return null; } -- cgit v1.2.3 From 7bae3824e74461fbd5c9f66f921b05a47a40cbf4 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 12 Oct 2019 23:59:21 -0400 Subject: Add support for text-to-speech playback --- ext/bg/js/audio.js | 18 ++++++++ ext/bg/settings.html | 4 +- ext/mixed/js/audio.js | 113 ++++++++++++++++++++++++++++++++++++++++++++++-- ext/mixed/js/display.js | 1 + 4 files changed, 132 insertions(+), 4 deletions(-) (limited to 'ext/bg/js/audio.js') diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 9508abf0..3efcce46 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -86,6 +86,24 @@ const audioUrlBuilders = { throw new Error('Failed to find audio URL'); }, + 'text-to-speech': async (definition, optionsContext) => { + const options = await apiOptionsGet(optionsContext); + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + }, + 'text-to-speech-reading': async (definition, optionsContext) => { + const options = await apiOptionsGet(optionsContext); + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + }, 'custom': async (definition, optionsContext) => { const options = await apiOptionsGet(optionsContext); const customSourceUrl = options.audio.customSourceUrl; diff --git a/ext/bg/settings.html b/ext/bg/settings.html index ffa5533e..15425b44 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -319,8 +319,10 @@
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js index 50bd321f..cf8b8d24 100644 --- a/ext/mixed/js/audio.js +++ b/ext/mixed/js/audio.js @@ -17,7 +17,90 @@ */ -function audioGetFromUrl(url) { +class TextToSpeechAudio { + constructor(text, voice) { + this.text = text; + this.voice = voice; + this._utterance = null; + this._volume = 1; + } + + get currentTime() { + return 0; + } + set currentTime(value) { + // NOP + } + + get volume() { + return this._volume; + } + set volume(value) { + this._volume = value; + if (this._utterance !== null) { + this._utterance.volume = value; + } + } + + play() { + try { + if (this._utterance === null) { + this._utterance = new SpeechSynthesisUtterance(this.text || ''); + this._utterance.lang = 'ja-JP'; + this._utterance.volume = this._volume; + this._utterance.voice = this.voice; + } + + speechSynthesis.cancel(); + speechSynthesis.speak(this._utterance); + + } catch (e) { + // NOP + } + } + + pause() { + try { + speechSynthesis.cancel(); + } catch (e) { + // NOP + } + } + + static createFromUri(ttsUri) { + const m = /^tts:[^#\?]*\?([^#]*)/.exec(ttsUri); + if (m === null) { return null; } + + const searchParameters = {}; + for (const group of m[1].split('&')) { + const sep = group.indexOf('='); + if (sep < 0) { continue; } + searchParameters[decodeURIComponent(group.substr(0, sep))] = decodeURIComponent(group.substr(sep + 1)); + } + + if (!searchParameters.text) { return null; } + + const voice = audioGetTextToSpeechVoice(searchParameters.voice); + if (voice === null) { return null; } + + return new TextToSpeechAudio(searchParameters.text, voice); + } + +} + +function audioGetFromUrl(url, download) { + const tts = TextToSpeechAudio.createFromUri(url); + if (tts !== null) { + if (download) { + throw new Error('Download not supported for text-to-speech'); + } + return Promise.resolve(tts); + } + + if (download) { + return Promise.resolve(null); + } + return new Promise((resolve, reject) => { const audio = new Audio(url); audio.addEventListener('loadeddata', () => { @@ -46,7 +129,7 @@ async function audioGetFromSources(expression, sources, optionsContext, download } try { - const audio = download ? null : await audioGetFromUrl(url); + const audio = await audioGetFromUrl(url, download); const result = {audio, url, source}; if (cache !== null) { cache[key] = result; @@ -56,7 +139,7 @@ async function audioGetFromSources(expression, sources, optionsContext, download // NOP } } - return {audio: null, source: null}; + return {audio: null, url: null, source: null}; } function audioGetTextToSpeechVoice(voiceURI) { @@ -71,3 +154,27 @@ function audioGetTextToSpeechVoice(voiceURI) { } return null; } + +function audioPrepareTextToSpeech(options) { + if ( + audioPrepareTextToSpeech.state || + !options.audio.textToSpeechVoice || + !( + options.audio.sources.includes('text-to-speech') || + options.audio.sources.includes('text-to-speech-reading') + ) + ) { + // Text-to-speech not in use. + return; + } + + // Chrome needs this value called once before it will become populated. + // The first call will return an empty list. + audioPrepareTextToSpeech.state = true; + try { + speechSynthesis.getVoices(); + } catch (e) { + // NOP + } +} +audioPrepareTextToSpeech.state = false; diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index cf38d09d..e0994f8a 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -197,6 +197,7 @@ class Display { this.options = options ? options : await apiOptionsGet(this.getOptionsContext()); this.updateTheme(this.options.general.popupTheme); this.setCustomCss(this.options.general.customPopupCss); + audioPrepareTextToSpeech(this.options); } updateTheme(themeName) { -- cgit v1.2.3