I am using the Web Speech API to read out an array of words with a short delay between each one (a spelling test for my son!). I have defined an async function to speak a single word and used setTimeout() to delay the following word by 5 seconds. Everything is working as required, except when the START button is pressed immediately after the STOP button, before the 5 second timeout has resolved. This results in the whole array of words starting again, with the remaining words from the initial test threaded in between. I have tried to fix this by cancelling the setTimeout method and by disabling the START button while the timeout is active, but without success.
// initiate the synth
const synth = window.speechSynthesis;
// grab the UI elements
const startButton = document.querySelector("#start");
let started = false;
const stopButton = document.querySelector("#stop");
stopButton.disabled = true;
// listen to the stop button
stopButton.addEventListener("click", () => {
startButton.disabled = false;
stopButton.disabled = true;
started = false;
synth.cancel();
});
// get the voices
const voices = synth.getVoices();
const GBvoice = voices.filter((voice) => {
return voice.lang == "en-GB";
});
// speak a single word
async function speakWord(word) {
const utterThis = new SpeechSynthesisUtterance(word);
utterThis.voice = GBvoice[1];
utterThis.pitch = 1;
utterThis.rate = 1;
synth.speak(utterThis);
}
// define delay function
const addDelay = (t) => {
return new Promise((resolve) => {
setTimeout(resolve.bind(null), t);
});
};
// define the spelling words
const words = ["column", "solemn", "autumn", "foreign", "crescent", "spaghetti", "reign", "fascinating", "whistle", "thistle"];
// problem - when start button is pressed during timeout, two lists of words are spoken
startButton.onclick = async function () {
startButton.disabled = true;
stopButton.disabled = false;
started = true;
for (let word of words) {
await speakWord(word).then(addDelay.bind(null, 5000));
if (!started) {
break;
}
}
};
<button id="stop">Stop</button>
<button id="start">Start</button>
It may seem a little over-engineered, but here’s the cancellation route done using the
AbortControllerAPI.For clarity of presentation (and because I have no working speech synthesis in my browser), all parts related to speech synthesis were removed and replaced with a bare
console.log, but you should be able to put them back easily.In essence: instead of using a bare flag variable, each ‘speech’ job creates a new object managing its own cancellation state, then puts that object in a global variable where it can be found by either button’s handler. Either button, when clicked, will cancel the current job, if there is one; the play button will then start a new job. This way, the ABA problem of the original approach is averted.
In this example you may be able to get away with replacing
AbortControllerwith something simpler (like a plain object with acancelledproperty), but in the general case where you need to invoke other Web APIs (likefetch), you may need to have anAbortControllerat the ready after all.