これはドリコム Advent Calendar 2017の13日目です。
12日目は平石陽介さんによる、Go言語とDockerでインフラエンジニア向けのシンプルなCLIツール開発環境を作るです。

サーバーサイドエンジニアの廣田です。
ドリコム Advent Calendarと言いながら、業務で全く使っていないWeb Audio APIの話をしたいと思います。
今回はAudioBufferについて掘り下げてみます。

Web Audio APIとは

本題に入る前に、Web Audio APIとは、ブラウザ上で音声信号処理を可能にするJavaScriptのAPIです。
単に音を鳴らすだけでなく、信号処理もできるというところがポイントです。
W3Cで仕様策定が進められており、モバイル含め主要ブラウザで動作します

Web Audio APIの第一歩

Web Audio APIを触ってみたい、という方にはこちらのCodelabが参考になります。
その他にもWeb Audio APIの入門記事や解説記事はググればいくらでも見つけられますが、Web Audio初心者がまず最初に触るのはOscillatorNodeAudioBufferになります。
やはりこのAPIを使って初めてやることは何か音を出してみることなので、入門記事の多くで、OscillatorNodeは波形を指定して単音を鳴らすのに、AudioBufferはオーディオファイルを読み込んで再生するのによく使われます。

AudioBufferについて

今回はAudioBufferについて掘り下げていきます。
AudioBufferでオーディオファイルを再生する際、decodeAudioDataでデコードしたオーディオファイルをcreateBufferSourceで作ったバッファに入れて…といった順序を踏みます。
ここではAudioBufferが具体的にどんなデータを持つのかを見ていきたいと思います。
W3Cの草案によれば

  • 1から1の範囲で正規化されたリニアPCMのデータであること
  • 1つ以上のチャンネルを持つこと
  • 非常に短いPCMデータに使われること

がざっくりわかります。

なお、非常に短いPCMデータとはおおよそ1分未満の長さとのことで、それ以上の長さになる音源やストリーミングの場合にはHTMLのaudio要素を取得できるMediaElementAudioSourceNodeを使えとも書かれています。
いつもLTの一発ネタで長い音源をAudioBufferにぶち込んですみません。

リニアPCMについて

音声のように波形で表されるアナログ信号を数値の連続でデジタルに表したものの1つです。
JavaScriptからは外れた話になりますが、音声信号処理を扱うWeb Audio APIではこの手の話は避けては通れません。
とはいえ、自分で音声の波形からPCMデータを求められるようになる必要はありません。
今回はパラメーターとして大事な「サンプリング周波数」と「量子化ビット数」について簡単に説明します。

サンプリング周波数とは、1秒あたりの波形を何個の数値に置き換えるかです。
例えばサンプリング周波数が44.1kHzというと、1秒あたり44,100の数値列で表すことになります。
そして、これらの数値列の最大値を決めるのが量子化ビット数です。
ファミコン風の音源を8bit音源などのように呼ぶことがありますが、この「8bit」がまさに量子化ビット数です。
量子化ビット数が8なら、0〜255の数値で表すといった具合です。
ちなみに、CDの音声はサンプリング周波数が44.1kHz、量子化ビット数が16bitとなっています。
Web Audio APIではこれらのデータを-1から1に正規化するので、実際は32ビット不動小数点の配列Float32Arrayとして扱われます。

チャンネルについて

チャンネルは先のリニアPCMのように難しい事はありません。
音源を幾つの方向から鳴らすかという意味です。
一般的なイヤホンでしたら左右の2方向からなので2chといった具合です。
オーディオファイルがリニアPCMのデータに変換される際、このチャンネルの数だけリニアPCMのデータが作られます。
つまり、AudioBufferはチャンネル数分のFloat32Arrayを持っています。
厳密にはリニアPCMはバイナリデータを扱うArrayBufferに格納されるのですが、ArrayBufferは通常の配列のような計算ができないためAudioBufferが扱いやすいようにFloat32Arrayとしてアクセスできるようにしてくれているのです。

AudioBufferのまとめ

ここまでの内容で、

  • -1から1の範囲で正規化されたリニアPCMのデータであること
  • 1つ以上のチャンネルを持つこと

