Speech Synthesis Test

by David Iglesias

HTML

<h1>Speech Synthesis</h1>

<textarea id="phrase">Hello, Alan!</textarea>
<hr />

<fieldset>
  <legend>Voice</legend>
  <div>
    <label> Voice:
    <select id="availableVoices">
      <option id="defaultVoice">None / Default</option>
    </select>
    </label>
  </div>
  <div>
    <label>
      <input type="checkbox" checked id="preferGoogle" />
      Prefer Google voices (Chrome only)
    </label>
  </div>
</fieldset>

<fieldset>
  <legend>Controls</legend>
  <button id="speakBtn">Speak</button>
  <button id="stopBtn">Stop</button>
</fieldset>

CSS

* { box-sizing: border-box; font-family: sans-serif; }
#phrase {
  width: 100%;
  height: 160px;
  font-size: 2em;
}

JavaScript

let synth = window.speechSynthesis;

speakBtn.addEventListener("click", speak);
stopBtn.addEventListener("click", stop);
phrase.addEventListener("keydown", handleEnter);
preferGoogle.addEventListener("change", updateVoices);
synth.addEventListener("voiceschanged", updateVoices);

// Say `phrase` with the selected `availableVoices`.
function speak() {
  const utterance = new SpeechSynthesisUtterance(phrase.value);
  utterance.voice = findVoice(availableVoices.value);
  synth.speak(utterance);
}

// Find a voice by its voiceURI.
function findVoice(URI) {
	let voice = synth.getVoices().find(
    (v) => v.voiceURI === URI
  );
  return voice;
}

// Stop speaking.
function stop() {
	synth.cancel();
}

// Get all voices and update the select
function updateVoices() {
  let voices = synth.getVoices()
    // "Google" voices are only available in Chrome.
    .filter((v) => !preferGoogle.checked || v.name.includes("Goog"))    
  // Group available voices by their lang.
  let grouped = Map.groupBy(voices, (v) => v.lang);
  renderVoiceSelect(grouped);
}

function handleEnter(event) {
  if (event.key === "Enter") {
    event.preventDefault();
    speak();
  }
}

// Add the available voices to the dropdown.
function renderVoiceSelect(voices) {
  availableVoices.replaceChildren(defaultVoice);
  let sortedKeys = [...voices.keys()].toSorted();
  sortedKeys.forEach((k) => {
    let optgroup = document.createElement('optgroup');
    optgroup.label = k;
    voices.get(k).forEach((voice) => {
      let option = document.createElement('option');
      option.innerText = voice.name;
      option.value = voice.voiceURI;
      optgroup.append(option);
    });
    availableVoices.append(optgroup);
  });
}

// Load the available voices.
updateVoices();