Getting the list of voices in speechSynthesis (Web Speech API)
Asked Answered
G

13

63

Following HTML shows empty array in console on first click:

<!DOCTYPE html>
<html>
    <head>
        <script>
            function test(){
                console.log(window.speechSynthesis.getVoices())
            }
        </script>
    </head>
    <body>
        <a href="#" onclick="test()">Test</a>
    </body>
</html>

In second click you will get the expected list.

If you add onload event to call this function (<body onload="test()">), then you can get correct result on first click. Note that the first call on onload still doesn't work properly. It returns empty on page load but works afterward.

Questions:

Since it might be a bug in beta version, I gave up on "Why" questions.

Now, the question is if you want to access window.speechSynthesis on page load:

  • What is the best hack for this issue?
  • How can you make sure it will load speechSynthesis, on page load?

Background and tests:

I was testing the new features in Web Speech API, then I got to this problem in my code:

<script type="text/javascript">
$(document).ready(function(){
    // Browser support messages. (You might need Chrome 33.0 Beta)
    if (!('speechSynthesis' in window)) {
      alert("You don't have speechSynthesis");
    }

    var voices = window.speechSynthesis.getVoices();
    console.log(voices) // []

    $("#test").on('click', function(){
        var voices = window.speechSynthesis.getVoices();
        console.log(voices); // [SpeechSynthesisVoice, ...]
    });
});
</script>
<a id="test" href="#">click here if 'ready()' didn't work</a>

My question was: why does window.speechSynthesis.getVoices() return empty array, after page is loaded and onready function is triggered? As you can see if you click on the link, same function returns an array of available voices of Chrome by onclick triger?

It seems Chrome loads window.speechSynthesis after the page load!

The problem is not in ready event. If I remove the line var voice=... from ready function, for first click it shows empty list in console. But the second click works fine.

It seems window.speechSynthesis needs more time to load after first call. You need to call it twice! But also, you need to wait and let it load before second call on window.speechSynthesis. For example, following code shows two empty arrays in console if you run it for first time:

// First speechSynthesis call
var voices = window.speechSynthesis.getVoices();
console.log(voices);

// Second speechSynthesis call
voices = window.speechSynthesis.getVoices();
console.log(voices);
Gametangium answered 2/2, 2014 at 17:25 Comment(4)
hmmm, just a guess, but do you have have the html attribute tags related to speech synthesis on your page by default? If not, it might be taking chrome a bit of time to work that part out by itself.Brinson
no other speech attributes are there. I have another update to my question. Maybe the problem is not with ready.Gametangium
In Dart, I called getVoices() after 5 seconds delay using Timer and got a populated list of available voices.Comstockery
@Mehdi. You should write an answer with some of your edits above. Calling getVoices() twice with one second delay solves the issue.Comstockery
S
115

According to Web Speech API Errata (E11 2013-10-17), the voice list is loaded async to the page. An onvoiceschanged event is fired when they are loaded.

voiceschanged: Fired when the contents of the SpeechSynthesisVoiceList, that the getVoices method will return, have changed. Examples include: server-side synthesis where the list is determined asynchronously, or when client-side voices are installed/uninstalled.

So, the trick is to set your voice from the callback for that event listener:

// wait on voices to be loaded before fetching list
window.speechSynthesis.onvoiceschanged = function() {
    window.speechSynthesis.getVoices();
    ...
};
Socorrosocotra answered 10/4, 2014 at 4:42 Comment(9)
That was a bad decision. The voice will be weird until loaded (French).Topple
The voices are loaded asynchronously, but are they all loaded at the same time or is the onvoiceschanged event fired more than once?Barehanded
Also, do you have to call speechSynthesis.getVoices() and then wait for onvoiceschanged to fire or does the event fire every time sometime during or after page load?Barehanded
@DouglasDeRizzoMeneghetti - Testing against Chrome, I don't find onvoiceschanged callback called until I perform some Speech work such as making a call to getVoices() in the first place.Monterrey
@Monterrey My solution involved calling speechSynthesis.getVoices() when the page loaded and trying to get the voice I wanted in the onvoiceschanged event, as well as every time I were to synthesize something as well, until I could get the voice I wanted and assign it to a variable. Also, in case I couldn't get the voice in time for the synthesis, setting the utterance lang property to the one corresponding to the language I wanted (pt-BR in my case) would do the trick in some platforms.Barehanded
For some reason in my case (chrome) the onvoiceschanged event is fired three times even if it seems all the voices are ready the first time. The result is that I heard the same sentence three times. My workaraound: exit = 0; window.speechSynthesis.onvoiceschanged = function() { voices = window.speechSynthesis.getVoices(); if (exit == 0){ var utterThis = new SpeechSynthesisUtterance(msg); utterThis.voice = voices[10]; synth.speak(utterThis); exit = 1; } }Tafia
Incomplete answer.Lepton
@DouglasDeRizzoMeneghetti onvoiceschanged will fire automatically when the page loads. You do not have to call getVoices first.Sainted
Turns out Firefox will make the voices already available when the page is reloaded/from same process. They thus won't fire that event and make the voices already available. So there you actually need the call to getVoices() in order to not wait that event that will never happen again. So if( getVoices().length ) { callback(); } else { onvoiceschanged = callback; }Riser
K
10

