Open5

WebAudioMini: WebAudio APIのレビューとサブセット

okuokuokuoku

prev: https://zenn.dev/okuoku/scraps/81099a883cf61f
next: https://zenn.dev/okuoku/scraps/53012e52ba545c

WebAudioのサブセットを作る

WebAudioは実は超クッソ激烈に高機能なAPIで、WebKitのWebAudio実装を取り出してオーディオエンジンに仕立てたものまで存在する。

https://github.com/LabSound/LabSound

もっとも、WebAudioはまだCR(勧告候補)の段階で、かつ、EmscriptenやUnityはWebAudioの機能性をガッツリ活用しているわけではないので、 Emscriptenから使われるWebAudioのサブセット を設計して、それだけ実装することにする。

WebAudioが不幸なのはあまりにも低レベルを意識しすぎたことに思える。シンセサイザーを作ったりするのには便利だが、何もかもがサンプル粒度で制御可能である必要があったりして移植性が著しく低く、プラットフォームのDSPや出力機構(Dolby Atmosなど)を活用する機会が無い。

Codec

Unity WebGLでオーディオをサポートする場合、最大の問題はCodecで、Unity WebGLはAAC(+m4a)を使ってくる。実は WebAudioは <audio> タグでサポートしているCodecを一般にサポートする必要があり 、AACについては(主に特許の都合で)お手軽なデコーダが存在しない。

Pure JavaScript実装はあるんだけど。。(気合入り過ぎだろ)

https://www.npmjs.com/package/audio-decode

対して、EmscriptenはPCMしか扱わない。というわけで、一旦Codecは無視かな。。後でGStreamerか何かに繋ぐって事で。。

okuokuokuoku

(-Nativeを取った)

ちょっとWebGLとは方針を替えて、機能性の多くをJavaScript側に持たせ、パフォーマンス影響の大きいミキサー部分だけをネイティブコードとする方針にした。EmscriptenやUnityでは基本的に、

[Source] → [3D Panner] → [Gain]

という連結が固定で使用され、自由なグラフ構築機能自体はほとんど使用されないため。

この音源 〜 パン(音の聴こえる方向の設定) 〜 ゲイン のひとまとまりを ボイス (voice) とし、ネイティブコード側では事前に128ボイス分のスロットを固定で用意しておき、JavaScript側ではSourceの連結状況を元にボイスを設定するようにする。

... という方針で実装できるかどうかという観点でWebAudioのAPI使用状況をレビューしていくことになる。

okuokuokuoku

WebAudio APIの利用箇所

とりあえずAPIの利用状況を調べ、音の鳴らないダミー実装を用意したい。

Emscripten

Emscriptenでは、SDLのオーディオ機能(SDL自体とSDL_Mixer互換実装)で使われる。ちなみに本家SDL2のEmscripten実装はEmscriptenのSDL互換実装のオーディオ機能を流用する。 ...なかなか複雑だな。。

