RSS Twitter Facebook
g200kg > Web Audio API 解説 > 10.フィルターの使い方

Web Audio API 解説

2019/01/19

10.フィルターの使い方



Biquad Filterとは

Web Audio API では汎用的なフィルタとして「BiquadFilter」、日本語では「双2次フィルター」という形式のものが用意されています。これはオーディオ系の信号処理でも頻繁に使用されるものです。この BiquadFilter には Robert Bristow-Johnson と言う人が書いた有名な設計手法の解説があって、通称「RBJ EQ Cookbook」などと呼ばれているのですが、Web Audio API の仕様でもフィルターの詳細についてはその「RBJ EQ Cookbook」を参考にしているという記述があります。

以前私もこの RBJ EQ Cookbook を凄く適当に翻訳してみた事がありますので、フィルターの中身に興味があれば参照してください。

VSTiの作り方-フィルタの料理法

フィルターの種類としては次の8種類が使用できます。

  • ローパスフィルター : 特定の周波数より下だけを通す
  • ハイパスフィルター : 特定の周波数より上だけを通す
  • バンドパスフィルター : 特定の周波数範囲だけを通す
  • ローシェルフフィルター : 特定の周波数より下を増幅または減衰させる
  • ハイシェルフフィルター : 特定の周波数より上を増幅または減衰させる
  • ピーキングフィルター : 特定の周波数を増幅する(減衰も可)
  • ノッチフィルター : 特定の周波数を減衰させる
  • オールパスフィルター : すべての周波数を通し位相特性だけを回転させる

これらは全て基本的には「RBJ EQ Cookbook」で解説されているものですが、ローパスフィルター、ハイパスフィルターについては 「RBJ EQ Cookbook」そのままというわけではありません。楽器用途として多少のチューニングが施されているようで、Q を上げてゆくとレゾナンスがきつめに掛かるようになっています 。


フィルターのパラメータ

フィルターノードのパラメータは次のとおりです。

パラメータ単位デフォルト値値の範囲内容
type文字列-"lowpass""lowpass"
"highpass"
"bandpass"
"lowshelf"
"highshelf"
"peaking"
"notch"
"allpass"
フィルターのタイプ
"lowpass":ローパス
"highpass":ハイパス
"bandpass":バンドパス
"lowshelf":ローシェルフ
"highshelf":ハイシェルフ
"peaking":ピーキング
"notch":ノッチ
"allpass":オールパス
frequencyAudioParamHz3500~ナイキスト周波数カットオフ周波数。範囲のナイキスト周波数はサンプルレートの1/2。このパラメータは a-rate です。
detuneAudioParamセント0-∞ ~ +∞デチューン。カットオフ周波数をずらします。単位の「セント」は半音の 1/100 で 1200 で 1 オクターブになります。このパラメータは a-rate です。
QAudioParamなし1-∞ ~ +∞フィルターのQ。このパラメータは a-rate です。
gainAudioParamdB0-∞ ~ +∞デシベルで表されるフィルターのゲインです。ローシェルフ/ハイシェルフ/ピーキングの場合のみ使用されます。このパラメータは a-rate です。
getFrequencyResponse
(freq,mag,phase)
関数---フィルター特性カーブを取得します。

フィルターを使用したサンプル

フィルターによってどんな効果が得られるのかはもう、耳と目で確かめるのが速いと思いますので、実際に音とスペクトラムを表示するサンプルを紹介します。

少し長くなっていますが、コードの後ろ半分は Analyser ノードとグラフ表示です。乱数ノイズまたは音楽データを元にしてフィルターを通した後、Analyser ノードでスペクトラムを計測しています。構成は下の図のようになります。

フィルターのタイプ、周波数、Q などを変化させるとスペクトラムはどうなって、どう聴こえるのかがわかるかと思います (※ Gain はローシェルフ、ハイシェルフ、ピーキングにだけ影響します)。



