はじめに (対象読者・この記事でわかること)
この記事は、Webアプリケーションで音声合成機能の実装を検討している方、特にJavaScriptのWeb Speech APIを使い始めたものの、speechSynthesis.getVoices()メソッドで利用可能な音声リストがなぜか取得できないと困っている開発者を対象にしています。
この記事を読むことで、getVoices()が空の配列を返す主な原因と、その正確な解決策について具体的に理解できます。また、ブラウザごとの挙動の違いや、音声合成を安定して動作させるための実践的なテクニックを習得し、あなたのWebアプリケーションにスムーズに音声機能を追加できるようになるでしょう。私も過去にこの問題で悩んだ経験があり、同じように時間を浪費することなく、本質的な実装に集中できるよう、この記事を書きました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 * HTML/CSSの基本的な知識 * JavaScriptの基本的な構文とDOM操作の基礎 * 非同期処理の概念(コールバック、Promiseなど)に触れたことがあると、より理解が深まります
Web Speech APIとspeechSynthesis.getVoices()の基本
Web Speech APIは、Webアプリケーションに音声認識(Speech Recognition)と音声合成(Speech Synthesis)の機能を提供するJavaScriptのAPIです。これにより、ユーザーは声でアプリケーションを操作したり、アプリケーションがテキストを読み上げたりする、よりインタラクティブな体験を構築できます。
今回焦点を当てるのは「音声合成(Text-to-Speech)」の方です。音声合成機能を使うことで、Webページ上のテキストを、まるで人が話しているかのように読み上げさせることができます。この機能の核となるのが、window.speechSynthesisインターフェースです。
window.speechSynthesisインターフェースは、SpeechSynthesisUtteranceオブジェクト(読み上げたいテキストや設定を格納)をキューに入れて管理したり、読み上げを一時停止・再開・中止したりする機能を提供します。そして、最も重要なメソッドの一つがgetVoices()です。
getVoices()メソッドは、現在ブラウザで利用可能なすべての音声(声質、言語、話者の種類など)のリストを返します。例えば、日本語の女性の声、英語の男性の声など、利用可能な選択肢を動的に取得し、ユーザーに選択肢として提示するためにこのメソッドは不可欠です。しかし、多くの開発者が最初に直面する問題が、「なぜかgetVoices()を呼び出しても空の配列が返ってきてしまう」という現象です。
これは、Web Speech APIの音声データがブラウザにロードされるタイミングが、DOMの準備完了よりも遅れることが多いためです。次に、この問題の具体的な原因と解決策について掘り下げていきます。
getVoices()で音声が取得できない原因と具体的な解決策
speechSynthesis.getVoices()が空の配列を返したり、期待する音声リストが得られない場合、その主な原因は、音声データがブラウザに完全にロードされる前にメソッドが呼び出されていることにあります。音声データは非同期にロードされるため、正しいタイミングで取得する必要があります。
ステップ1: ロードタイミングの問題を理解する
多くのJavaScriptコードは、HTMLが完全に読み込まれた後(DOMContentLoadedイベント発生後)に実行されます。しかし、Web Speech APIの音声データは、それよりもさらに遅れてロードされることがあります。これは、ブラウザが利用可能な音声(OSにインストールされているものや、クラウドサービスから提供されるものなど)を動的に検出・準備するためです。
例えば、以下のようにすぐにgetVoices()を呼び出しても、ほとんどの場合、期待する音声は取得できません。
Javascript// HTMLが読み込まれた後すぐに実行されるJavaScript document.addEventListener('DOMContentLoaded', () => { const voices = window.speechSynthesis.getVoices(); console.log("初期状態の音声リスト:", voices); // 空の配列が返ってくる可能性が高い });
このコードでは、DOMContentLoadedイベントが発火した時点では、まだWeb Speech APIの音声データが準備できていないため、空の配列が返されてしまうのです。
ステップ2: voiceschangedイベントリスナーを活用する
この問題を解決するための最も確実な方法は、window.speechSynthesisオブジェクトが提供するvoiceschangedイベントを利用することです。このイベントは、利用可能な音声リストが変更された(つまり、ロードが完了したか、利用可能な音声が増減した)時に発火します。
このイベントをリッスンすることで、音声データが完全に準備できたタイミングでgetVoices()を呼び出すことができます。
Javascript// 利用可能な音声が変更された(ロードが完了したなど)時に発火するイベント window.speechSynthesis.onvoiceschanged = () => { const voices = window.speechSynthesis.getVoices(); console.log("利用可能な音声:", voices); if (voices.length > 0) { // 例: ドロップダウンリストに音声オプションを追加する const voiceSelect = document.getElementById('voice-select'); if (voiceSelect) { // select要素が存在するか確認 voices.forEach((voice) => { const option = document.createElement('option'); option.textContent = `${voice.name} (${voice.lang})`; option.value = voice.name; // またはvoice.voiceURI voiceSelect.appendChild(option); }); console.log(`${voices.length}個の音声がロードされました。`); } } else { console.log("利用可能な音声が見つかりませんでした。"); } }; // 初回のgetVoices()は空でも問題ないが、voiceschangedイベントで確実に取得する // または、初回ロード時に一度だけgetVoices()を呼び出し、voiceschangedが発火するのを待つ // 通常は、onvoiceschangedを設定しておけば、ブラウザが準備できた時に自動的に実行される
このアプローチにより、ブラウザがバックグラウンドで音声データの準備を終えたタイミングで確実にリストを取得できるようになります。
ステップ3: ブラウザ互換性とユーザー操作の要件
voiceschangedイベントは非常に有効ですが、いくつかのブラウザの挙動や特定の要件にも注意が必要です。
- ブラウザ互換性: Web Speech APIは主要なモダンブラウザ(Chrome, Firefox, Edge, Safariなど)でサポートされていますが、実装には若干の違いがある場合があります。特に、利用できる音声の種類や、
voiceschangedイベントの発火タイミングに差異が見られることがあります。 - HTTPS要件: 一部のブラウザ(特にChrome)では、Web Speech APIの機能がセキュアなコンテキスト(HTTPS接続)でのみ利用可能、または完全に機能するといった制限がある場合があります。開発中は
http://localhostやhttp://127.0.0.1であれば問題なく動作することが多いですが、本番環境にデプロイする際はHTTPSを導入することを強く推奨します。 - ユーザー操作のトリガー: 特定のブラウザや環境では、
speechSynthesis.speak()メソッドがユーザーの直接的な操作(クリックイベントなど)によってトリガーされないと、音声が再生されない、または再生が中断されることがあります。これは、自動再生ポリシー(Autoplay Policy)によるもので、ユーザーを不快にさせる可能性のある予期せぬ音声再生を防ぐためのセキュリティ機能です。getVoices()自体はユーザー操作を必要としませんが、取得した音声を使って実際にテキストを読み上げる際には、この点に留意する必要があります。
ハマった点やエラー解決
私が実際にハマった、あるいはよく見かける問題と解決策を挙げます。
- 問題点1:
DOMContentLoadedやwindow.onload直後にgetVoices()を呼んでしまう- これは前述の通り、最も一般的な間違いです。HTML/JSのロード完了と、音声データのロード完了は別物です。
- 問題点2:
voiceschangedイベントが一度しか発火しないと誤解しているvoiceschangedは、利用可能な音声リストに「変更があった時」に発火します。ブラウザによっては、最初のロード時だけでなく、例えば新しい言語パックがインストールされたり、ブラウザの設定が変更されたりした際にも発火する可能性があります。そのため、イベントハンドラ内でDOM要素を操作する際は、重複追加を防ぐロジック(例: 既存のオプションをクリアしてから追加する)を検討すると良いでしょう。
- 問題点3:
speak()メソッドが動作しない、または途中で止まる- これは
getVoices()の問題ではありませんが、音声合成全体でよくある問題です。ユーザー操作(ボタンクリックなど)をトリガーにしてspeechSynthesis.speak()を呼び出すように設計しましょう。 - 長時間再生する場合、
utterance.onendやutterance.onerrorイベントを監視し、エラーハンドリングや次の音声のキューイングを適切に行う必要があります。
- これは
解決策: 完全な実装例
voiceschangedイベントを活用し、ユーザー操作で音声再生を行うための具体的なHTMLとJavaScriptのコード例を示します。
Html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Speech API 音声合成デモ</title> <style> body { font-family: sans-serif; margin: 20px; } label, select, textarea, button { display: block; margin-bottom: 10px; } textarea { width: 100%; max-width: 500px; height: 100px; } button { padding: 10px 20px; cursor: pointer; } </style> </head> <body> <h1>Web Speech API 音声合成デモ</h1> <label for="voice-select">音声を選択:</label> <select id="voice-select"> <option value="">ロード中...</option> </select> <label for="text-to-speak">読み上げるテキスト:</label> <textarea id="text-to-speak">こんにちは、これはWeb Speech APIによる音声合成のテストです。利用可能な音声リストを取得し、このテキストを読み上げています。</textarea> <button id="speak-button">読み上げ開始</button> <button id="stop-button">停止</button> <div id="status"></div> <script> const voiceSelect = document.getElementById('voice-select'); const textToSpeak = document.getElementById('text-to-speak'); const speakButton = document.getElementById('speak-button'); const stopButton = document.getElementById('stop-button'); const statusDiv = document.getElementById('status'); let currentVoices = []; // 利用可能な音声を保持する配列 // 1. voiceschanged イベントをリッスンして音声リストを更新 window.speechSynthesis.onvoiceschanged = () => { currentVoices = window.speechSynthesis.getVoices(); console.log("利用可能な音声が更新されました:", currentVoices); updateVoiceSelect(); }; // 音声選択ドロップダウンを更新する関数 function updateVoiceSelect() { voiceSelect.innerHTML = ''; // 既存のオプションをクリア if (currentVoices.length === 0) { const option = document.createElement('option'); option.textContent = "利用可能な音声がありません"; voiceSelect.appendChild(option); statusDiv.textContent = "エラー: 利用可能な音声が検出されませんでした。"; return; } // オプションを動的に追加 currentVoices.forEach((voice) => { const option = document.createElement('option'); option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' (デフォルト)' : ''}`; option.value = voice.name; // voice.nameをvalueとして使用 voiceSelect.appendChild(option); }); // 初期選択を設定(日本語があれば日本語を優先) const defaultJaVoice = currentVoices.find(voice => voice.lang === 'ja-JP'); if (defaultJaVoice) { voiceSelect.value = defaultJaVoice.name; } else { voiceSelect.selectedIndex = 0; // なければ最初の音声を選択 } statusDiv.textContent = `${currentVoices.length}個の音声がロードされました。`; } // 2. 読み上げボタンのイベントリスナー speakButton.addEventListener('click', () => { if (speechSynthesis.speaking) { console.log("現在読み上げ中です。"); return; } const text = textToSpeak.value; if (!text) { statusDiv.textContent = "読み上げるテキストを入力してください。"; return; } const utterance = new SpeechSynthesisUtterance(text); // 選択された音声を設定 const selectedVoiceName = voiceSelect.value; const selectedVoice = currentVoices.find(voice => voice.name === selectedVoiceName); if (selectedVoice) { utterance.voice = selectedVoice; } else { statusDiv.textContent = "選択された音声が見つかりません。デフォルトを使用します。"; } // その他の設定(オプション) utterance.pitch = 1; // 音の高低 (0.1 - 2) utterance.rate = 1; // 再生速度 (0.1 - 10) utterance.volume = 1; // 音量 (0 - 1) // イベントハンドラ utterance.onstart = () => { statusDiv.textContent = "読み上げ開始..."; speakButton.disabled = true; stopButton.disabled = false; }; utterance.onend = () => { statusDiv.textContent = "読み上げ完了。"; speakButton.disabled = false; stopButton.disabled = true; }; utterance.onerror = (event) => { statusDiv.textContent = `読み上げエラー: ${event.error}`; console.error("SpeechSynthesisUtterance error:", event); speakButton.disabled = false; stopButton.disabled = true; }; // 読み上げ開始 window.speechSynthesis.speak(utterance); }); // 3. 停止ボタンのイベントリスナー stopButton.addEventListener('click', () => { if (speechSynthesis.speaking) { window.speechSynthesis.cancel(); statusDiv.textContent = "読み上げを停止しました。"; speakButton.disabled = false; stopButton.disabled = true; } }); // 初回ロード時に念のためupdateVoiceSelectを呼び出す // これは、voiceschangedが既に発火している場合(高速なロードなど)に対応するため // ただし、通常はonvoiceschangedイベントで十分 if (window.speechSynthesis.getVoices().length > 0) { updateVoiceSelect(); } else { // 音声がまだロードされていない場合は、イベントリスナーが発火するのを待つ statusDiv.textContent = "音声データをロード中..."; } </script> </body> </html>
このコードでは、window.speechSynthesis.onvoiceschangedを使って確実に音声リストを取得し、ドロップダウンリストに表示しています。また、ユーザーがボタンをクリックすることで初めて音声が再生されるようにし、自動再生ポリシーの問題も回避しています。
まとめ
本記事では、JavaScriptのWeb Speech APIにおけるspeechSynthesis.getVoices()メソッドで利用可能な音声が取得できないという問題に焦点を当て、その原因と具体的な解決策を詳細に解説しました。
voiceschangedイベントの活用: 最も重要なのは、音声データが非同期でロードされるため、window.speechSynthesis.onvoiceschangedイベントをリッスンし、このイベントが発火した後にgetVoices()を呼び出すことです。これにより、確実に利用可能な音声リストを取得できます。- ロードタイミングの理解:
DOMContentLoadedイベントでは音声データがまだ準備できていない可能性があることを理解し、適切なタイミングで処理を行うことが肝要です。 - ブラウザの挙動とユーザー操作の考慮: HTTPS環境での利用や、音声再生にはユーザーの直接的な操作が必要になる場合があることにも留意し、堅牢な実装を目指しましょう。
この記事を通して、Web Speech APIを使った音声合成機能の実装におけるよくある落とし穴を回避し、安定した音声機能をあなたのWebアプリケーションに組み込むための実践的な知識を得られたことでしょう。ユーザーにとってよりアクセシブルでインタラクティブな体験を提供するために、ぜひ今回の知見を活用してみてください。
今後は、音声合成のさらなるカスタマイズ(声質の変更、速度・音量の調整、SSMLの利用など)や、音声認識との連携についても記事にする予定です。
参考資料
- MDN Web Docs: Web Speech API
- MDN Web Docs: SpeechSynthesis.getVoices()
- MDN Web Docs: SpeechSynthesis.onvoiceschanged
- MDN Web Docs: SpeechSynthesisUtterance