RSS Twitter Facebook
g200kg > Web Audio API 解説 > 14.ウェイブシェイパーの使い方

Web Audio API 解説

2019/01/19

14.ウェイブシェイパーの使い方



WaveShaper とは

WaveShaper は入力の大きさに対する出力の大きさのカーブのテーブルで信号の変換を行うという仕組みのもので、主に音に歪を加えるディストーションエフェクトとして用いられます。シンセやエフェクト関係以外ではあまり聞きなれない言葉かも知れませんが、動作自体は非常に単純です。下図では赤線が WaveShaper のテーブルで横軸が -1~+1 の範囲の入力、縦軸が出力となります。

青線で示したサイン波の入力に対して緑線の出力はあるレベルでクリップされているのがわかります。

この図の場合はあるレベルでハードクリップしているだけですが、ゆるやかな曲線でサチュレーションさせたり、もっと複雑なカーブのテーブルを通して過激な音を作る事も可能です。

Web Audio API のノードとしては、new WaveShaperNode() または audiocontext.createWaveShaper() で作成して、プロパティの curve に Float32Array のテーブルをセットするだけで使用できます。もう一つ oversample というパラメータがありますが、これは入力のサンプルを2倍または4倍にオーバーサンプリング (通常ブラウザの実装はサンプリング周波数が 44.1kHz ですので 88.2kHz や 176.4kHz)してから処理を行うオプションで特に高品質な処理が必要な時に使用しますが、その分 CPU の負荷は重くなります。


const waveshaper = new WaveShaperNode(audioctx);
waveshaper.curve = table;

WaveShaperのパラメータ

パラメータ単位デフォルト値値の範囲内容
curveFloat32Array-null-ウェーブシェイピングのカーブ配列
oversample文字列-"none""none"
"2x"
"4x"
オーバーサンプリングの指定

ディストーションカーブの作り方

一般的なオーバードライブ/ディストーションの効果を得る方法として、例えば ここ では下のようなテーブル作成の手法が紹介されています。


if ((amount >= 0) && (amount < 1)) {

    ND.dist = amount;

    var k = 2 * ND.dist / (1 - ND.dist);

    for (var i = 0; i < n_samples; i+=1) {
        // LINEAR INTERPOLATION: x := (c - a) * (z - y) / (b - a) + y
        // a = 0, b = 2048, z = 1, y = -1, c = i
        var x = (i - 0) * (1 - (-1)) / (n_samples - 0) + (-1);
        this.wsCurve[i] = (1 + k) * x / (1+ k * Math.abs(x));
    }

}

この式がどんなカーブになっているかをグラフにしてみると下の図のようになります。ディストーションのパラメータ、amount の設定で入力をそのままスルーして出力する直線の状態から、矩形波状にクリップする形に変化するのがわかります。



テーブルのサイズと補間について

さて、このテーブルの大きさは仕様上、下限は最低限カーブが作れる 2 となっていますが上限は特に定められていません。

また、テーブルを参照するにあたって直線補間が行われる事が仕様で明記されています。つまり 2 点のみのテーブル [-1, 1] を指定すると、-1 ~ +1 の入力に対してそのまま -1 ~ +1 が出力される事になります。かつてはこの補間が行われない状態の実装があったので点数の少ないテーブルで階段状のテーブルを作成していわゆる「ビットクラッシャー」エフェクトを作るという応用もあったのですが、現在はその手法は使えません。同じ事をするには点数の多いテーブルで階段型のカーブを作成する必要があります。

ウェイブシェイパーのサンプル

それではサンプルとして前述のビットクラッシャーを作ってみます。BufferSource から適当な音を流し、WaveShaper で指定のステップ数の階段状に加工します。また、確認のため Analyser で出力の波形データを表示します。

なおこのサンプルはもともとテーブルの補間なし実装の応用として小さなテーブルを使っていたのですが、上記のように補間されるようになりましたので、4096 サイズのテーブルに階段状のカーブを作るように変更しました。


<!DOCTYPE html>
<html>
<body>
<h1>WaveShaper Test</h1>
<hr/>
<div>
<button id="play">Play</button> <button id="stop">Stop</button><br/>
<table>
<tr><th>Steps</th><td><input type="range" id="steps" min="2" max="32" value="4"/></td><td id="stepsval"></td></tr>
</table>
<br/>
<canvas id="graph" width=256 height=256></canvas>
</div>
<script>

window.addEventListener("load", async ()=>{
    const audioctx = new AudioContext();
    const buffer = await LoadSample(audioctx, "./loop.wav");
    const shaper = new WaveShaperNode(audioctx);
    const analyser = new AnalyserNode(audioctx);
    let src = null;

    shaper.connect(analyser).connect(audioctx.destination);
    Setup();

    const canvasctx = document.getElementById("graph").getContext("2d");

    document.getElementById("stop").addEventListener("click", ()=>{
        if(src) src.stop();
        src = null;
    });
    document.getElementById("play").addEventListener("click", ()=>{
        audioctx.resume();
        if(src === null) {
            src = new AudioBufferSourceNode(audioctx, {buffer: buffer, loop: true});
            src.connect(shaper);
            src.start();
        }
    });
    document.getElementById("steps").addEventListener("input",Setup);

    function LoadSample(actx, url) {
        return new Promise((resolv)=>{
            fetch(url).then((response)=>{
                return response.arrayBuffer();
            }).then((arraybuf)=>{
                return actx.decodeAudioData(arraybuf);
            }).then((buf)=>{
                resolv(buf);
            })
        });
    }

    function Setup() {
        const steps = document.getElementById("steps").value;
        document.getElementById("stepsval").innerHTML = steps;
        curve = new Float32Array(4096);         // Make Curve (length = steps)
        for(let i = 0; i < 4096; ++i) {
            curve[i] = (((i/4096) * steps)|0)/(steps-1)*2-1;
        }
        shaper.curve = curve;                   // set curve to WaveShaper
    }

    // For graph display
    var wavdata = new Uint8Array(256);
    function DrawGraph() {
        analyser.getByteTimeDomainData(wavdata);
        canvasctx.fillStyle = "#000000";
        canvasctx.fillRect(0, 0, 256, 256);
        canvasctx.fillStyle = "#008022";
        for(var i = 0; i < 256; ++i) {
            d = wavdata[i] - 128;
            if(d == 0)
                d = 1;
            canvasctx.fillRect(i, 128, 1, d);
        }
    }
    Setup();
    setInterval(DrawGraph, 100);
});
</script>
</body>
</html>
テストページ:ウェイブシェイパー




g200kg