RSS Twitter Facebook

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

[Web Audio API] AudioWorklet を 1 ファイルで書く方法


AudioWorklet は Web Audio API でユーザーが自分独自のカスタムノードを作成できるようにするための仕組みで、プログラムのメインスレッドで走らせる AudioWorkletNode と音声処理スレッドで実行する AudioWorkletProcessor と呼ばれる部分で構成されます。

基本的な書き方としては AudioWorkletNode 側と AudioWorkletProcessor 側を別ファイルの ".js" で書くというスタイルが標準的で、これは別に悪い事ではないのですが前身となった ScriptProcessor が単に関数を定義するだけで使えたのに比べると、ちょっと試しに軽く使いたい時に、別ファイルを準備するのがちょっと面倒くさいと感じてしまいます (大規模なプログラムを書くつもりなら分けておいた方が良いと思うんですが)。

という事で、1ファイルでメインプログラムとプロセッサをまとめて書く方法を幾つか試してみます。


今回ベースにするコード

まずはベースとして前回の記事 ([WebAudio API] AudioWorklet の使い方) で書いた AudioWorklet のオーバードライブを使用します。

プロセッサ側のコードは次のようになっています。

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);

プロセッサのコードは "OverDrive" クラスを定義して、registerProcessor() で名前を付けて登録する、という処理になっています。 このプロセッサを使うメインプログラムとして取り合えず簡単なものを書いたものが次のコードです。


<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>
<script>
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule('overdrive-proc.js');
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

このコードのテストページ


音源を <audio> タグでドキュメント内に置いて MediaElementAudioSourceNode で読み込む方法で、オーバードライブを経由して鳴らします。ちなみに window.onload で初期化をしていますが、プライバシーポリシー制限でユーザー操作が無い状態で作った AudioContext は "suspend" 状態ですので、スライダーを触ったら resume() するという事をやっています。

プロセッサのファイル "overdrive-proc.js" を読み込んでいるのが 9 行目、audioctx.audioWorklet.addModule() です。


取り合えずプロセッサ側のコードを文字列として埋め込んでみる

まずはファイル "overdrive-proc.js" の内容をそのまま文字列に入れておいて DataURI 経由でアクセスするという方法です。38 行目の処理、
await audioctx.audioWorklet.addModule('data:text/javascript,'+encodeURI(overdriveproc));
がそれです。'data:text/javascript,' に続けてプロセッサの文字列をエスケープしてくっ付ける事で、URL としてアクセスできるようにしています。

単純に文字列としてコードを入れるのは、昔の JavaScript だと文字列が改行で途切れないように全ての行末に「\」を付ける必要があったりしてあまり実用的ではなかったのですが、ES6 でテンプレート文字列が使えるようになってからは、コード全体を 「`」で囲むだけですのでこれでもまあいけなくはないです。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>
<script>
const overdriveproc =`
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);
`;
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule('data:text/javascript,'+encodeURI(overdriveproc));
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

特殊なタイプのスクリプトとして HTML 内に埋め込む方法

他のやり方としては HTML 内に特殊なスクリプトとして埋め込む方法があります。スクリプトのタイプをそのまま実行される JavaScript と解釈されないように特殊な名前で定義します。

こういうやり方は WebGL の GLSL シェーダーを HTML 内に書く時などにも使われていて 'x-shader/x-vertex' とか 'x-shader/x-fragment' などとする事が多いようです。'x-' を付けるのは標準のタイプではない、という意味になります。 ここでは、AudioWorklet 用の JavaScript なので、
<script type="x-audioworklet/javascript">
としました。中身は document.getElementById('id').innerHTML で取ってこれますので処理としては文字列とそれほど違いはないですね。

ここの表示ではプロセッサのコードが一応 JavaScript としてシンタックスハイライトが効いているので文字列の場合よりかなり読みやすいですが、これはエディタによると思います。VScode では未知のスクリプト扱いで駄目でした。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>

<script id="overdriveproc" type="x-audioworklet/javascript">
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);
</script>

<script>
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule(
        'data:text/javascript,'
        + encodeURI(document.getElementById('overdriveproc').innerHTML)
    );
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

普通の JavaScript のクラスとして宣言してしまう方法

前の 'x-audioworklet' あたりの方法がまあ妥当かと思いつつ、最後にもうひとつトリッキーなやりかたで、普通に JavaScript のクラスとして書いてしまう方法もなくはないです。

もちろんそのままプロセッサのコードを メインのコード内に書いても動かないので少し細工します。

まずプロセッサ用のクラスは 'AudioWorkletProcessor' クラスを継承して作成しますが、この AudioWorkletProcessor はメインスレッド側には存在しないため、そもそもエラーになるのでダミーを宣言しておきます。
class AudioWorkletProcessor{}
それから次の 'addAudioWorklet()' がメインスレッド側のクラスをプロセッサとして登録するための関数です。やっている事は宣言されているクラスオブジェクトから 'toString()' で文字列化して、後は文字列として埋め込む方法と違いはありません。

ただし引数として取るのはクラスオブジェクトそのものですので、プロセッサ登録のための 'registerProcessor()' は自前で追加しています。また登録される名前はクラス名そのものになります。ここではクラス 'OverDrive' で宣言しているのでノードを作成する時も
new AudioWorkletNode(context, 'OverDrive')
となります。

addAudioWorklet()の戻り値は 'addModule()' と同じく Promise ですので必要なら await で待つなりしてください。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>

<script>
// Register Class as an AudioWorklet Processor
class AudioWorkletProcessor{}
function addAudioWorklet(context, proc){
    var f=`data:text/javascript,${encodeURI(proc.toString())}; registerProcessor("${proc.name}",${proc.name})`;
    return context.audioWorklet.addModule(f);
}

// AudioWorklet Processor Class
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;
    }
}

// Main Program
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await addAudioWorklet(audioctx, OverDrive);
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

これならエディタで確実にシンタックスハイライトが効くというのが良い所です。
メインプログラムのスコープ内にクラスオブジェクトとして存在してしまっているのが無駄と言えば無駄ですが、許せる範囲でしょうか。

Posted by g200kg : 2019/01/15 00:20:39