Nodejsでオーディオ信号処理。Web Audio APIを調べてみた。
こんにちは!
JavaScriptは20年も前から知っているのに、昨今のNodejsとWeb browserのJavaScriptの状況を調べるにしたがって、これらの技術の奥深さに感心しきりな、しずかなかずしです。
前回は、naudiodonというNodejsのモジュールを使ってRaspberry PiにつないだUSBマイクから音声を取り出す、という記事を書きました。
naudiodonというモジュールをインストールする際のハマりポイントや、モジュールを使った実際のプログラミングに関してお話しました。
しかし、この記事ではマイクから取得したデータをファイルに書き込むだけ。取得したオーディオ信号そのものには何の変更も加えていません。
これでは、"信号処理した"とは言えませんね。
そこで、今回は、JavaScript(特にNodejs)でオーディオ信号の処理を実行する際に必要な技術や知識を整理します。
サンプリングしたデータの理解
デジタル化されたオーディオ信号を扱うには、コンピューターの中で波形がどのように表現されているかを知る必要があります。
我々人間が聴く音は、空気の振動です。つまり波です。
波をコンピューターの中に表現する時に、アナログの波としては扱えないので、デジタル化します。いわゆる、サンプリングと呼ばれるプロセスですね。
アナログの波は、横軸が時間、縦軸に波の振幅をとるようにグラフに書くと以下の図のようになります。下の図では、赤の線がアナログの「波」とすると、コンピューターのメモリーに格納されるのは青色のポイントです。つまり、飛び飛びの値です。サンプリングプロセスは、アナログの波を、以下の図のように離散的に、本来のアナログ値に「近い値」としてメモリー上のデータに格納することによって行われます。
ここで、サンプリングの精度に関して考えてみます。
上の図では、振幅(波の幅)は-8から7の16段階の値として表現されます。たったの16段階です。ここで、16段階の中の4という値(左から3つ目の青丸)を取り上げます。この4という数値は波が細かくなればなる程、デジタルの4で表される「アナログ値」の幅が大きくなります。
例えていうと、100点満点のテストの50点と51点という結果の違いが、10点満点のテストの結果だと5点と丸められてしまうのに似ています。
つまり、点数の開きを精度高く比較したければ、16段階よりももっと細かくしていく必要があります。実は、これが音質に大きく影響します。音質が良いと言うのは、再生した時に如何に滑らかにアナログ信号に近い音を表現できるか、ということにかかっているからです。
16段階というのはコンピューターの世界では、4ビットで表現可能です。これを16ビット(2の16乗=65536段階)まで拡張したのがCD(Compact Disc)のフォーマットです。
前回ご紹介したソースコードにもう一度登場してもらいます。
ここで、sampleFormat: portAudio.SampleFormat16Bitと記載のあるコード。これが波の大きさ(振幅)を16ビット段階、つまり65536段階で録音しますよ、というパラメータになります。「ビット長」や「ビット幅」、「Bit Depth」と呼んだりします。
同様のことが、横軸である時間に対しても当てはまります。横軸も細ければ細かいほどアナログの波を正しく再現できる訳です。この場合の尺度は、1秒間に何回値を取るか、という尺度になります。例えば、1秒間に20回青丸を取得するのが20Hz(ヘルツ)。44100回取るのが、44.1kHz(キロヘルツ)です。CDは44.1kHzで取得します。この値は「サンプリング周波数」とか「サンプリングレート」と呼ばれます。上記のソースコードでは、sampleRate: 44100というコードが、サンプリング周波数の指定です。
このくらいの精度で音声を取ると、音楽としても割と「聴ける」品質のオーディオ信号がサンプリングできる、という訳ですね。
サンプルフォーマットの注意点
ビット長やビット幅と呼ばれる波形の振幅を数値化する際に注意点があります。
それは、16ビットや8ビットといったデータサイズの中に、数値がどのように格納されるか、ということです。前回にも登場したPort Audioのライブラリの解説にSampleFormatに関する記述があります。
The floating point representation (paFloat32) uses +1.0 and -1.0 as the maximum and minimum respectively.
paUInt8 is an unsigned 8 bit format where 128 is considered “ground"
先程16ビットは63356段階で表現と言いましたが、実際には、-32768から32767までの符号付きの整数値として表現されます。パソコンならC言語の処理系でsigned shortが16ビットのことが多いのですが、その範囲の値を取ります。
とろこが、32ビットの場合はfloat(浮動小数点)形式で表現され、この場合は波形の振幅は、-1.0から1.0の間に収まるように格納されます。
さらに面倒なのは、上記の引用にもあるように、8ビットの場合は、256段階で表現されますが、この時は正の整数の範囲(0〜255)で波形を描くので振幅の中心地(ground)は128になります。
なんともややこしい話です。
例えば、数値演算のライブラリがfloat(32ビット浮動小数点)で処理を行う場合には例えば、CDの形式(signed int 16ビット、44.1kHz)のオーディオ信号を処理するような場合には、1サンプル毎に-1.0から1.0の範囲に変換する必要があるのです。
JavaScriptならWeb Audio APIを使おう
JavaScriptでオーディオ信号処理を実行すうるのに最近ではちゃんとしたフレームワークが定義されています。
それがWeb Audio APIです。
MDN web docsのブラウザ互換表をみると、ChromeやSafariといった多くのモダンブラウザでサポートされている仕組みです。
この仕組みを使うと、Inputとして、シンセサイザーのような発振器(Oscillator)を使ったり、ゲインや周波数特性を操作するようなEffectsモジュールを使うことが可能で、それらを組み合わせて最終的にDestinationとしてのスピーカーから音を鳴らす、という処理を実行することができます。これらの機能は、AudioNodeというベースとなるインタフェースを実装しています。このインターフェースには、AudioNode.connect()というメソッドがあります。この関数は、自分の次に処理させるAudioNodeを引数に取ります。connect()関数は、受けっ取ったNodeをreturnで返すので、nodeA.connect(nodeB).connect(nodeC)のように、入力から出力に向かって、さまざまなAudioNodeのインスタンスを「接続」することができます。
このように、AudioNodeを実装したEffectsをAudioContextに組み込むことで、上記の絵のような一連のオーディオ処理を行う「流れ」を構築できるような仕組みになっています。
そして、AudioNodeの入力となるオーディオのサンプルデータは、AudioBufferというクラスで扱います。
AudioBufferはチャンネルごとのバッファがgetChannelData()で取り出せて、取り出したBufferはFloat32Arrayです。値は-1.0から1.0の浮動小数点の数値で扱いやすいのです。
NodejsでWeb Audio APIを使う
ここまでは、Web Audio APIという名前だけあってWebブラウザ側の実装という話でした。
が、AudioBufferやAudioNodeといった同じインターフェースをNodejs向けに実装しているモジュールがあります。
ちょっと調べた感じだと以下が使えそうです。
両者がどのWeb Audio APIのフレームワークの中でどのインターフェースを実装しているか、サポート状況は以下のテーブルがわかりやすいです。
上記のリンクには、WAAPISimというモジュールとの比較もありましたが、WAAPISimはWeb Audio APIをサポートしていなブラウザのコンパチビリティを担保するためのモジュールのようでした。Nodejsでオーディオ信号処理に使いたい、という用途としては有用ではないのでここでは置いておきます。
さて、上記のコンパチビリティ比較をみるとweb-audio-engineが優秀に見えます。
一方、私の用途としては、web-audio-api系のRepositoryで公開されている、pcm-utilというモジュールが、サンプルフォーマットの変換をした上でAudioBufferに格納するなどに便利そうでした。
これらのモジュールの使い道は、別途詳細記事を起こすつもりです。おたのしみに。
ブラウザ上のJavaScriptでHTML5と組み合わせてオーディオ処理をする内容に関しては、以下のように書籍も色々出ているようです。
私のようにRaspberry Piでちょっとした信号処理を行いたい、という用途ではどうなのでしょうか…
さらなる探求が必要です。