朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

オリジナルの音声アシスタントを作ろう (2) - Media Capture and Streams API

はじめに

こんにちは。朝日ネットでWebアプリケーションの開発を行っている tommy です。

前回Web Speech API を使ってブラウザで音声認識をやってみました。 ただし、現状では対応ブラウザが少ない上に、まだ不安定という問題がありました。

そのため、今回はブラウザでマイクの音を拾うことができる API の Media Capture and Streams を使ってみようと思います。

今回の大まかな流れ

  1. getUserMedia 関数を使いマイクからの入力のストリームを得る。
  2. Web Audio API を使い取得したストリームから音声データを得る。
  3. 音声データを加工し、グラフとして表示したり、音声ファイルを作ったりする。

getUserMedia

getUserMedia は Media Capture and Streamsで規定されている、マイクから音声、カメラから動画(写真) を得るための関数です。 Media Capture and Streams は WebRTC(Web Real-Time Communication) を実現するための一環として推し進められているものです。

もともとは、 navigator オブジェクトがこの関数を直接持つことになっていましたが、最新仕様では MediaDevices オブジェクトが持つように変更になりました。その際に関数の使用方法も若干変わっています。ただし、こちらの仕様で実装されているブラウザはまだそれほどありません。

ブラウザ互換性を保つには MDN のサンプルコード を参考にするのが早いと思います。

navigator.mediaDevices = navigator.mediaDevices || ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia) ? {
   getUserMedia: function(c) {
     return new Promise(function(y, n) {
       (navigator.mozGetUserMedia ||
        navigator.webkitGetUserMedia).call(navigator, c, y, n);
     });
   }
} : null);

navigator.mediaDevices.getUserMedia({audio : true}) // 今回は音声だけ欲しい。
.then(function(stream) {
 // streamを使っていろいろする。
})
.catch(function(err) {
  console.log(err.name + ": " + err.message);
});

getUserMedia を呼ぶと、thenで渡した関数に MediaStream オブジェクトがわたってかえってきます。このオブジェクトを使いマイクからの入力を取得します。

こちらの API も前回同様、端末のマイクにアクセスするため、https でページを表示 している必要があります。また、初回の getUserMedia 呼び出し時に利用者へマイクへのアクセスの許可が求められます。

Web Audio API

MediaStream オブジェクトを得られたら、そこから音声データを取り出すために Web Audio API を使用します。 こちらは、ブラウザ上でさまざまな音声加工を可能にしてくれる API です。

Web Audio API は 音声入力元(今回の場合マイク) と 音声出力先(今回の場合は必要は無いがスピーカなど) の間に、ノードと呼ばれるオブジェクトを接続し、各ノードで音声データを加工します。

f:id:a-tommy:20180720153529p:plain

これはマイクとスピーカーの間に アンプやエフェクターをつなげるイメージと同じで、音声加工の業界では一般的な概念らしいです。

今回は、まずマイクに MediaStreamAudioSourceNode をつなげ、次に ScriptProcessorNode をつなげます1 。ScriptProcessorNode から生の音声データが得られます。最後にスピーカーを接続します。 以下のようなイメージです。

f:id:a-tommy:20180720154454p:plain

コードにすると以下の様になります。

var AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
navigator.mediaDevices.getUserMedia({audio: true}).then(function(stream) {
  // ノードの準備
  var source = audioCtx.createMediaStreamSource(stream);
  var scriptNode = audioCtx.createScriptProcessor(4096, 1, 1);
  scriptNode.onaudioprocess = function (e) {
    var input = e.inputBuffer.getChannelData(0); // 生の音声データ
    // inputを使っていろいろやる
    // ...
    // 入力をそのまま出力する
    // var output = e.outputBuffer.getChannelData(0);
    // for (var i=0;i<input.length;i++) {
    //   output[i] = input[i];
    // }
  };
  // それぞれのノードを接続
  source.connect(scriptNode);
  scriptNode.connect(audioCtx.destination);
}).catch(function(e) { alert("開始できません(" + e + ")。"); } );

createScriptProcessor はもともとは createJavaScriptNode という名前で JavaScriptで直に音声データを加工するためのノードを作成します。 引数の意味はそれぞれ、バッファサイズ・入力のチャンネル数・出力のチャンネル数 となっており、 指定したバッファサイズ分のデータが溜まると onaudioprocess 関数が呼ばれます。 このバッファのデータが何秒分のデータになるかは サンプルレート の値によって変わってきます。

onaudioprocess 関数内では e.inputBuffer.getChannelData によって音声データを取得することができます。 取得したデータは -1 から 1 までの値が入った Float32Array となります。

コメントアウトしていますが、 e.outputBuffer.getChannelData で得られる配列に値を入れるとスピーカーに出力されます。 (ただし、環境によっては、これをやると、ハウリングして大変なことになります。)

サンプルコード

Media Capture and Streams と Web Audio API を実際に使ったプログラムを書いてみました。 サンプルのコードは以下のブラウザにて動作確認をしています。

  • iOS版 Safari 11
  • PC版 Google Chrome (バージョン: 67.0.3396.99)
  • Android版 Google Chrome (バージョン: 67.0.3396.87)
  • PC版 Firefox (バージョン: 61.0.1)

サンプル1: 波形グラフを出力する

先ほどの入力データをそのままグラフに出力するシンプルなプログラムです。

f:id:a-tommy:20180720155200j:plain

https://a-tommy.github.io/assistant/graph
(コード)

開始を押すとマイクから拾った音の、グラフへの出力が開始し、終了を押すとマイクからの入力を終了します。 グラフ描画には d3 の ver.5 を利用しています。

コードの中身のおおよそとしては、先ほどの input 配列の内容をそのままグラフに出力しているだけです。 グラフの横幅のサンプル数は固定なので、グラフが表す時間は環境によって(サンプルレートの違いにより)変わります。

サンプル2: Flac ファイルとしてダウンロードする

今度は先ほどのコードを改変し、Flac ファイルとしてダウンロードするコードにしてみました。 開始を押すと録音を開始し、終了を押すと録音した内容を Flac ファイルとしてダウンロードします。

https://a-tommy.github.io/assistant/flac
(コード)

Flac にエンコードするのに libflac.js を利用させてもらっています。 コード簡素化のために一括でエンコードするようにしているので、長時間の録音は厳しいと思います。

最終的に Flac ファイルの Blob をダウンロードするのではなく サーバに送信する様にすることも可能です。

まとめ

いかがでしたでしょうか。 今回は、ブラウザでマイクから生の音声データを取得し、そこから音声ファイルを作るところまでをやってみました。

次回は、今回作成したファイルをサーバで受け取り音声認識処理をしていく部分を作成しようと思います。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。


  1. 本記事では ScriptProcessorNode を使っていますが、ScriptProcessorNode は問題を抱えているため現在非推奨となっています。代わりとして AudioWorklet が策定されていますが、現時点では対応ブラウザは Chrome くらいしかありません。 [参考]