SpeechSynthesis (TTS)

Sintetizador de voz nativo


Los navegadores tienen un sistema de síntesis de voz integrado en el navegador, denominado TTS (text-to-speech), el cuál permite leer textos en múltiples idiomas y con diferentes configuraciones.

Esto es algo muy interesante desde el punto de vista de la accesibilidad, ya que hay usuarios que no pueden acceder a una página sin la utilización de ayudas como estos sintetizadores de voz. No obstante, también son útiles a la hora de leer textos o utilizarlos para experimentos o juegos.

Para explicar como utilizar estas capacidades del navegador, debemos conocer los tres elementos fundamentales de esta API de navegador:

  1. El motor de síntesis de voz: speechSynthesis
  2. Los mensajes de voz: SpeechSynthesisUtterance
  3. La voz: speechSynthesisVoice

Veamos cada uno de ellos.

Pasar texto a voz (TTS)

Para comenzar a trabajar con estos mensajes de voz, utilizaremos el objeto speechSynthesis del navegador. Para ello, nos adelantaremos un poco a algo que veremos más adelante, que es el objeto SpeechSynthesisUtterance, que es el mensaje de voz y el cuál necesitamos para reproducir un mensaje de texto en voz.

El código inicial es el siguiente, donde tenemos una función addMessage() que se encarga de añadir a la cola de mensajes del sintetizador de voz un mensaje nuevo cada vez que pulsamos. Prueba a pulsarlo varias veces y lo comprobarás:

const addMessage = () => {
  const message = new SpeechSynthesisUtterance("Hola a todos");
  speechSynthesis.speak(message);
}

const button = document.querySelector(".tts");
button.addEventListener("click", () => addMessage());
<button class="tts">Iniciar</button>

El sintetizador de voz (TTS) del navegador funciona como una cola, es decir, cada vez que utilizamos la función speak(), añadimos a la cola de mensajes un nuevo mensaje de voz. Por su parte, el sintetizador irá reproduciendo por orden los mensajes de la lista.

Al igual que tenemos dicho método speak() (al cuál le pasamos el mensaje por parámetro), tenemos otros métodos como pause(), resume() o cancel(), que nos permiten gestionar esa lista de mensajes en cola.

Métodos Descripción
speak(mensaje) Añade la frase mensaje a la cola de frases por leer.
pause() Pausa la síntesis de voz y reproducción actual (si la hay).
resume() Continua la síntesis de voz pausada previamente.
cancel() Cancela la reproducción actual y todas las que están en cola.

Por si fuera poco, también tenemos una serie de propiedades que podemos consultar para saber si el motor de síntesis de voz del navegador está pausado, tiene mensajes en cola o está actualmente reproduciendo un mensaje de voz:

Propiedad Descripción
paused Indica si la reproducción de voz se encuentra pausada.
pending Indica si el sintetizador tiene frases pendientes.
speaking Indica si el sintetizador se encuentra hablando actualmente.

Veamos un ejemplo, donde podamos ver en juego estas propiedades y métodos. Pulsa en el botón de añadir mensajes para acumularlos y ponerlos en cola, y también podrás pausar/reanudar un mensaje, así como cancelar todos los mensajes en cola:

const addMessage = () => {
  const text = "Debería visitar la página de ManzDev o su canal de Twitch.";
  const message = new SpeechSynthesisUtterance(text);
  speechSynthesis.speak(message);
}

const pauseResume = () => {
  if (speechSynthesis.paused)
    speechSynthesis.resume();
  else
    speechSynthesis.pause();
}

const cancel = () => speechSynthesis.cancel();

const [ttsButton, pauseResumeButton, cancelButton] = document.querySelectorAll("button");
ttsButton.addEventListener("click", () => addMessage());
pauseResumeButton.addEventListener("click", () => pauseResume());
cancelButton.addEventListener("click", () => cancel());
<button class="tts">Añadir mensaje</button>
<button class="pause-resume">Pausar / Reanudar</button>
<button class="cancel">Cancelar todo</button>

Personalizar mensajes de voz

Antes vimos un pequeño ejemplo donde creabamos un objeto de tipo SpeechSynthesisUtterance, que no es más que un mensaje de voz que se leerá a través del sintetizador de voz del navegador. Sin embargo, esa funcionalidad es la más básica que podemos utilizar.

