RSS Twitter Facebook

2019/01/14 (2019年01月 のアーカイブ)

[WebAudio API] AudioWorklet の使い方


さて、2019 年になって ScriptProcessor の後継とされる AudioWorklet が Chrome で動き始めてからもう一年くらい経ちましたかね。まだまだガリガリ使われているという感じでもないのは、以前の ScriptProcessor に比べると構造が複雑になってちょっととっつきにくくなったからでしょうか。少しハードルが上がった感はありますね。

それに現状は Web Audio API の仕様とブラウザの実装がまだ少し乖離している所が残っているので気を付けないとハマるかも知れません。

とは言っても ScriptProcessor がいつまであるかもわからないし、そろそろちゃんと使わないとですね。

という事でまずはサンプルを書いてみました。

AudioWorklet (OverDrive)


たいしたものではないですが、これはオーバードライブエフェクトを AudioWorklet で実装したものです。[Play]を押すと音楽が流れて [Drive] のツマミでかかり具合を調整できます。


ソースコード overdrive-proc.js


class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}
registerProcessor("OverDrive", OverDrive);


AudioWorklet では、音声処理を実行するプロセッサはメインのプログラムとは別ファイルで用意するのが基本です。

この 'overdrive-proc.js' がプロセッサ部分になりますが、AudioWorkletProcessor クラスから派生した class を定義し、registerProcessor() で登録します。プロセッサはメインプログラムとは別のオーディオ処理専用のスレッドで実行され、変数を受け渡したりというメインプログラムの世界とは直接的なやりとりはできません。

ここで定義したクラス内の process() 関数が実際の処理を行います。引数の inputs が音声信号の入力の配列、outputs が音声信号の出力の配列、parameters はこのノードが持っているパラメータの値を表します。OverDrive のようなエフェクター的なノードの場合はデフォルト状態の入力が 1 本、出力が 1 本で良いです(ステレオ音声等のマルチチャンネルも入力の数としては 1 本です)。

なので inputs[0] および outputs[0] が処理すべき入出力になります。入出力はブロック (128 サンプル) 毎に渡されますので、各チャンネルの各サンプルを入力から取得して処理を行い出力に渡しています。

ちなみにオーバードライブ処理というと一般的には tanh 関数なんかが良く使われるのですが、ここで行っているのは

\(output = Math.pow(input, 定数)\)

という処理を正負対称にくっつけたものを使っています。ここで定数を 1.0 ~ 0.05、つまり実数乗根とすると 1.0 で歪み無し、小さくなる程すぐに 1.0 に漸近するカーブになるので歪みが大きくなります。更にパラメータの 0.0 ~ 1.0 に対して冪乗の定数は

\(Math.pow(0.05, パラメータ)\)

で 1.0 ~ 0.05 の範囲にマップしています。入力に対する出力カーブは次のようになります。
まあこのあたりは好みもあるので適当ですが。

後は process() の最後が return true になっている所に注目です。とりあえず今は process() は true を返す、という事で良いですがこれはまた後述。

それから、static get parameterDescriptors() いうメソッドがありますが、これはこの OverDrive ノードが持っているパラメータを表しています。Oscillator ノードの frequency や Gainノードの gain のようなものです。

ここではdrive という 0 ~ 1 の範囲のパラメータを一つだけ持っていて animationRate が "k-rate" である事が宣言されています。

animationRate は "k-rate" と "a-rate" があり、"a-rate" はパラメータが 1 回の process() の呼び出し内でサンプル単位で変化し(する可能性があり)、"k-rate" はブロック単位なので process() 呼び出し内では必ず固定値になります。"a-rate" だともう少しコード量が増えてしまうし、"k-rate" より処理が重くなります。


ソースコード メインプログラム (html)

さて、プロセッサは登録しましたがこれを使う側です。
async function Init(){
    audioctx = new AudioContext();
    buffer = await LoadSample(audioctx, "./loop.wav");
    await audioctx.audioWorklet.addModule("overdrive-proc.js");
    overdrive = new AudioWorkletNode(audioctx,"OverDrive");
    overdrive.drive = overdrive.parameters.get("drive");
    vol = new GainNode(audioctx,{gain:0.5});
    analyser = new AnalyserNode(audioctx);
    src = new AudioBufferSourceNode(audioctx, {buffer:buffer, loop:true});

    src.connect(overdrive).connect(vol).connect(analyser).connect(audioctx.destination);

    //....
}
初期化の部分だけですが、async/await で書いています。
  • AudioContext の作成
  • 音源のロード
