新たなプロジェクト「DOM-SPEAKER(ドムスピーカー)」を公開しました。

DOM-SPEAKER のデモ

HTML や XML のファイルから要素を抜き出し、階層を視覚的に表示し、Web Speech API の音声合成(SpeechSynthesis)でタグ名を読み上げるボーカルシンセサイザーです。

音声の種類を変更したり、読み上げのピッチ(音程)や速さなどを変更することが可能です。

あらかじめ用意されたプリセットの HTML データに加えて、直接ローカルファイルを読み込むこともできます。また、解析した HTML データや音声の設定は localStorage に保存できます。

なお、本アプリで使用している Speech Synthesis は現時点では試験的な機能であり、OS やブラウザによって大きく挙動が異なりますのでご了承ください。

本アプリは以下のセクションで構成されています。

アプリの各セクションの名称を示した図。1. HEADER、2. DISPLAY、3. VOICE CONTROLS、4. BUTTONS、5. STORAGE、6. HTML PRESETS から構成されている
  1. HEADER
  2. DISPLAY
  3. VOICE CONTROLS
  4. BUTTONS
  5. STORAGE
  6. HTML PRESETS

読み込んだファイルのタイトルと URL が表示されます。

タイトルの値は読み込んだファイル内の <title> 要素が参照されますが、見つからない場合には「Untitled」が表示されます。なお、タイトルは自由に変更可能です。

URL は、まず <link rel="canonical"> が参照され、そのあとに <meta property="og:url"> が参照されます。どちらも見つからない場合には「Canonical URL not found」が表示されます。

ディスプレイには、読み込んだファイルの HTML や XML の要素が解析され、タグ名とともにネストに応じた深さで視覚化されます。

ここで表示されるタグ名は、通常は <body> から下の要素です。

「DISPLAY」のスクリーンショット

この各要素を押下すると、タグ名が読み上げられ再生位置に設定されます。

また、ディスプレイエリア内はキーボードでも操作することができます。

操作キー
エリア外から再生位置にフォーカスTab / Shift + Tab
音声を再生 / 再生位置に設定Enter
フォーカスの移動 / / / / PageUp / PageDown
最初の要素にフォーカスHome
最後の要素にフォーカスEnd
自動再生 / 一時停止Space
自動再生の一時停止Esc

なお、Esc キーによる自動再生の一時停止については、ディスプレイエリアに限らずにページ全体で有効なキー操作です。

ディスプレイの下にはモードを変更するスイッチが 2 つ用意されています。

「VERTICAL-LR」を選択すると、ディスプレイ内の要素は垂直向きになり、スクロールは水平方向に切り替わります。

「VERTICAL-LR」を有効にした状態のスクリーンショット

ちなみに、この見た目の切り替えは CSS の :has() 擬似クラスで実現しています。

「AUTO SCROLL」を選択すると、再生位置に応じてスクロール位置が移動します。デフォルトではオンになっていますが、自動再生中にエリア内を自由にスクロールしたいときなどにチェックを外します。

このエリアでは、音声データの種類や読み上げの設定を変更できます。

「VOICE CONTROLS」のスクリーンショット

「VOICE」では、音声データ(音源)を選択できます。ここで表示されるリストには OS にインストールされている音源とブラウザ独自の音源が含まれるため閲覧環境によって異なります。

なお「Google」や「Microsoft」といった接頭辞が付いたオンライン音源がありますが、これらについては動作が安定しないことがあります。

また、OS に音声データを追加することも可能です。ただ、ダウンロードサイズが大きいデータもあるため事前にご確認ください。

Mac の場合には「鐘の音(Bells)」や「ロボット風ボイス(Zarvox)」といった、飛び道具的な音源も用意されているので、よりシンセライクな楽しみ方ができます。

「PITCH」では、読み上げのピッチ(音程)を変更できます。

音声データによってはピッチが変化しないものもあります。

項目
範囲0 から 100
初期値50
変換後の値0 から 2

「変換後の値」は JS で使用する際に変換した値の範囲を意味します。例えば「PITCH」のスライダーの値が 75 の場合には、JS で使用するときには 1.5 に変換されます。

「MOD」は、Modulation(変調)を意味し、要素の階層にあわせてピッチに変化をつけることができます。正の値を与えると、要素の階層が深くなるごとにピッチが高くなり、負の値を与えると、反対に階層が深くなるごとにピッチが低くなります。

最初に表示されるプリセット「Tuner」は、構造が階段状になっているので、この変化が確認しやすくなっています。

項目
範囲-100 から 100
初期値-20
変換後の値-0.1 から 0.1

