Creating a speech synthesizer using Web Speech API and React
Tags: react, javascript, webdev, ai
Published: 2022-04-22 20:59:31

Hello fellow readers!
In this post we are going to build a very simple speech synthesizer application using React and a browser built-in speech API.

## Introduction

Some of you may not know that browsers even had a built-in speech API (at least I didn't), but here we are going to explore more of it. This API had it's first draft [in 2012](https://www.w3.org/community/speech-api/2012/06/13/first-draft-of-speech-javascript-api-published-by-speech-api-community-group/) and described the interfaces for speech **recognition** and **synthesis**. The updated draft can be found [here](https://wicg.github.io/speech-api/) if you are curious enough to read.

Below you can see the [current browser support](https://caniuse.com/?search=speech) for this API. As you can see, it's pretty wide adopted (even in Safari!).

![Speech API browser support table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j8vbk5ipdr1so8l4t6ty.png)

Here we are going to address only the **synthesis API**. In this sense, our application will have a text input for the user to type what should be spoken and a selector to pick the wanted voice.

> **Important note:** during this article I will be using the Chrome browser. You may have different results if you are using another one.

The final result will be like this:

![Final result of the application. There is a text input, a voice selector and a button to speak the typed text.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9gj2udi4jtcr59qlbpzx.png) 

That being said, let's start!

## The voice selector component

This component is a simple _select_ element to allow the user to choose between the voices offered by the browser.
Let's begin with the API object itself. If you are using a browser that enables it, you may find this object globally in the window:

```js
console.log(window.speechSynthesis)
```

### Component structure

This component will basically hold a state to the voice list and a _select_ element to choose between them.
The state is typed with `SpeechSynthesisVoice`, which is an object that has some properties regarding the voices offered by the browser, as: _name, language and a default flag_, that will be true to the default voice of your browser.

Let's start with the initial structure and we will be incrementing it later:

```tsx
const synth = window.speechSynthesis;

const VoiceSelector = ({ selected = 0, setSelected }: VoiceSelectorProps) => {
  const [voices, setVoices] = useState<SpeechSynthesisVoice[]>([]);

  return (
    <select
      value={selected}
      onChange={(e) => setSelected(parseInt(e.target.value))}
    >
      {voices.map((voice, index) => (
        <option key={index} value={index}>
          {voice.name} ({voice.lang}) {voice.default && ' [Default]'}
        </option>
      ))}
    </select>
  );
};

export default VoiceSelector;
```

### Getting the voice list

In this API, there is a specialized function to get the voices offered by the browser. You can check it out directly on your dev tools:

```js
window.speechSynthesis.getVoices()
```

Let's change our component a little bit to init the state.
Here we will have a `populateVoice` function that calls the API function and sets the state. Then, we are going to call it in a useEffect.


```tsx
const VoiceSelector = ({ selected = 0, setSelected }: VoiceSelectorProps) => {
  const [voices, setVoices] = useState<SpeechSynthesisVoice[]>([]);

  const populateVoiceList = useCallback(() => {
    const newVoices = synth.getVoices();
    setVoices(newVoices);
  }, []);

  useEffect(() => {
    populateVoiceList();
    if (synth.onvoiceschanged !== undefined) {
      synth.onvoiceschanged = populateVoiceList;
    }
  }, [populateVoiceList]);

  return (
    <select
      value={selected}
      onChange={(e) => setSelected(parseInt(e.target.value))}
    >
      {voices.map((voice, index) => (
        <option key={index} value={index}>
          {voice.name} ({voice.lang}) {voice.default && ' [Default]'}
        </option>
      ))}
    </select>
  );
};
```

You may ask yourself why don't we just init the state directly with the voices like this:

```ts
const [voices, setVoices] = useState<SpeechSynthesisVoice[]>(synth.getVoices());
```

There is a little gotcha on the approach above (if you try, you ought to see an empty select). [According to Web Speech API Errata (E11 2013-10-17)](https://stackoverflow.com/a/22978802/5357218), the voices are loaded asynchronously. Therefore, there is an event called `onvoiceschanged` (which we are using) that is fired when the voices are ready (this behavior might be different from one browser to another).

You can learn more about this behavior [here](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API#speech_synthesis).

That's it for the voice selector component. Let's move next to the application itself.

## The application component

Our application component will control the voice selector state and do the magic of converting a text into a speech.
Let's first start with the simple structure. It's a simple form with a text input, the voice selector and a submit button:

```tsx
const synth = window.speechSynthesis;

const App = () => {
  const [textValue, setTextValue] = useState<string>('');
  const [selectedVoice, setSelectedVoice] = useState<number>(0);

  if (!synth)
    return <span>Aw... your browser does not support Speech Synthesis</span>;

  return (
    <form>
      <input
        type="text"
        value={textValue}
        onChange={(e) => setTextValue(e.target.value)}
      />
      <VoiceSelector selected={selectedVoice} setSelected={setSelectedVoice} />
      <button type="submit">Speak</button>
    </form>
  );
};
```

As you can see, the application holds two states:

- **textValue**: controls the input value
- **selectedVoice**: controls the selected voice

Also, I've put a security check to ensure the browser has the speech API.

Let's now attach the submit handler to the form. When the user submits it, the API must read the input's content and speak it with the selected voice. Check it out:

```tsx
const speak = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const synth = window.speechSynthesis;
    const utterance = new SpeechSynthesisUtterance(textValue);

    // As the voices were already loaded in the voice selector
    // we don't need to use the onvoiceschanged event
    utterance.voice = synth.getVoices()[selectedVoice];

    synth.speak(utterance);
 };
```

Let's break it down:

1. First, we create a `SpeechSynthesisUtterance` object with the typed text as the constructor's argument.
2. Then, we plug-in the selected voice into the `voice` property of the newly created utterance object. Note that I just call the `getVoices` function with the selected voice index.
3. Lastly, but not less important, we call the `speak` function of the synthesis API. And _voilà_! Our synthesizer is ready.

Now we have our complete application component:

```tsx
const synth = window.speechSynthesis;

const App = () => {
  const [textValue, setTextValue] = useState<string>('');
  const [selectedVoice, setSelectedVoice] = useState<number>(0);

  if (!synth)
    return <span>Aw... your browser does not support Speech Synthesis</span>;

  const speak = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const synth = window.speechSynthesis;
    const utterance = new SpeechSynthesisUtterance(textValue);
    utterance.voice = synth.getVoices()[selectedVoice];

    synth.speak(utterance);
  };

  return (
    <form onSubmit={speak}>
      <input
        type="text"
        value={textValue}
        onChange={(e) => setTextValue(e.target.value)}
      />
      <VoiceSelector selected={selectedVoice} setSelected={setSelectedVoice} />
      <button type="submit">Speak</button>
    </form>
  );
};
```

You can run this example [here](https://stackblitz.com/edit/react-ts-wdhbgb?file=index.tsx) to test this and hear your browser speaking.

## Other features

The synthesis API has some cool features that weren't exposed here, such as:

- **stop**: you can stop the speak at any time!
- **pitch and rate**: you can customize the pitch and rate of the speaking

You can learn more about these features and much more on [mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API#speech_synthesis).

## Conclusion

This wraps up our adventure on the speech synthesis API world. I hope you all enjoyed it and if you have any doubts or opinion, please use the comment section below!