🗣️

Chrome の音声合成APIは voiceschanged イベントと組み合わせて使う

2025/01/14に公開

TL;DR

Google Chrome で speechSynthesis.getVoices() するときは voiceschanged イベントを監視しつつ使うと良いです。

speechSynthesis.addEventListener("voiceschanged", () => {
  const voices = speechSynthesis.getVoices();
});

やりたいこと

ブラウザで音声合成をして Text-to-Speech をやりたい場合、例えば次のように書くだけで最低限OKです。このコードを実行すると、#play ボタンをクリックした際にブラウザが「こんにちは」と話してくれます。

const playButton = document.getElementById("play");
playButton.addEventListener("click", () => {
    const utter = new SpeechSynthesisUtterance();
    utter.text = "こんにちは";
    window.speechSynthesis.speak(utter);
});

声色も設定可能です。使用可能な声色の一覧は次のように取得することができます。

const voices = window.speechSynthesis.getVoices();

ここまでを組み合わせると、例えば次のようにして、Googleが用意した日本語向きの声色で「こんにちは」と喋らせることができる……はずです。

const playButton = document.getElementById("play");
playButton.addEventListener("click", () => {
    const voice = speechSynthesis
      .getVoices()
      .find((vo) => vo.voiceURI === "Google 日本語");

    const utter = new SpeechSynthesisUtterance();
    utter.text = "こんにちは";
    utter.voice = voice;
    window.speechSynthesis.speak(utter);
});

ところが実際に Google Chrome でこれを実行してみると、初回はデフォルトの声色で音声が流れ、2回目以降はGoogleの声色で音声が流れると思います。不思議な挙動です。

原因

Chormium のソースを追ったわけではないのですが、どうも Google Chrome は speechSynthesis を初めて呼び出したタイミングで声色リストを非同期で探しているようです。先のサンプルコードのように、speechSynthesis を初めて呼び出しつつ getVoices() を実行すると、その時点では声色リストを取得できていないので、空配列が返ってきます。

// [] が出力される
console.log(window.speechSynthesis.getVoices());

実験として、初回 speechSynthesis を呼び出したあと数十ミリ秒ほど待ってから再度 getVoices() をしてみると、意図した声色リストがリストアップされます。

// [] が出力される
console.log(window.speechSynthesis.getVoices());

setTimeout(() => {
    // SpeechSynthesisVoice[] が出力される
    console.log(window.speechSynthesis.getVoices());
}, 50);

解決方法

先の例のように setTimeout で固定時間待ってからメインの処理を実行するのも悪くはないですが、Google Chrome は認識している声色一覧に変更があった場合 voiceschagned イベントを発火してくれるので、これを監視しておくのがスマートです。

const playButton = document.getElementById("play");
// 準備が整うまで再生UIを無効化しておく
playButton.setAttribute("disabled", "");

/** @type {SpeechSynthesisVoice | null} */
let voice = null;

// voiceschanged イベントを監視
speechSynthesis.addEventListener("voiceschanged", () => {
  voice =
    speechSynthesis
      .getVoices()
      .find((vo) => vo.voiceURI === "Google 日本語") ?? null;
  // 準備が整ったら再生UIの無効化を解除して有効化する
  if (voice !== null) {
    playButton.removeAttribute("disabled");
  }
});

playButton.addEventListener("click", () => {
  const utter = new SpeechSynthesisUtterance();

  utter.voice = voice;
  utter.text = "こんにちは";

  speechSynthesis.speak(utter);
});

Firefox, Safari の場合

ここまでの話は Google Chrome の話です。Firefox や Safari では speechSynthesis.getVoices() をいきなり呼び出してもきちんと値が返ってきてくれます。特に気にすることなく speechSynthesis.getVoices() を呼び出せば OK です。

逆に Firefox や Safari では voiceschagned イベントが発火しません。文中のサンプルコードのように voiceschanged イベントが発火したときにだけ声色を取得したり再生UIを有効化したりするようなコードを書くと、Firefox や Safari で正常に動作しなくなるので注意が必要です。

Discussion