After studying the behavior on Google Chrome and Firefox, this is what can get all voices:

Since it involves something asynchronous, it might be best done with a promise:

const allVoicesObtained = new Promise(function(resolve, reject) {
  let voices = window.speechSynthesis.getVoices();
  if (voices.length !== 0) {
    resolve(voices);
  } else {
    window.speechSynthesis.addEventListener("voiceschanged", function() {
      voices = window.speechSynthesis.getVoices();
      resolve(voices);
    });
  }
});

allVoicesObtained.then(voices => console.log("All voices:", voices));

Note:

  1. When the event voiceschanged fires, we need to call .getVoices() again. The original array won't be populated with content.
  2. On Google Chrome, we don't have to call getVoices() initially. We only need to listen on the event, and it will then happen. On Firefox, listening is not enough, you have to call getVoices() and then listen on the event voiceschanged, and set the array using getVoices() once you get notified.
  3. Using a promise makes the code more clean. Everything related to getting voices are in this promise code. If you don't use a promise but instead put this code in your speech routine, it is quite messy.
  4. You can write a voiceObtained promise to resolve to a voice you want, and then your function to say something can just do: voiceObtained.then(voice => { }) and inside that handler, call the window.speechSynthesis.speak() to speak something. Or you can even write a promise speechReady("hello world").then(speech => { window.speechSynthesis.speak(speech) }) to say something.
Kevenkeverian answered 17/1, 2020 at 11:41 Comment(0)
Z
9

You can use a setInterval to wait until the voices are loaded before using them however you need and then clearing the setInterval:

var timer = setInterval(function() {
    var voices = speechSynthesis.getVoices();
    console.log(voices);
    if (voices.length !== 0) {
      var msg = new SpeechSynthesisUtterance(/*some string here*/);
      msg.voice = voices[/*some number here to choose from array*/];
      speechSynthesis.speak(msg);
      clearInterval(timer);
    }
}, 200);

$("#test").on('click', timer);
Zora answered 10/2, 2016 at 20:42 Comment(3)
Timer is better option for me as onvoiceschanged appears to not be supported on safari.Hermilahermina
listen on an event is better... we don't want to setTimeout or setInterval to keep pinging it ... unless if there is no other methodKevenkeverian
@Hermilahermina voiceschanged event works in Safari now, idk when it was addedPicket
U
5

heres the answer

function synthVoice(text) {

  const awaitVoices = new Promise(resolve=> 
    window.speechSynthesis.onvoiceschanged = resolve)  
  .then(()=> {
    const synth = window.speechSynthesis;

    var voices = synth.getVoices();
    console.log(voices)

    const utterance = new SpeechSynthesisUtterance();
    utterance.voice = voices[3];        
    utterance.text = text;

    synth.speak(utterance);
  });
}
Unlace answered 29/11, 2017 at 4:47 Comment(1)
Have synthesis working in one chrome tab. Tried this in another chrome tab. Yes, I appended synthVoice("Hello world!"), but no sound was heard.Ilysa
T
3

At first i used onvoiceschanged , but it kept firing even after the voices was loaded, so my goal was to avoid onvoiceschanged at all cost.

This is what i came up with. It seems to work so far, will update if it breaks.

loadVoicesWhenAvailable();

function loadVoicesWhenAvailable() {
         voices = synth.getVoices();

         if (voices.length !== 0) {
                console.log("start loading voices");
                LoadVoices();
            }
            else {
                setTimeout(function () { loadVoicesWhenAvailable(); }, 10)
            }
    }
