RSS Twitter Facebook
g200kg > Web Audio API 解説 > 15.コンプレッサーの使い方

Web Audio API 解説

2019/01/19

15.コンプレッサーの使い方



DynamicsCompressor

DTM を多少でもかじった経験がある方ならばコンプレッサーがどういうものかは説明するまでもないかと思いますが、一言で言えば大きな音が入った時にボリュームを絞って音量の変化を抑えるエフェクトです。

入力される信号のレベルを整えたり、音色作りの一環として利用したりとさまざまなケースがあると思いますが、最終出力段に入れて信号がハードクリップしないようにするという使い方が Web Audio API で想定する代表的な使い方ではないでしょうか。

例えばゲームアプリで下の図のように多数の効果音が重なって鳴る可能性がある場合など、単純に最悪ケースを想定してクリップしないように信号レベルを決めると平均音量が小さくなり過ぎてしまいます。こういう場合にコンプレッサーを使うと運悪く多数の音が重なった部分だけ音量を絞り込んで歪まないようにする、というような事ができます。

コンプレッサーのパラメータ

Web Audio API の DynamicsCompressor には下の表のように一般的なコンプレッサーにあるようなパラメータが大体そろっています。

これらのパラメータは全て AudioParam 型なのでオートメーションで動かす事も可能ですが、おそらくそういう事をするのは特殊なケースで、通常は固定の値を設定するだけになるでしょう。また、リダクションはコンプレッションの効き方をグラフ表示する場合などに読み出しのために使用するだけで設定はできません。

パラメータ単位デフォルト値値の範囲内容
thresholdAudioParamdB-24-100~0スレッショルド。k-rate
kneeAudioParamdB300~40ニー特性。スレッショルドの上側のスムージング幅。k-rate
ratioAudioParamなし121~20コンプレッション率。k-rate
reduction数値dB無信号時は0-20~0現在の瞬間のリダクション率。グラフ表示用でリードオンリー
attackAudioParam0.0030~1.0アタックタイム。k-rate
releaseAudioParam0.250~1.0リリースタイム。k-rate

パラメータの名前だけ見れば、普通のコンプレッサーのつもりで値を設定して良さそうなのですが、要注意なのが「knee (ニー)」の設定です。また、普通のコンプレッサーでは「メイクアップゲイン」と言う全体を底上げするゲインのパラメーターがあるものも多いのですが、Web Audio API の DynamicsCompressor にはこの設定が無く、他のパラメータから自動的に設定されます。パラメータの効き方には癖がありますので、やみくもに数値を設定しても思ったような動作にならないという事態に陥りそうです。

knee はいわゆるニー特性(スレッショルドを超えた時のカーブの曲がり方)を指定するもので、入力がスレッショルドを超えるとコンプレッションが効き始め、スレッショルド+ニーのレベルで指定したレシオに到達します。

デフォルトの設定の場合、thresholdが -24 で knee が 30 ですから、入力レベルが +6dB になってようやく設定されている圧縮比 12 になります。

またメイクアップゲインに関しては Chrome での挙動を見ていると threshold / knee / ratio の各値から自動設定されているようで仕上がりの出力レベルがどうなるかは各パラメータの設定によって変動します。この辺の挙動は直感的に把握しにくく、出力レベルを厳密に管理する目的で使うには実際に値を設定して確認する必要がありそうです。

パラメータの設定の仕方としては次のようなパターンが考えられます。

  • knee を 0 にしてハードニーで使う。ただしメイクアップは勝手に設定されます。
  • knee = -threshold にすると 0dB が入力された時に指定した ratio になる。この時の出力レベルは説明が難しい状態になりますが 0dB は超えないそれなりの値になるようです。
  • デフォルト状態から一切パラメータをいじらずにそのまま使う

デフォルトの knee = 30dB のままで threshold や ratio をいじってもコンプレッションカーブの全体像がどうなっているのかが把握しにくい感じです。

デフォルトのパラメータは一般的な用途なら最終出力段に使って破綻しないように選ばれているように思いますので、3 番目が実は一番おすすめのような気もします。複数の音源をまとめてクリップせず適度な音量でならす、という目的ならば下手にいじらず、デフォルトのパラメータのまま使う方が良いかも知れません。


コンプレッサーのサンプル

それではサンプルプログラムです。これはコンプレッサーを通したオシレーターの出力を徐々に上げていって出力レベルの変化がどうなるかという、コンプレッションカーブを実測するものです。threshold / knee / ratio の設定によって出力レベルのカーブがどう変わるかが確認できます。

ノードの接続としてはオシレータをゲインを通してコンプレッサーに入れ、出力をスクリプトプロセッサーで測定しています。「Test Start」ボタンを押すとテストが始まり、自動的にレベルを変化させながら入出力特性のグラフを作成しています。