Tenemos una serie de propiedades que podemos utilizar en un mensaje de voz, entre los que se encuentran el idioma, el tono y la velocidad de la voz, el texto, el volumen y la voz que leerá el mensaje:

Propiedad Descripción
lang Idioma que utilizará el motor de voz. Por ejemplo, es-ES.
pitch Indica el tono de la voz. Valor entre 0 y 2. Por defecto, 1.
rate Indica la velocidad o ritmo de la voz. Valor entre 0 y 10. Por defecto, 1.
text Indica el texto que leerá el sintetizador de voz.
volume Indica el volumen del mensaje. Valor entre 0 y 1. Por defecto, 1.
voice Referencia a la voz que se utilizará para el proceso de síntesis de voz.

Ten en cuenta que dependiendo del navegador y el sistema operativo utilizado, puedes tener a tu disposición más o menos motores de síntesis de voz. Además, cada uno de ellos puede tener menos opciones de personalización (por ejemplo, algunos motores puede que no tengan el tono a 0).

Veamos las capacidades de modificación que tenemos a nuestra disposición para modificar los motores de voz del navegador:

<fieldset>
  <legend>Datos del mensaje de voz</legend>

  <select class="voice"></select>

  <label>Tono:
    <input class="pitch" type="range" min="0" max="2" step="0.25" value="1">
  </label>
  <label>Velocidad:
    <input class="rate" type="range" min="0" max="2" step="0.25" value="1">
  </label>
  <label>Volumen:
    <input class="volume" type="range" min="0" max="1" step="0.1" value="1">
  </label>

  <textarea cols="60" rows="3" placeholder="Aquí tu mensaje de texto"
            class="text">Hola. Aún no me sigues en mi canal de Twitch</textarea>
</fieldset>

<button class="start">Hablar</button>
<button class="cancel">Cancelar</button>
fieldset > * {
  display: block;
  margin-bottom: 1rem;
}

select,
textarea,
button {
  font-family: Jost, sans-serif;
  font-size: 1.5rem;
}
const voiceOptions = document.querySelector(".voice");
const startButton = document.querySelector(".start");
const cancelButton = document.querySelector(".cancel");
const pitchInput = document.querySelector(".pitch");
const rateInput = document.querySelector(".rate");
const volumeInput = document.querySelector(".volume");

speechSynthesis.addEventListener("voiceschanged", () => {
  const voices = speechSynthesis.getVoices();
  const options = voices.map((voice, index) => {
    return `<option value="${index}">${voice.name}</option>`;
  });
  voiceOptions.innerHTML = options.join("");
});

startButton.addEventListener("click", () => {
  const text = document.querySelector(".text").value;
  const message = new SpeechSynthesisUtterance(text);
  const index = voiceOptions.selectedIndex;
  message.voice = speechSynthesis.getVoices()[index];
  message.pitch = pitchInput.value;
  message.rate = rateInput.value;
  message.volume = volumeInput.value;
  speechSynthesis.speak(message);
});

cancelButton.addEventListener("click", () => speechSynthesis.cancel());

Eventos de mensajes de voz

Sobre los objetos de mensajes de voz podemos utilizar una serie de eventos que se dispararán en momentos clave de la locución del mensaje. Los eventos que podemos utilizar son los siguientes:

Eventos Descripción
start Se dispara cuando se empieza a reproducir un mensaje.
pause Se dispara cuando se pausa un mensaje.
resume Se dispara cuando se reanuda un mensaje pausado.
mark Se dispara cuando se llega a una etiqueta <mark>.
error Se dispara cuando ocurre un error en la lectura de voz.
end Se dispara cuando se llega al final de un mensaje.
boundary Se dispara cuando se llega al límite de una palabra o frase.

Cada uno de estos eventos, tiene la propiedad charIndex, elapsedTime, name y utterance, que nos permitirá obtener información para utilizar en el evento. Veamos un ejemplo donde ponemos estas features a funcionar:

<div class="message-box"></div>
<div class="status"></div>
<button class="start">Hablar</button>
<button class="clear">Limpiar</button>
.status,
.start,
.clear {
  font-size: 1.5rem;
}