の内容がわかりました。
つまり、AudioBufferはチャンネル数分のリニアPCMをFloat32Arrayとして持っていると捉えれば良いでしょう。
あとはgetChannelDataで指定したチャンネルのFloat32Arrayを取り出して、楽曲と連動したオーディオビジュアライザに利用したり波形を編集してエフェクトをかけたりと好きにできます。
それではみなさん、良きWeb Audioライフを!

さらに踏み込んでAudioWorklet

さて、好きにできると言っても、どのような計算をすれば波形を編集できるかは、DTMや音声信号処理のより専門的な知識が必要になってきます。
今回は、Web Audio APIで波形を編集するためのAudioWorkletについて簡単な紹介をして終わりたいと思います。
なお、「Web Audio APIのAudioBufferと仲良くなる」については前項で終わりです。
ここから先はまとめもオチもないおまけですが、ここまででWeb Audio APIに興味が湧いた方の参考になればと思って進めます。

AudioWorkletは、Web Audio APIで用意されているgainNodedelayNodeの他に、実際に信号処理の計算を書いて自作のAudioNodeを用意することができる機能です。
ただし、この機能はまだ主要ブラウザには実装されていません。
Chrome Canaryの最新版では実装されており、以下のオプションを有効にすることによって使えるようになります。
chrome://flags/#enable-experimental-web-platform-features

AudioWorkletでもAudioBufferと同じく、リニアPCMのデータを持ちます。
AudioWorkletは入力と計算後の出力を複数持つことができますが、その1つ1つはチャンネル数分の長さの配列になります。
そして、それらの各要素が各チャンネルのリニアPCMを格納したFloat32Arrayになります。
配列の配列の配列になるので文字で書くとややこしいですが、先のChrome CanaryでAudioWorkletが実装された際に公開されたデモが非常にわかりやすいです(音がなります)。

例えば、“Hello AudioWorklet!”で公開されているデモは何も計算を行わずただ音を素通りさせるだけのフィルターが実装されていますが、どのように入力を取って出力に反映されるかがよくわかる例となっています。

process(inputs, outputs) {
let input = inputs[0];
let output = outputs[0];
for (let channel = 0; channel < output.length; ++channel) {
output[channel].set(input[channel]);
}

return true;
}

このデモではOscillatorNodeで生成した単音を入力に取りそれをそのまま出力するので、入出力はそれぞれ1つだけ用意されています(2-3行目)。
そして、入力のチャンネル1つ1つをそのまま出力にコピーしています(4-6行目)。

次の“Noise with AudioParam Modulation”は単音にホワイトノイズを重ね合わせるデモですが、ここでFloat32Arrayを具体的に扱っています。

process(inputs, outputs, parameters) {
let output = outputs[0];
let amplitude = parameters.amplitude;
for (let channel = 0; channel < output.length; ++channel) {
let outputChannel = output[channel];
for (let i = 0; i < outputChannel.length; ++i) {
outputChannel[i] = 2 * (Math.random() - 0.5) * amplitude[i];
}
}

return true;
}

重ね合わせるための波形を用意するだけなので、入力はなく出力のみです。
具体的に出力するチャンネルの1つにデータを書き込んでいるのは6-8行目になります。
ホワイトノイズなのでランダムな値を-1から1の範囲に収まるように計算していることがわかるかと思います。
ここで、amplitudeはホワイトノイズの振幅を決めるパラメーターなのですが、これが配列になっているのも大きなポイントです。
Web Audio APIではAudioContext内の時間軸でパラメーターの値を自動で変化させるオートメーション機能がありますが、AudioWorkletで自作したパラメーターもその変化の対象とすることができるのです。

さいごに

ここまで読み進めてこられたかたは、Web Audio APIでオーディオファイルがどのように扱われるか、そしてそれらをどのように処理するかの一端が見えたかと思います。
先の項で少しだけ触れたAudioParamのオートメーションや、AudioWorkletの前身となるScriptProcessorなど、まだまだ話し足りないことはたくさんありますが、この辺りはすでにネットに存在する解説記事に十分な情報があるかと思います。
それでは改めて、良きWeb Audioライフを!