グラフの目盛りが最大 20dB までありますが、出力(縦方向)が 0dB を超えると音として出力する時点でクリップする事になります。入力側(横方向)の threshold を超える信号 (例えば 0dB を超える場合は Web Audio API の内部処理中で -1 ~ +1 を超える振幅になっている信号を意味します)がコンプレッサーでどのように押さえ込まれるかが観測できます。

テストページ:コンプレッサー

<!DOCTYPE html>
<html>
<body>
<h1>DynamicsCompressor Test</h1>
<table>
<tr><th>Threshold</th><td><input id="thresh" type="range" min="-100" max="0" step="0.1" value="-24"/></td><td id="threshval"></td></tr>
<tr><th>Knee</th><td><input id="knee" type="range" min="0" max="40" step="0.1" value="30"/></td><td id="kneeval"></td></tr>
<tr><th>Ratio</th><td><input id="ratio" type="range" min="1" max="20" step="0.1" value="12"/></td><td id="ratioval"></td></tr>
<tr><th>Attack</th><td><input id="atk" type="range" min="0" max="0.1" step="0.001" value="0.003"/></td><td id="atkval"></td></tr>
<tr><th>Release</th><td><input id="rel" type="range" min="0" max="1" step="0.01" value="0.25"/></td><td id="relval"></td></tr>
</table>
<button id="start">Test Start</button><br/>
<canvas id="cv" width="364" height="364"></canvas>

<script>

window.addEventListener("load", ()=>{
    const audioctx = new AudioContext();
    const gaintable = new Array(100);
    for(let i = 0; i < 100; ++i)
        gaintable[i] = 0;
    
    const sig = new OscillatorNode(audioctx);
    const gain = new GainNode(audioctx, {gain:0});
    const comp = new DynamicsCompressorNode(audioctx);
    const ana = new AnalyserNode(audioctx);
    const wavdata = new Float32Array(512);
    let timer;
    let testcount = 0;
    let currentOutLevel = 0;
    let testing = 0;
    let maxlev = 0;
    sig.connect(gain).connect(comp).connect(ana).connect(audioctx.destination);
    sig.start();
    Draw();
    Setup();

    document.getElementById("start").addEventListener("click",()=>{
        if(audioctx.state=="suspended")
            audioctx.resume();
        testing = 1;
        gain.gain.value = 0;
        testcount = -2;
        maxlev = 0;
        timer = setInterval(TestInterval, 50);
    });
    document.getElementById("thresh").addEventListener("input", Setup);
    document.getElementById("knee").addEventListener("input", Setup);
    document.getElementById("ratio").addEventListener("input", Setup);
    document.getElementById("atk").addEventListener("input", Setup);
    document.getElementById("rel").addEventListener("input", Setup);

    function Setup(){
        comp.threshold.value = document.getElementById("threshval").innerHTML
            = document.getElementById("thresh").value;
        comp.knee.value = document.getElementById("kneeval").innerHTML
            = document.getElementById("knee").value;
        comp.ratio.value = document.getElementById("ratioval").innerHTML
            = document.getElementById("ratio").value;
        comp.attack.value = document.getElementById("atkval").innerHTML
            = document.getElementById("atk").value;
        comp.release.value = document.getElementById("relval").innerHTML
            = document.getElementById("rel").value;
    }

    function TestInterval() {
        if(testcount>0){
            ana.getFloatTimeDomainData(wavdata);
            for(let i = 0; i < wavdata.length; ++i){
                const d = Math.abs(wavdata[i]);
                if(d > maxlev)
                    maxlev = d;
            }
            gaintable[testcount-1] = maxlev;
        }
        maxlev = 0;
        Draw(testcount-1);
        gain.gain.value = Math.pow(10, (testcount - 80) / 20);
        if(++testcount > 100) {
            gain.gain.value = 0;
            clearInterval(timer);
            testing = 0;
        }
    }

    function Draw(n) {
        const cv = document.getElementById("cv");
        const ctx = cv.getContext("2d");
        ctx.fillStyle = "#404040";
        ctx.fillRect(0, 0, 364, 364);
        ctx.fillStyle = "#20c040";
        for(let i = 0; i < 100; ++i) {
            let v = gaintable[i];
            if(v < 1e-128)
                v = 1e-128;
            v = Math.max(-80, Math.LOG10E * 20 * Math.log(v));
            v = (20 - v) * 3;
            ctx.fillRect(i * 3 + 32, v + 32, 3, 300 - v);
        }
        ctx.fillStyle = "#c06060";
        for(let i = 0; i <= 100; i += 10) {
            ctx.fillRect(32, 32 + i * 3, 300, 1);
            ctx.fillRect(32 + i * 3, 32, 1, 300);
            ctx.fillText((20 - i) + "dB", 5, i * 3 + 35);
            ctx.fillText((20 - i) + "dB", 320 - i * 3, 345);
        }
        ctx.fillStyle = "#f0e480";
        ctx.fillRect(34 + n*3, 32, 1, 300);
    }
});
</script>

</body>
</html>





g200kg