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 | |
| parent | 2f88bcf82c4dd82d1dd2f035717effaa2673e3c2 (diff) | |
Add option for text-to-speech
| -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; +} |