の次に
  • addModule
で、さっき作った "overdrive-proc.js" を追加していますが、この API は Promise を返しますので await で終了を待っています。これでメインプログラム側で OverDrive が使えるようになります。 そして
  • overdrive = new AudioWorkletNode(audioctx,"OverDrive");
が実際に AudioWorklet のノードを作成している部分です。これにより普通のノードとして使用できる AudioWorklet ノードが作成され、自動的にユーザーからは見えないオーディオ処理スレッド側では対応する overdrive-proc.js の OverDrive 処理のインスタンスが生成されます。

次の
  • overdrive.drive = overdrive.parameters.get("drive");
は、OverDrive ノードが持っているパラメータにアクセスしやすくするために overdrive ノードのプロパティにくっつけている処理です。これで、GainNode.gain と同様に OverDrive.drive という形式でアクセスできます。これについてはまた後述。

最後の行で、ノードの接続は

src(BufferSource音源) => overdrive(AudioWorklet) => vol(メインボリューム用Gain) => analyser(波形表示用) => destination

と普通のノードと同様に connect() で接続しています。後は音源用のデータを fetch で読んだり、ツマミをいじった時に オーバードライブのパラメータを変更したり、出てきた音の波形を Canvas で描いたりしていますけど、今までの WebAudio のプログラムと違う所は無いと思います。


気を付けないといけない事

今はまだ仕様と実装が完全に一致していない部分があったりするので幾つか注意点があります。
  • プロセッサの process() の戻り値は、ノードのライフタイムを制御します。

    仕様上は false を返した場合、ノードに信号が入力されている間だけ動作を続け、接続が無くなると自動的にノードが破棄される、という事になっています。これはその内、実装に反映されるのではないかと思いますが、今は false を返すとノードが止まってしまうので必ず true を返して動かしっぱなしにする必要があります。

  • 作成した AudioWorkletNode のパラメータにアクセスするには
    overdrive.parameters.get("drive")
    のように parameters.get() を経由する必要があります。例えば OscillatorNode.frequency や DelayNode.detayTime のようには行かない所が、まあ事情はあると思いますがちょっと残念ではありますね。
    このサンプルではせめて、という事でメインプログラム側でノードを作った後、
    overdrive.drive = overdrive.parameters.get("drive")
    とやって overdrive.drive でアクセスできるようにしています。これがプロセッサ側のコードでできれば良いんですけどね。

    アクセスした先のオブジェクトは普通の AudioParam ですので、.value に値を代入したり、setValueAtTime() などのオートメーション関数を使ったり、他のノードの出力を接続して変調を掛けたりという事は通常通りできます。

  • この例では使っていないのですが、AudioWorklet のノードのパラメータに作成と同時に初期値を渡したい時、少し書式が違います。

    例えば GainNode を作成する時、
    new GainNode(audioctx, {gain:0.5})
    とやって作成時に gain を 0.5 に設定するなんて事ができますが、AudioWorklet ノードの場合、例えば OverDrive の drive を 0.5 に設定するなら、
    new AudioWorkletNode(audioctx,"OverDrive",{drive:0.5});
    ではなくて、
    new AudioWorkletNode(audioctx,"OverDrive",{parameterData:{drive:0.5}});
    のようになります。これが一応仕様上定められた書き方ですが、仕様書内の EXAMPLE コードでも書き方が違っている部分があったりするので混乱しやすい所です。

  • AudioWorklet だけの問題ではないですが、去年導入された Privacy Policy の影響で WebAudio で音を出すには最初にユーザーの操作が無くてはいけません。AudioWorklet は動き始める前に AudioContext を使って必要なモジュールの登録処理等が必要ですので、処理の順序を考えるのが少し面倒になった感があります。まあちゃんと考えれば良いのですけど。

  • その内修正は入ると思いますが、基本的に Web Audio API の仕様書内にある EXAMPLE はまだちゃんと整備されていないのでそのままでは動かないものが多いです。

    今のところ実際に動作するコードのサンプルとして一番信頼できるのは、Google の WebAudio 開発者である @hoch さんが ChromeLabs で公開している Audio Worklet のサンプルページ です。

という事で、まだ少し気を付けつつ、ではありますけど AudioWorklet もそろそろガシガシ使えるフェーズに入りつつあるんじゃないですかね。

Posted by g200kg : 2019/01/14 03:40:23