<!DOCTYPE html>
<html>
<body>
<h1>BiquadFilter Test</h1>
<div>
<table>
<tr><th>Type</th><td><select id="type"><option>LPF</option><option>HPF</option><option>BPF</optioin>
<option>LowShelf</option><option>HighShelf</option><option>Peaking</option><option>Notch</option><option>AllPass</option>
</select></td></tr>
<tr><th>Freq</th><td><input type="range" id="freq" min="100" max="20000" value="5000"/></td><td id="freqval"></td></tr>
<tr><th>Q</th><td><input type="range" id="q" min="0" max="50" step="0.1" value="5"/></td><td id="qval"></td></tr>
<tr><th>Gain</th><td><input type="range" id="gain" min="-50" max="50" value="0"/></td><td id="gainval"></td></tr>
</table>
<button id="playnoise">Play Noise</button>
<button id="playmusic">Play Music</button>
<button id="stop">Stop</button>
</div>
<br/>
<p><canvas id="cvs" width=512 height=256></canvas></p>
<script>

window.addEventListener("load", async ()=>{
    const audioctx = new AudioContext();
    let src = null;
    const noisebuff = new AudioBuffer({channels:1, length:audioctx.sampleRate, sampleRate:audioctx.sampleRate});
    const musicbuff = await LoadSample(audioctx,"./loop.wav");
    const filter = new BiquadFilterNode(audioctx,{frequency:5000, q:5});
    const analyser = new AnalyserNode(audioctx,{smoothingTimeConstant:0.7, fftSize:1024});
    filter.connect(analyser).connect(audioctx.destination);

    const noisebuffdata = noisebuff.getChannelData(0);
    for(let i = 0; i < audioctx.sampleRate; ++i)
        noisebuffdata[i] = (Math.random() - 0.5) * 0.5;

    const cv = document.getElementById("cvs");
    const ctx = cv.getContext("2d");
    const analysedata = new Float32Array(1024);

    document.getElementById("playnoise").addEventListener("click", ()=>{
        if(audioctx.state=="suspended")
            audioctx.resume();
        if(src)
            src.stop();
        src = new AudioBufferSourceNode(audioctx, {buffer: noisebuff, loop:true});
        src.connect(filter);
        src.start();
    });
    document.getElementById("playmusic").addEventListener("click", ()=>{
        if(audioctx.state=="suspended")
            audioctx.resume();
        if(src)
            src.stop();
        src = new AudioBufferSourceNode(audioctx, {buffer: musicbuff, loop:true});
        src.connect(filter);
        src.start();
    });
    document.getElementById("stop").addEventListener("click", ()=>{
        if(src){
            src.stop();
            src = null;
        }
    });
    document.getElementById("type").addEventListener("change", Setup);
    document.getElementById("freq").addEventListener("input", Setup);
    document.getElementById("q").addEventListener("input", Setup);
    document.getElementById("gain").addEventListener("input", Setup);
    Setup();
    setInterval(DrawGraph,100);

    function Setup() {
        filter.type = ["lowpass","highpass","bandpass","lowshelf","highshelf","peaking","notch","allpass"][document.getElementById("type").selectedIndex];
        filter.frequency.value = document.getElementById("freqval").innerHTML = document.getElementById("freq").value;
        filter.Q.value = document.getElementById("qval").innerHTML = document.getElementById("q").value;
        filter.gain.value = document.getElementById("gainval").innerHTML = document.getElementById("gain").value;
    }
    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);
            })
        });
    }
    ///////////////////////// for Graph
    function DrawGraph() {
        analyser.getFloatFrequencyData(analysedata);
        ctx.fillStyle = "#000000";
        ctx.fillRect(0, 0, 512, 256);
        ctx.fillStyle = "#009900";
        for(var i = 0; i < 512; ++i) {
            var f = audioctx.sampleRate * i / 1024;
            y = 128 + (analysedata[i] + 48.16) * 2.56;
            ctx.fillRect(i, 256 - y, 1, y);
        }
        ctx.fillStyle = "#ff8844";
        for(var d = -50; d < 50; d += 10) {
            var y = 128 - (d * 256 / 100) | 0;
            ctx.fillRect(20, y, 512, 1);
            ctx.fillText(d + "dB", 5, y);
        }
        ctx.fillRect(20, 128, 512, 1);
        for(var f = 2000; f < audioctx.sampleRate / 2; f += 2000) {
            var x = (f * 1024 / audioctx.sampleRate) | 0;
            ctx.fillRect(x, 0, 1, 245);
            ctx.fillText(f + "Hz", x - 10, 255);
        }
    }
});
</script>
</body>
</html>

テストページ:BiquadFilterテスト




g200kg