「RATE」では、読み上げの速度を変更できます。極端に速度を上げることで、アタックの短いパーカッシブな音を得ることもできます。

音声データによっては速度が変化しないものもあります。

項目
範囲0 から 100
初期値25
変換後の値0 から 4

ちなみに、SpeechSynthesisUtterance.rate の範囲では 0.1 から 10 とされているのですが、4 を超えてもほとんど変化が得られないので、このような設定にしています。

「BPM」では、再生時の速度(テンポ)を変更できます。

項目
範囲0 から 240
初期値60

指定した値はミリ秒(ms)に変換されて、setInterval() の待ち時間として使用されます。

なお、この setInterval() で処理を呼び出したときに、音声の再生が終わっていない場合には次のタグには移動せずにもう一周するため、タグ名の長さによって独特のリズムが生まれます。

`setInterval()` のフローチャート。待ち時間 `1000ms` が経過したときに、`pending` が `true` の場合にはそのまま `setInterval()` に戻り、`false` の場合には `speak()` で音声を再生して、インデックスをインクリメントしてから `setInterval()` に戻る
待ち時間が 1000ms の例。SpeechSynthesispending の場合には、そのまま setInterval() の処理に戻る

「VOLUME」では、音量を変更できます。

項目
範囲0 から 100
初期値100
変換後の値0 から 1

ボタンエリアには「IMPORT」と「PLAY」の 2 つのボタンが配置されています。

「BUTTONS」のスクリーンショット

「IMPORT」ボタンを選択すると、ローカルのファイルを読み込むことができます。

対象となるファイルの型は text/html もしくは text/xml なので、HTML ファイルに加えて XML ファイルや SVG ファイルも読み込むことができます。

なお、読み込むファイルのサイズが大きいと、解析や描画処理に時間を要する場合があります。

「PLAY」ボタンを選択すると、「BPM」で指定したテンポで自動再生が開始します。もう一度選択すると一時停止します。

キーボードの Esc キーでも一時停止します。

「STORAGE」では、解析した HTML / XML のデータや音声の設定を localStorage に保存できます。

「STORAGE」のスクリーンショット。3 つのアイテムが登録されている

「SAVE」ボタンを選択するとテキストフィールドが出現し、保存するアイテム名を入力できます。初期値はタイトルが入力されています。

入力を終えるとリストに追加され、localStorage にもデータが保存されます。

リストの中から保存したアイテム名を選択すると設定が反映され、アイテム名の隣にある削除アイコン(Delete)を選択すると、保存したアイテムが削除されます。

「HTML PRESETS」では、あらかじめ用意された HTML データを読み込むことができます。

「VOICE CONTROLS」の値は保持していないため、音声の設定は変更されません。

「HTML PRESETS」のスクリーンショット

最初に表示されるデータで、音声の試聴やチューニング用として用意しています。

開発者がウェブデザイン関連のスクールに通っていたときの課題として作成したサイトのデータです。その名のとおり、テーブルレイアウトが主流の時代の HTML です。

開発者がその後、初めて CMS を使用して作成した個人ブログの HTML です。当時使用していた CMS が Movable Type の 3.33 だったので、このような名前を付けています。

このサイトのホームを動的に読み込みます。今後内容が更新されて HTML の構造が変わると、生成される内容も変化します。

読み込む HTML ファイルのコードに独自属性を付与することで、データの解析方法をカスタマイズすることができます。

data-dom-speaker="body"

この見出しのリンク

ファイルをインポートしたときに、通常は <body> より下の要素が対象になりますが、任意の要素に data-dom-speaker="body" を指定することで対象となる要素を変更できます。

data-dom-speaker="body" の例
<body>
  <main>
    <h1></h1>
    <p></p>

    <div data-dom-speaker="body">
      <!-- ここから対象になる -->
      <section>
        <h2></h2>
        <p></p>
      </section>
      <!-- ここまで対象になる -->
    </div>
  </main>
</body>

また、<html data-dom-speaker="body"> のように指定することで、<head> 要素以下もすべて対象に含めることができます。

<html> 要素に data-dom-speaker="body" を指定する例
<html data-dom-speaker="body">
  <!-- ここから対象になる -->
  <head>
    <!-- ... -->
  </head>
  <body>
    <!-- ... -->
  </body>
  <!-- ここまで対象になる -->
</html>

data-dom-speaker="ignore"

この見出しのリンク

要素に data-dom-speaker="ignore" を付けると、その要素自身が対象外となり、データには含まれなくなります。子孫要素には影響しません。