Triode answered 6/10, 2017 at 16:27 Comment(1)
what does LoadVoices() refer to?Sequence
L
2

setInterval solution by Salman Oskooi was perfect

Please see https://jsfiddle.net/exrx8e1y/

function myFunction() {

  dtlarea=document.getElementById("details");
  //dtlarea.style.display="none";
  dtltxt="";

  var mytimer = setInterval(function() {

      var voices = speechSynthesis.getVoices();
      //console.log(voices);
      if (voices.length !== 0) {

        var msg = new SpeechSynthesisUtterance();

        msg.rate = document.getElementById("rate").value; // 0.1 to 10
        msg.pitch = document.getElementById("pitch").value; //0 to 2
        msg.volume = document.getElementById("volume").value; // 0 to 1

        msg.text = document.getElementById("sampletext").value; 
        msg.lang =  document.getElementById("lang").value; //'hi-IN';

        for(var i=0;i<voices.length;i++){

            dtltxt+=voices[i].lang+' '+voices[i].name+'\n';

            if(voices[i].lang==msg.lang) {
              msg.voice = voices[i]; // Note: some voices don't support altering params
              msg.voiceURI = voices[i].voiceURI;
              // break;
            }
        }

        msg.onend = function(e) {
          console.log('Finished in ' + event.elapsedTime + ' seconds.');
          dtlarea.value=dtltxt; 
        };

        speechSynthesis.speak(msg);

        clearInterval(mytimer);

      }
  }, 1000);

} 

This works fine on Chrome for MAC, Linux(Ubuntu), Windows and Android

Android has non-standard en_GB wile others have en-GB as language code Also you will see that same language(lang) has multiple names

On Mac Chrome you get en-GB Daniel besides en-GB Google UK English Female and n-GB Google UK English Male

en-GB Daniel (Mac and iOS) en-GB Google UK English Female en-GB Google UK English Male en_GB English United Kingdom hi-IN Google हिन्दी hi-IN Lekha (Mac and iOS) hi_IN Hindi India

Lascivious answered 2/1, 2018 at 7:15 Comment(0)
K
1

Another way to ensure voices are loaded before you need them is to bind their loading state to a promise, and then dispatch your speech commands from a then:

const awaitVoices = new Promise(done => speechSynthesis.onvoiceschanged = done);

function listVoices() {
    awaitVoices.then(()=> {
        let voices = speechSynthesis.getVoices();
        console.log(voices);
    });
}

When you call listVoices, it will either wait for the voices to load first, or dispatch your operation on the next tick.

Kayleigh answered 16/5, 2017 at 9:51 Comment(1)
this doesn't work for me, console.log(voices) is never executedTafia
L
1

I used this code to load voices successfully:

<select id="voices"></select>

...

  function loadVoices() {
    populateVoiceList();
    if (speechSynthesis.onvoiceschanged !== undefined) {
      speechSynthesis.onvoiceschanged = populateVoiceList;
    }
  }

  function populateVoiceList() {
    var allVoices = speechSynthesis.getVoices();
    allVoices.forEach(function(voice, index) {
      var option = $('<option>').val(index).html(voice.name).prop("selected", voice.default);
      $('#voices').append(option);
    });
    if (allVoices.length > 0 && speechSynthesis.onvoiceschanged !== undefined) {
      // unregister event listener (it is fired multiple times)
      speechSynthesis.onvoiceschanged = null;
    }
  }

I found the 'onvoiceschanged' code from this article: https://hacks.mozilla.org/2016/01/firefox-and-the-web-speech-api/

Note: requires JQuery.

Works in Firefox/Safari and Chrome (and in Google Apps Script too - but only in the HTML).

Loper answered 9/10, 2018 at 8:6 Comment(0)
C
1
async function speak(txt) {
    await initVoices();
    const u = new SpeechSynthesisUtterance(txt);
    u.voice = speechSynthesis.getVoices()[3];
    speechSynthesis.speak(u);
}

function initVoices() {
  return new Promise(function (res, rej){
    speechSynthesis.getVoices();
    if (window.speechSynthesis.onvoiceschanged) {
       res();
    } else {
      window.speechSynthesis.onvoiceschanged = () => res();
    }
  });
}
Crumpton answered 3/2, 2021 at 21:44 Comment(0)
S
1

While the accepted answer works great but if you're using SPA and not loading full-page, on navigating between links, the voices will not be available.

