🧊

WebAudioでの音声信号処理

WebAudioでの音声信号処理 〜入門以前〜 - console.lealog();

この内容を踏まえて、実際にコードを書いていく際のポイントなど。

2019年末の情報です。

AudioContext

今も昔も、WebAudio APIを触るなら絶対に必要なやつ。

const ctx = new AudioContext();

基本的にはコレで万事よしと言いたいところですが、Safariは今でも`webkitAudioContext`とプレフィックスが必要。

あんま知られてないと思うけど、サンプリングレートの指定もできる。

const ctx = new AudioContext({ sampleRate: 8000 });

ただしこれも、Safariでは動かない・・。

PCMデータを得る

2019年の末の選択肢としては2つ。

  • ScriptProcessor
  • AudioWorkletProcessor

それぞれ見てみる。

ScriptProcessor

  • いわゆるデファクト
  • ただしメインスレッドで動くので、非推奨とされ仕様からは消された存在
    • 正確には、コールバックがメインスレッドで呼ばれてしまう
const ctx = new AudioContext();

// bufferSize, numberOfInputChannels, numberOfOutputChannels
const processorNode = ctx.createScriptProcessor(1024, 1, 1);
processorNode.onaudioprocess = ev => {
  const { inputBuffer, outputBuffer } = ev;

  // ここでinputBufferから各チャンネルを取り出す
  // もちろん最初に指定したチャンネル数しかない
  // それぞれは AudioBuffer というクラスになってる
  // こうすると得られるサンプルは、Float32Array
  const samples = inputBuffer.getChannelData(0);

  // 音声処理はここでやる

  // 最終的にoutputBufferへ返す
  // 単にbypassするならこう
  outputBuffer.getChannelData(0).set(samples);
};


// IN
fooNode.connect(processorNode);
// OUT
processorNode.connect(ctx.destination);

サンプリングレートに応じた頻度で、この`onaudioprocess`イベントが発火するようになってる。
なので`bufferSize`が`1024`で、`sampleRate`が`8000`の場合は、7回/秒くらいで呼ばれる。

ちなみに量子化ビット数は`Float32Array`なので32bitと、中々の高音質。

1イベントで得られるサンプルの長さは`bufferSize`によって決まり、範囲は`256~16384`らしい(Chromeいわく)
引数を`0`にすると、ブラウザが自動で選んでくれる + それがおすすめらしい。

AudioWorkletProcessor

`ScriptProcessor`が非推奨になって、こっちを使えというやつ。

詳しいことは以下の記事を。
ちょっと古いけど、状況は何も変わってなかったので・・。

AudioWorkletについて調べたメモ - console.lealog();

ただこのモダンなAPIは、Chromeでしか実装されておりません!

こっちは`process()`が呼ばれるたびに、チャンネルごとの`Float32Array(128)`が固定長で得られる。
呼ばれる頻度は変わらずサンプリングレートに依存していて、8000Hzの場合は`8000 / 128 = 62.5回/秒`くらい。

こっちは`AudioBuffer`ではなくただの`Float32Array`なので、`getChannelData()`とかできないので注意。

はやく普及してくれ〜〜。

WebRTCもどき(エンコード

OPUSやPCMUやFLACやらなんでもいいけど、独自にエンコードしたい場合。

基本的にはもう察しが付くと思いますが、`ScriptProcessor`か`AudioWorkletProcessor`で、PCMをエンコードして飛ばす。

`ScriptProcessor`ならメインスレッドにいるのでそのまま`WebSocket`などにつなげる。
単に`WebWorker`を作って、そっちにPCMを渡して、処理 + `WebSocket`で飛ばすのも手。

`AudioWorkletProcessor`は`AudioWorkletGlobalScope`にいるので、`WebSocket`がありません。
なので、`port`プロパティを使って`postMessage()`してメインスレッドに戻す必要がある。

そう考えると、この用途に`AudioWorkletProcessor`はあんまりいらなくて、単なる`WebWorker` + `ScriptProcessor`でいい説。

`WebWorker`でメインスレッドからPCMデータを受け取って、中でWASMを使ってエンコードして、メインスレッドへ返す or WebSocketで飛ばす。

WebRTCもどき(デコード)

実はコーデック次第では、`decodeAudioData()`でそのまま`AudioBuffer`にして再生できちゃう。

ブラウザが`audio`要素でそのまま再生できるような`mp3`とか`flac`とかそういったものなら。
Media Capabilities APIとかで確認してもよいはず。

あとはそれを適当にキューイングしながら再生すればよい。

自分でデコードする場合は、送信側と受信側でサンプルレートをあわせることをお忘れなく。

おまけ: 他にわかったこと

AudioNodeの作成

const ctx = new AudioContext();

// old
const gain = ctx.createGain();
gain.gain.value = 0;

// new
const gain = new GainNode(ctx, { gain: 0 })

みたく、各コンストラクタから直接つくれる。Chromeなら。

AudioNodeの接続

// old
sourceNode.connect(compNode);
compNode.connect(gainNode);
gainNode.connect(ctx.destination);

// new
sourceNode
  .connect(compNode)
  .connect(gainNode)
  .connect(ctx.destination);

まぁ複数つなぎたい場合は困るけど、1本ならシュッと書ける。

sampleRate

最近のモダンブラウザは、だいたいデフォルトで`48000`です。

ただし、iOSSafariだけは、`44100`だった。
かつ、Safariは`sampleRate`の指定ができないので、他の環境で指定して合わせる必要がある。

macSafari x iOSSafariを独自につなげたい場合は、リサンプラーを実装する必要がある・・。

OfflineAudioContext

使ったことないけど、一気にダウンサンプリングしたいときなどに有用らしい。

// Chrome, Firefox
new OfflineAudioContext({ channels: 1, length: 10, sampleRate: 48000 });
// Safari
new webkitOfflineAudioContext(1, 10, 48000);

引数がぜんぜん違うとSOで話題だった。

ConstantSourceNode

入力としてのPCMはいらないけど、決まったレートで`AudioWorkletProcessor `を動かしたいときとかに便利。