data-dom-speaker="ignore" の例
<body>
  <h1></h1>
  <p></p>

  <!-- `<section>` のみが対象外になる -->
  <section data-dom-speaker="ignore">
    <!-- 子孫要素には影響しない -->
    <h2></h2>
    <p></p>
  </section>
</body>

data-dom-speaker="ignore-all"

この見出しのリンク

要素に data-dom-speaker="ignore-all" を付けると、子孫要素も含めて対象外となり、データには含まれなくなります。

data-dom-speaker="ignore-all" の例
<body>
  <h1></h1>
  <p></p>

  <!-- `<section>` 以下の子孫要素も含めて対象外になる -->
  <section data-dom-speaker="ignore-all">
    <h2></h2>
    <p></p>
  </section>
</body>

data-dom-speaker="rest"

この見出しのリンク

要素に data-dom-speaker="rest" を付けると、その要素は読み上げられなくなるので、休符として使用することができます。また、この属性を指定した要素は、ディスプレイエリアではタグ名ではなく <…>(三点リーダ)が表示されます。

以下の例では「div」が 3 回読み上げられますが、最後の <div> は読み上げられません。

data-dom-speaker="rest" の例
<body>
  <div></div>
  <div></div>
  <div></div>
  <!-- 最後の `<div>` は読み上げられない -->
  <div data-dom-speaker="rest"></div>
</body>

本アプリについて、現時点では以下の問題を認識しています。

繰り返しになりますが、Speech Synthesis は実験的な機能なため、OS やブラウザによって挙動が大きく異なります。

ダウンロードされている音声データによって異なるのはもちろんですが、同じ名前の音声であっても、デバイスや OS によって読み上げ方が異なることがあります。

例えば、iPadOS と macOS で同じ「Cellos(en-US)」という音声を鳴らすと、iPadOS では「RATE」で速度を細かく調整できますが、macOS では速度が変わりません。

OS のバージョンに起因するかもしれませんが、細かくは調査できていません。

音声データの不確実性

見出し「音声データの不確実性」

音声の読み上げ方については、SpeechSynthesisUtterance で設定しています。このインタフェースには pitchrate といったプロパティがあり、本アプリの「PITCH」や「RATE」の値はこれらのプロパティに反映されます。

しかし、音声データによっては pitchrate に対応していないことがあり、それを調べる方法が見つからず、実際にレンジスライダーを動かしてみないと対応の可否がわかりません。

また、rate については、音声を切り替えたときには即時反映されずに、レンジスライダーを操作すると反映されることがあります。

加えて、原因は不明ですが「Google」のプレフィックスが付いた音声データでは、rate を変更すると音声が流れなくなってしまいます。苦肉の策ですが、これらの音源については rate の値が変わらないように調整しています。

このように、選択した音声データによって思わぬ不具合が発生する可能性があります。

音声の同時再生はできない

見出し「音声の同時再生はできない」

Speech Synthesis では複数の音声を同時に鳴らすことができません。

例えば、ウィンドウをいくつか開いてから「PLAY」ボタンを選択すると、順番に音声が再生され、いずれか 1 つの音声しか流すことができません。

Web Audio API との連携の難しさ

見出し「Web Audio API との連携の難しさ」

開発当初は Web Audio API で簡単なリズムトラックを鳴らすことも試みました。

同時に音を鳴らすこと自体はできるのですが、音声の種類によって読み上げのタイミングに個性があり、テンポを同期させるのが難しかったので見送りました。

また、Web Speech API の音声を Web Audio API に接続して加工するといったことも試したかったのですが、方法を見つけることができませんでした。

HTML は同じ構成要素がパターン化して繰り返して出現することが多いため、音声として聞くことで、そのリズムを楽しむことができます。

対応するかは今のところ未定ですが、追加機能としては以下が考えられそうです。

  • 「STORAGE」からアイテムを削除した後に元に戻せるようにする(undo)
  • 「STORAGE」に登録した後にアイテム名を変更できるようにする
  • 「STORAGE」に登録したアイテムの順番を並べ替えられるようにする
  • 変更したデータのエクスポート機能
  • 自動再生時のループのオン・オフ切り替え
  • 読み込んだファイルから favicon を取得して表示
  • タイトルの編集を contenteditable から <input> に変更
  • レンジスライダーの値を数値から直接編集できるようにする
  • ブックマークレットやブラウザ拡張機能

また、HTML の構造を解析して、Web Audio API で音を鳴らす派生版のアプリも考えられますが、自由度が高いだけに完成までの道のりは長くなりそうです。

「STORAGE」の UI については、以下のドラムシンセのウェブアプリを参考にしました。