This will run on a full-page load

window.speechSynthesis.onvoiceschanged

For SPA, it wouldn't run.

You can check if it's undefined, run it, or else, get it from the window object.

An example that works:

let voices = [];
if(window.speechSynthesis.onvoiceschanged == undefined){
     window.speechSynthesis.onvoiceschanged = () => {
     voices = window.speechSynthesis.getVoices();
   }
 }else{
    voices = window.speechSynthesis.getVoices();
 }
 // console.log("voices", voices);
Sensitize answered 22/4, 2022 at 13:57 Comment(0)
T
1
    let voices = speechSynthesis.getVoices();
    let gotVoices = false;
    if (voices.length) {
        resolve(voices, message);
    } else {
        speechSynthesis.onvoiceschanged = () => {
            if (!gotVoices) {
                voices = speechSynthesis.getVoices();
                gotVoices = true;
                if (voices.length) resolve(voices, message);
            }
        };
    }
function resolve(voices, message) {
    var synth = window.speechSynthesis;
    let utter = new SpeechSynthesisUtterance();
    utter.lang = 'en-US';
    utter.voice = voices[65];
    utter.text = message;
    utter.volume = 100.0;
    synth.speak(utter);
}

Works for Edge, Chrome and Safari - doesn't repeat the sentences.

Thereinafter answered 8/6, 2022 at 16:37 Comment(1)
Works for me using react js. EdgeHaulm
S
0

I had to do my own research for this to make sure I understood it properly, so just sharing (feel free to edit).

My goal is to:

  • Get a list of voices available on my device
  • Populate a select element with those voices (after a particular page loads)
  • Use easy to understand code

The basic functionality is demonstrated in MDN's official live demo of:

https://github.com/mdn/web-speech-api/tree/master/speak-easy-synthesis

but I wanted to understand it better.

To break the topic down...

SpeechSynthesis

The SpeechSynthesis interface of the Web Speech API is the controller interface for the speech service; this can be used to retrieve information about the synthesis voices available on the device, start and pause speech, and other commands besides.

Source

onvoiceschanged

The onvoiceschanged property of the SpeechSynthesis interface represents an event handler that will run when the list of SpeechSynthesisVoice objects that would be returned by the SpeechSynthesis.getVoices() method has changed (when the voiceschanged event fires.)

Source

Example A

If my application merely has:

var synth = window.speechSynthesis;
console.log(synth);
console.log(synth.onvoiceschanged);

Chrome developer tools console will show:

enter image description here

Example B

If I change the code to:

var synth = window.speechSynthesis;

console.log("BEFORE");
console.log(synth);
console.log(synth.onvoiceschanged);

console.log("AFTER");
var voices = synth.getVoices();

console.log(voices);
console.log(synth);
console.log(synth.onvoiceschanged);

The before and after states are the same, and voices is an empty array.

enter image description here

Solution

Although i'm not confident implementing Promises, the following worked for me:

Defining the function

var synth = window.speechSynthesis;
// declare so that values are accessible globally
var voices = [];


function set_up_speech() {

    return new Promise(function(resolve, reject) {

        // get the voices
        var voices = synth.getVoices();

        // get reference to select element
        var $select_topic_speaking_voice = $("#select_topic_speaking_voice");

        // for each voice, generate select option html and append to select
        for (var i = 0; i < voices.length; i++) {

            var option = $("<option></option>");

            var suffix = "";

            // if it is the default voice, add suffix text  
            if (voices[i].default) {
                suffix = " -- DEFAULT";
            }

            // create the option text
            var option_text = voices[i].name + " (" + voices[i].lang + suffix + ")";

            // add the option text
            option.text(option_text);

            // add option attributes
            option.attr("data-lang", voices[i].lang);
            option.attr("data-name", voices[i].name);

            // append option to select element
            $select_topic_speaking_voice.append(option);
        }

        // resolve the voices value
        resolve(voices)

    });

}

Calling the function

// in your handler, populate the select element    
if (page_title === "something") {
set_up_speech()
}
Sequence answered 24/8, 2018 at 5:41 Comment(0)
E
0

Android Chrome - turn off data saver. It was helpfull for me.(Chrome 71.0.3578.99)

// wait until the voices load
   window.speechSynthesis.onvoiceschanged = function() {
    window.speechSynthesis.getVoices();

};
Elegancy answered 4/1, 2019 at 19:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.