.message-box {
  font-size: 2rem;
  color: white;
  background: black;
  padding: 2px;
  height: 52px;
  text-align: center;
  text-shadow: 1px 1px 0 red, -1px -1px 0 red;
}

.status {
  height: 200px;
  overflow-y: auto;
  background: #aaa;
  padding: 0.5rem;
  margin-bottom: 1rem;
}
const messageBox = document.querySelector(".message-box");
const status = document.querySelector(".status");
const startButton = document.querySelector(".start");
const clearButton = document.querySelector(".clear");

const text = `Esto es un mensaje de ejemplo de la web LenguajeJS.com.`;
const message = new SpeechSynthesisUtterance();

startButton.addEventListener("click", () => {
  message.text = text;
  speechSynthesis.speak(message);
});

const setMsg = (elapsedTime, text) => {
  const time = ~~(elapsedTime / 100) / 10;
  status.innerHTML += `[${time}s] ${text} <br>\n`;
}

message.addEventListener("start", ({ elapsedTime }) => {
  setMsg(elapsedTime, `Ha empezado la síntesis de voz.`);
});
message.addEventListener("end", ({ elapsedTime }) => {
  setMsg(elapsedTime, `Ha terminado la síntesis de voz.`);
  messageBox.textContent = "";
});
message.addEventListener("boundary", ({ name, elapsedTime, charIndex, charLength }) => {
  const fragment = text.substring(charIndex, charIndex + charLength);
  setMsg(elapsedTime, `Ha alcanzado un límite (${name} => ${fragment}).`);
  messageBox.textContent = fragment;
});

clearButton.addEventListener("click", () => (status.textContent = ""));

Voces (TTS) disponibles

Como habrás podido comprobar, los navegadores suelen disponer de varias voces para leer los mensajes de texto. La cantidad de voces que tengas depende del navegador, del sistema operativo y de las voces que tengas instaladas en el apartado de idiomas. Si no te fijaste en ello y no sabes que voces tiene tu navegador, puedes echar un vistazo en el segundo ejemplo de esta página.

El objeto speechSynthesis que vimos al principio de este artículo, permite acceder a las voces del sistema mediante el método getVoices(). Sin embargo, es recomendable acceder a él mediante el evento voiceschanged, ya que es posible que los navegadores tarden algo de tiempo en preparar los motores de síntesis de voz y no estén disponibles hasta que se dispare ese evento:

Método o evento Descripción
getVoices() Muestra una lista con las voces disponibles para utilizar.
voiceschanged Evento que se dispara cuando la lista de voces cambia.

Al acceder a la lista de voces mediante getVoices(), cada una de las voces disponibles en el navegador tiene varias propiedades que nos proporcionan algo de información sobre la misma. Las propiedades son las de la siguiente tabla:

Propiedades Descripción
default Indica si esta voz es la que se utilizará por defecto.
lang Indica el idioma de la voz.
localService Indica si la voz es un servicio local o no (es un servicio remoto).
name Indica el nombre de la voz.
voiceURI Indica el tipo de URI y ubicación de la voz.

Veamos como acceder a las voces que tiene nuestro sistema en el siguiente ejemplo:

<table>
  <tr>
    <th>Número</th>
    <th>Nombre del motor de voz</th>
    <th>Voz por defecto</th>
    <th>¿Servicio local?</th>
    <th>Idioma</th>
  </tr>
</table>
table {
  font-family: Jost, sans-serif;
  font-size: 1.25rem;
  border: 1px solid black;
  padding: 3px;
  min-height: 550px;
}

th {
  background: black;
  color: white;
}

td, th {
  padding: 5px;
}
const table = document.querySelector("table");

speechSynthesis.addEventListener("voiceschanged", () => {
  const voices = speechSynthesis.getVoices();
  const voicesList = voices.map((voice, index) => {
    const showBool = (value) => value ? "✅" : "❌";
    return `<tr><td>${index}</td><td>${voice.name}</td>` +
           `<td>${showBool(voice.default)}</td><td>${showBool(voice.localService)}</td>` +
           `<td>${voice.lang}</td></tr>`;
  });
  table.innerHTML += voicesList.join("");
});

Esta lista se obtiene a través del navegador que estás usando actualmente. Si compruebas con otro navegador, verás que la lista de voces cambiará. De la misma forma, en otro sistema operativo no estarán disponibles las mismas voces.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev