Web Audio API 解説
10.フィルターの使い方
Biquad Filterとは
Web Audio API では汎用的なフィルタとして「BiquadFilter」、日本語では「双2次フィルター」という形式のものが用意されています。これはオーディオ系の信号処理でも頻繁に使用されるものです。この BiquadFilter には Robert Bristow-Johnson と言う人が書いた有名な設計手法の解説があって、通称「RBJ EQ Cookbook」などと呼ばれているのですが、Web Audio API の仕様でもフィルターの詳細についてはその「RBJ EQ Cookbook」を参考にしているという記述があります。
以前私もこの RBJ EQ Cookbook を凄く適当に翻訳してみた事がありますので、フィルターの中身に興味があれば参照してください。
フィルターの種類としては次の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":オールパス |
frequency | AudioParam | Hz | 350 | 0~ナイキスト周波数 | カットオフ周波数。範囲のナイキスト周波数はサンプルレートの1/2。このパラメータは a-rate です。 |
detune | AudioParam | セント | 0 | -∞ ~ +∞ | デチューン。カットオフ周波数をずらします。単位の「セント」は半音の 1/100 で 1200 で 1 オクターブになります。このパラメータは a-rate です。 |
Q | AudioParam | なし | 1 | -∞ ~ +∞ | フィルターのQ。このパラメータは a-rate です。 |
gain | AudioParam | dB | 0 | -∞ ~ +∞ | デシベルで表されるフィルターのゲインです。ローシェルフ/ハイシェルフ/ピーキングの場合のみ使用されます。このパラメータは 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テスト