OpenALは歴史的な3D Audio APIで、今のところこれくらいしかクロスプラットフォームな3DオーディオAPIの選択肢が無い。Emscriptenの実装はドップラー効果を除いてエフェクト類が一切実装されていないが、指向性のある完全な3D音源が実装されている。(ちなみにWebAudioではドップラー効果は削除されている https://github.com/emscripten-core/emscripten/issues/4587 -- Emscriptenでは、結局OpenAL実装を完全にリライトしてドップラー効果を再度導入している https://github.com/emscripten-core/emscripten/pull/5367 )

SDL_Mixerは純粋な2DオーディオAPIで、音源には高さの概念がない。また、LoadWavによって圧縮音源を読み取ることができ、それらにはWebAudioの decodeAudio を使用している。

Unity WebGL

Unity WebGLでは _JS_Sound_Create_Channel のような JS_Sound で始まるネイティブ関数としてオーディオ処理が実装されている。Unityは通常FMODをオーディオエンジンとして使用するが、WebGLでは自前の発声システムをAudioClipの実装のため(だけ)に持っていることになる。この辺の制限はドキュメントに記述されている:

https://docs.unity3d.com/Manual/webgl-audio.html

実際にエクスポートされている関数は少い。

 "_JS_Sound_Create_Channel": _JS_Sound_Create_Channel,
 "_JS_Sound_GetLength": _JS_Sound_GetLength,
 "_JS_Sound_GetLoadState": _JS_Sound_GetLoadState,
 "_JS_Sound_Init": _JS_Sound_Init,
 "_JS_Sound_Load": _JS_Sound_Load,
 "_JS_Sound_Load_PCM": _JS_Sound_Load_PCM,
 "_JS_Sound_Play": _JS_Sound_Play,
 "_JS_Sound_ReleaseInstance": _JS_Sound_ReleaseInstance,
 "_JS_Sound_ResumeIfNeeded": _JS_Sound_ResumeIfNeeded,
 "_JS_Sound_Set3D": _JS_Sound_Set3D,
 "_JS_Sound_SetListenerOrientation": _JS_Sound_SetListenerOrientation,
 "_JS_Sound_SetListenerPosition": _JS_Sound_SetListenerPosition,
 "_JS_Sound_SetLoop": _JS_Sound_SetLoop,
 "_JS_Sound_SetLoopPoints": _JS_Sound_SetLoopPoints,
 "_JS_Sound_SetPaused": _JS_Sound_SetPaused,
 "_JS_Sound_SetPitch": _JS_Sound_SetPitch,
 "_JS_Sound_SetPosition": _JS_Sound_SetPosition,
 "_JS_Sound_SetVolume": _JS_Sound_SetVolume,
 "_JS_Sound_Stop": _JS_Sound_Stop,

これらを丸ごとパクってネイティブ側で実装することも考えたが、それだとEmscriptenがサポートできないのでWebAudioをサブセットする方向にしている。

UnityがEmscripten側のOpenALを使っていない理由は判然としないが、たぶん圧縮オーディオのサポートが良くないためだろう。

Unity WebGLでも3D panningは必要なため、SDL_Mixer(こちらは圧縮オーディオをサポートしている)を使うことはできない。

Equal-power stereo panning, with a simple L-R control, is 99% of the desired scenarios.

つまりUnity WebGLはWebAudio想定の99%に入ることができなかったと言える。

okuokuokuoku

ダミー実装を用意

https://github.com/okuoku/cwgl-proto/commit/fc04c7578cb01be60460fc2b283bfc7276f5c04d

とりあえずUnityをクラッシュさせない程度のダミー実装を作成した。

function dummynode(){
    return {
        connect: function(){},
        disconnect: function(){},
    };
}

function dummybuffer(){
    return {};
}

function wrappromise(promise, success, fail){
    if(! success){
        return promise;
    }else{
        promise.then(success).catch(fail);
    }
}

function audioctx_mini(){
    return {
        decodeAudioData: function(data, success, fail){
            const p = new Promise((res, err) => {
                res(dummybuffer());
            });
            return wrappromise(p, success, fail);
        },
        listener: {
            positionX: 0,
            positionY: 0,
            positionZ: 0,
            setPosition: function(){},
            setOrientation: function(){}
        },
        createGain: function(){
            let node = dummynode();
            node.gain = {};
            node.gain.setValueAtTime = function(){};
            return node;
        },
        createPanner: function(){
            let node = dummynode();
            node.setPosition = function(){};
            return node;
        },
        createBufferSource: function(){
            let node = dummynode();
            node.playbackRate = {};
            node.playbackRate.value = 1;
            node.start = function(){};
            return node;
        },
        /*
        createBuffer: function(){
            return dummynode();
        },
        */
    }
}

module.exports = audioctx_mini;

createBuffer は使われなかった = AACの実装なしでは一切音が出ないということでちょっと困ったな。。これは後回しかな。。

okuokuokuoku

(後廻し)

FFMPEG.wasmとかで良いんじゃないかと思ったけどアレWASMスレッドとSharedArrayBuffer必須なのか。。(Node.js的にExperimental feature)

というわけでちょっと後廻し。libavcodecかGStreamerだな。。どっちも開発環境を用意するのが面倒だけど。