webブラウザ上での音声区間検出 (Voice Activity Detection) を試してみたら意外と使えそうだった

# このドキュメントは何？

webブラウザ上で音声区間検出 (Voice Activity Detection, VAD) をやってみた結果をまとめています。

> ※ **注意**: 本記事では一部ソースコードにて Navigator.getUserMedia() (MediaDevices.getUserMedia()[^1]) を使用しています。2017年8月現在、SafariおよびiOSのwebブラウザでは、getUserMedia()をサポートしていないため該当のソースコードは正しく動作しない可能性が高いです[^2]。


# 音声区間検出って何？

音声区間検出とは、音声波形の中で人などが発話している時区間を検出することです。音声データの圧縮技術等に活用されています。

# ライブラリの検討

今回は、webブラウザ上で音声区間検出をやってみたかったので、 [vad.js](https://github.com/kdavis-mozilla/vad.js) を試してみる事にしました。このライブラリは比較的低学習コストで利用できるので利用方法さえ間違えなければオススメです。（おまけに軽量）。

# vad.jsってどうやって使うの？

## まずは使ってみる

とりあえず vad.jsのREADMEにならって使ってみましょう。README中のサンプルコードを参考に以下のようなHTMLファイルを用意します。また、このHTMLファイルのあるディレクトリ（フォルダ）を基準に `lib/vad.js` のパスの位置に前述の `vad.js` を配置しておきます（要ダウンロード）。

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>VAD Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
    <script type="text/javascript" src="lib/vad.js"></script>
    <script type="text/javascript">
    <!--

    // webブラウザ間のAudioContext (音声処理オブジェクト)表記ゆれを吸収する
    window.AudioContext = window.AudioContext || window.webkitAudioContext;

    // AudioContextを作成する
    var audioContext = new AudioContext();

    // 音声処理初期設定関数（下記getUserMediaのコールバックとして、マイクとの接続開始時に実行） 
    function startUserMedia(stream) {

        // 音声ストリーム音源オブジェクトの作成
        var source = audioContext.createMediaStreamSource(stream);

        // VAD のオプション設定 (詳細後述)
        var options = {
            source: source,  // 区間検出対象となるストリーム音源オブジェクトの指定
            voice_stop: function() {console.log('voice_stop');}, // 音声区間検出開始時ハンドラ指定
            voice_start: function() {console.log('voice_start');} // 音声区間検出終了時ハンドラ指定
        }; 
    
        // VADオブジェクト作成 (なお、本オブジェクトは以降使用する必要はない)
        var vad = new VAD(options);
    }

    // webブラウザ間のgetUserMedia(音声／画像デバイスアクセス用オブジェクト)表記ゆれを吸収する
    navigator.getUserMedia = navigator.getUserMedia || 
                             navigator.mozGetUserMedia || 
                             navigator.webkitGetUserMedia;

    // getUserMediaを起動し、マイクアクセスを開始する
    navigator.getUserMedia(
        {audio: true},  // オプション(音声(audio)デバイスへアクセス)
        startUserMedia, // 成功時コールバック処理 (上記にて定義したstartUserMedia関数)
        function(e) {   // エラー時コールバック処理
          console.log("No live audio input in this browser: " + e);
        }
    );

    -->
    </script>
  </head>
  <body>
  </body>
</html>
```

上記のHTMLファイルをwebブラウザで開くと、ブラウザが「マイク使用許可」を確認してきます。「許可する」のボタンを押下すると録音（とは言っても音声ファイル化はせず、ブラウザ上で録音音声ストリームを取得）が開始されます。

ブラウザ上で音声区間の検出開始／検出終了される度に、ブラウザのコンソールに ``voice_start`` / ``voice_stop`` が表示されます。（ブラウザのコンソールはF12キーの押下等で開いておきましょう）

どうですか？意外と違和感なく検出してくれる事がわかりますね。

## 結局手順ってどうなってるの？

vad.jsを利用するための最小限の手順を以下にまとめます。

1. `getUserMedia()` 等により音声ストリームオブジェクト(`MediaStream`)を取得する
2. `AudioContext.createMediaStreamSource()` により音声ストリーム音源オブジェクト(`MediaStreamSource`)を生成する
3. VADオブジェクト用のオプションに以下を設定する
	* `source:` 音声ストリーム音源オブジェクト
	* `voice_start: ` 音声区間開始検出時の処理ハンドラ
	* `voice_end: ` 音声区間終了検出時の処理ハンドラ
4.  `new VAD(オプション)` によりvad.jsと音声ストリームオブジェクトを紐づける
5. マイク等から音声ストリームを流し込み開始

わずか5ステップ！簡単ですね。

## 設定可能なオプションについて

VAD はオブジェクト作成時にオプション設定値を指定する事ができます。一部の設定値はデフォルト値のままで問題無いため、環境や利用用途に合わせて設定した方が良い設定値のみについて以下で説明します。

| オプション名 | 説明 | デフォルト値 | 特記事項 |
| :--------: | :---- | ----: | :---- |
| source | 区間検出対象となるストリーム音源 (MediaStreamSource) オブジェクト | null | |
| voice_start | 音声区間検出開始時ハンドラ | function() {} | |
| voice_stop | 音声区間検出終了時ハンドラ | function() {} | |
| energy_threshold_ratio_pos | 音声区間検出開始 エネルギー閾値レティオ設定係数 | 2 | 値が大きいほど開始を検出しにくくなる. 100程度を設定すると大声でも検知しない. 逆に-1程度を設定すると雑音でも検知してしまう. 実際の利用においては0.1から10.0の範囲で設定するのが現実的. |
| energy_threshold_ratio_neg | 音声区間検出終了 エネルギー閾値レティオ設定係数 | 0.5 | 値が大きいほど終了を検出しにくくなる. 1以上を設定するとちょっとでも雑音が入っていると終了しない. 逆に0.01程度を設定すると開始しても即終了してしまう. 実際の利用においては0.1から1.0の範囲で設定するのが現実的. |
| energy_integration | 音声エネルギー積分計算における影響度レティオ設定係数 | 1 | 値が大きいほど過敏に開始および終了を検知するようになる (ただし、100程度を設定すると開始を検知しない. また、-1程度を設定すると終了を検知しなくなる). 実際の利用においては0から1.0の範囲で設定するのが現実的. |

 ``energy_threshold_ratio_pos``, ``energy_threshold_ratio_neg``, ``energy_integration`` の3値については、実際にマイクから音声を入力して色々と試してみた結果、以下の値が良い感じで音声区間を検出してくれる事がわかりました。

| 設定パラメータ |  値 |
| :----------: | --: |
| energy_threshold_ratio_pos | 1.5 |
| energy_threshold_ratio_neg | 0.75 |
| energy_integration         | 0.25 |

# 課題とか注意点とか

## 人間の発話以外も検出できてしまう

vad.js は音響モデル等は特に使用しておらず、単純に音エネルギーを元に音声区間か否かを検知しているシンプルなライブラリとなっています。このため、人間以外の音声（犬の鳴き声、さらには扇風機などの機械の音）でも反応する場合があります。その点は考慮して利用する必要があります。

## 音声区間開始検出の課題

上記サンプルコードを試してみるとわかりますが、声が入力されてから音声区間を検出開始するまでに少しばかりの遅延が発生します。これは、このライブラリに限らずリアルタイムでマイクから入力される音声を相手にする場合はどーやっても直面する課題です。このままだと、「音声区間検出された期間だけ音声を録音する！」のような仕組みを実装しようとしてもはじめの部分だけ抜け落ちて録音することになってしまいます。

この課題については、常に音声ストリームをバッファリングしておく等により、ある程度対策は立てられる可能性が高いです（後日調査して記載）

# まとめ

本記事ではwebブラウザ上で音声区間の検出を試してみました。

この技術と音声認識技術等を併用すれば、自前で音声入力可能なチャット画面とかSiriみたいなバーチャルエージェントを作成できちゃうかもしれません。夢が広がりますね。


[^1]: Navigator.getUserMedia()は[MediaDevice.getUserMedia()に今後置き換えていく予定との事](https://developer.mozilla.org/ja/docs/Web/API/Navigator/getUserMedia)らしいが現状 (2017年8月) `MediaDevice` をサポートしているブラウザがFirefoxのみなので本記事では `Navigator.getUserMedia()` で記載。

[^2]: iOS11 から[サポートされるとかしないとか噂あり](https://caniuse.com/#search=getusermedia)