diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2019-10-12 22:50:22 -0400 |
---|---|---|
committer | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2019-10-13 12:20:56 -0400 |
commit | 21a2730cdeef80327285f77a155451a8fbf67939 (patch) | |
tree | 01f53f126535e5d7109a4822d07b8c3967ebe918 /ext | |
parent | 2f88bcf82c4dd82d1dd2f035717effaa2673e3c2 (diff) |
Add option for text-to-speech
Diffstat (limited to 'ext')
-rw-r--r-- | ext/bg/js/options.js | 3 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 73 | ||||
-rw-r--r-- | ext/bg/settings.html | 11 | ||||
-rw-r--r-- | ext/mixed/js/audio.js | 13 |
4 files changed, 99 insertions, 1 deletions
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index fac17d68..4854cd65 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -287,7 +287,8 @@ function profileOptionsCreateDefaults() { sources: ['jpod101'], volume: 100, autoPlay: false, - customSourceUrl: '' + customSourceUrl: '', + textToSpeechVoice: '' }, scanning: { diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 77815955..debf9f2f 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -48,6 +48,7 @@ async function formRead(options) { options.audio.autoPlay = $('#auto-play-audio').prop('checked'); options.audio.volume = parseFloat($('#audio-playback-volume').val()); options.audio.customSourceUrl = $('#audio-custom-source').val(); + options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); @@ -119,6 +120,7 @@ async function formWrite(options) { $('#auto-play-audio').prop('checked', options.audio.autoPlay); $('#audio-playback-volume').val(options.audio.volume); $('#audio-custom-source').val(options.audio.customSourceUrl); + $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); @@ -326,6 +328,77 @@ async function audioSettingsInitialize() { const options = await apiOptionsGet(optionsContext); audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); audioSourceUI.save = () => apiOptionsSave(); + + textToSpeechInitialize(); +} + +function textToSpeechInitialize() { + if (typeof speechSynthesis === 'undefined') { return; } + + speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); + updateTextToSpeechVoices(); + + $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); +} + +function updateTextToSpeechVoices() { + const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); + voices.sort(textToSpeechVoiceCompare); + if (voices.length > 0) { + $('#text-to-speech-voice-container').css('display', ''); + } + + const select = $('#text-to-speech-voice'); + select.empty(); + select.append($('<option>').val('').text('None')); + for (const {voice} of voices) { + select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); + } + + select.val(select.attr('data-value')); +} + +function compareLanguageTags(a, b) { + if (a.substr(0, 3) === 'ja-') { + return (b.substr(0, 3) === 'ja-') ? 0 : -1; + } else { + return (b.substr(0, 3) === 'ja-') ? 1 : 0; + } +} + +function textToSpeechVoiceCompare(a, b) { + const i = compareLanguageTags(a.voice.lang, b.voice.lang); + if (i !== 0) { return i; } + + if (a.voice.default) { + if (!b.voice.default) { + return -1; + } + } else if (b.voice.default) { + return 1; + } + + if (a.index < b.index) { return -1; } + if (a.index > b.index) { return 1; } + return 0; +} + +function textToSpeechTest() { + try { + const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; + const voiceURI = $('#text-to-speech-voice').val(); + const voice = audioGetTextToSpeechVoice(voiceURI); + if (voice === null) { return; } + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = 'ja-JP'; + utterance.voice = voice; + utterance.volume = 1.0; + + speechSynthesis.speak(utterance); + } catch (e) { + // NOP + } } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index ea3e8c18..ffa5533e 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -293,6 +293,16 @@ <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> </div> + <div class="form-group" style="display: none;" id="text-to-speech-voice-container"> + <label for="text-to-speech-voice">Text-to-speech voice</label> + <div class="input-group"> + <select class="form-control" id="text-to-speech-voice"></select> + <div class="input-group-btn"> + <button class="btn btn-default" id="text-to-speech-voice-test" title="Test voice" data-speech-text="よみちゃん"><span class="glyphicon glyphicon-volume-up"></span></button> + </div> + </div> + </div> + <div class="form-group options-advanced"> <label for="audio-custom-source">Custom audio source <span class="label-light">(URL)</span></label> <input type="text" id="audio-custom-source" class="form-control" placeholder="Example: http://localhost/audio.mp3?expression={expression}&reading={reading}"> @@ -658,6 +668,7 @@ <script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/templates.js"></script> <script src="/bg/js/util.js"></script> + <script src="/mixed/js/audio.js"></script> <script src="/bg/js/settings-profiles.js"></script> <script src="/bg/js/settings.js"></script> diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js index b905140c..5e3b9164 100644 --- a/ext/mixed/js/audio.js +++ b/ext/mixed/js/audio.js @@ -58,3 +58,16 @@ async function audioGetFromSources(expression, sources, optionsContext, createAu } return {audio: null, source: null}; } + +function audioGetTextToSpeechVoice(voiceURI) { + try { + for (const voice of speechSynthesis.getVoices()) { + if (voice.voiceURI === voiceURI) { + return voice; + } + } + } catch (e) { + // NOP + } + return null; +} |