Web Audio API 解説
12.アナライザーの使い方
アナライザーノードでできる事
既に「フィルターの使い方」の時にも使いましたが、Analyser ノードは信号のスペクトラム情報や生データを取得するために使用します。
使い方によってはこのスペクトラム情報を元になんらかの音の加工を行ったり、精密な測定を行ったりする事も可能ですが、どちらかと言えばビジュアライザーなどでスペクトルの表示を簡単に行うという目的に特化していて、仕様書にも「ビジュアリゼーションアプリ」用と言うような記述があります。
アナライザーからデータを取得するのは下表の4つの関数なのですが、どれも関数を呼び出した時点でもっとも直近の分析データを返します。なので、入力信号の全てを一律に周波数分析にかけてどうこうすると言うよりはインターバルタイマーか何かで動いているスペクトルアナライザーのグラフ表示関数が必要に応じてここからデータを取得して描画する、というような使い方がもっとも合っているようです。
getFloatFrequencyData(Float32Array array) | Float32Array にスペクトル情報を取得する。 |
getFloatTimeDomainData(Float32Array array) | Float32Array に信号の生データを取得する。 |
getByteFrequencyData(Uint8Array array) | Unit8Array にスペクトル情報を取得する。 |
getByteTimeDomainData(Uint8Array array) | Uint8Array に信号の生データを取得する。 |
特に下の 2 つは出力が Uint8Array ですので、本来の信号からはかなり情報が欠落しています。どちらもほぼ同じ形でデータが取れるようになっていますので、スイッチ 1 つで「スペクトル表示」「波形表示」を切り替えると言うような用途が想定されていると思われます。出力値も Uint8 の 0-255 の範囲で音楽などの一般的な信号レベルに対していい感じに動くような状態にデフォルトのレンジが設定されています。
横軸方向は周波数リニアで、設定した fftSize と配列のインデックス i に対して (audioctx.sampleRate * i / fftSize) となります。つまり、fftSize を1024とすればインデックスが 512 の位置がナイキスト周波数になります。ビジュアライザーとして使用する場合はもう少し狭い範囲で使用しても良いかも知れません。
getFloatFrequencyData(Float32Array array) については、信号を FFT にかけた後のマグニチュードをポイント数で割って正規化し、対数を取って dB 化した値が返されるようで、通常の音楽信号なら大体 -30 ~ -60 あたりの値が出力されます。単位は dBFS/Hz のようなものですから、ピュアトーンを入力した場合などで狭い範囲にエネルギーが集中していれば -30 を越えて 0 に近づきますが、FFT の性質上の -6dB と窓関数の影響での -6dB がありますので、最大のピークで -12 のあたりです。
Uint8 で取り出す場合にはこの値をもとに minDecibels プロパティと maxDecibels プロパティで指定される範囲が 0-255 にマップされます。minDecibels のデフォルト値は -100、maxDecibels のデフォルト値は-30です。
アナライザーノードのサンプル
BufferSource で適当に音を流して Analyser に入れ、setInterval() で Canvas に描画しています。
プロパティの smoothingTimeConstant は 0-1 の範囲でスペクトラムデータの動きの速さを設定します。0 だともっとも速く、1 に近づくほど遅くなります。これは TimeDomain データには影響しません。
minDecibels/maxDecibels は通常は触らなくても良いと思います。横方向は fftSize が 1024 で、読み出しているデータが 256 ですからサンプリングレートの 1/4 までのグラフになります。
またこの Analyser ノードは destination を除けば唯一出力をどこにも接続しなくて良いと明記されているノードですが、一応出力端子があって入力がスルーで出てきます。
せっかくなのでグラフ描画にはグラデーションをつけてビジュアライザーぽくしています。
<!DOCTYPE html>
<html>
<body>
<h1>Analyser</h1>
<button id="play">Play</button> <button id="stop">Stop</button><br/>
<table>
<tr><th>Frequency/TimeDomain</th><td><select id="mode"><option>Frequency</option><option>TimeDomain</option></select></td></tr>
<tr><th>SmoothingTimeConstant</th><td><input id="smoothing" type="range" min="0" max="1" step="0.01" value="0.9"/></td><td id="smoothingval">0.9</td></tr>
</table>
<br/>
<canvas id="graph" width=256 height=256></canvas>
<script>
window.addEventListener("load", async ()=>{
const audioctx = new AudioContext();
const soundbuf = await LoadSample(audioctx, "./loop.wav");
let mode = 0;
let src = null;
const analyser = new AnalyserNode(audioctx, {smoothingTimeConstant:0.9});
document.getElementById("play").addEventListener("click",()=>{
if(audioctx.state=="suspended")
audioctx.resume();
if(src == null) {
src = new AudioBufferSourceNode(audioctx, {buffer:soundbuf,loop:true});
src.connect(analyser).connect(audioctx.destination);
src.start();
}
});
document.getElementById("stop").addEventListener("click",()=>{
if(src) src.stop();
src = null;
});
document.getElementById("mode").addEventListener("change",(ev)=>{
mode = ev.target.selectedIndex;
});
document.getElementById("smoothing").addEventListener("input",(ev)=>{
analyser.smoothingTimeConstant = document.getElementById("smoothingval").innerHTML = ev.target.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);
})
});
}
const canvasctx = document.getElementById("graph").getContext("2d");
const gradbase = canvasctx.createLinearGradient(0, 0, 0, 256);
gradbase.addColorStop(0, "rgb(20,22,20)");
gradbase.addColorStop(1, "rgb(20,20,200)");
const gradline = [];
for(let i = 0; i < 256; ++i) {
gradline[i] = canvasctx.createLinearGradient(0, 256 - i, 0, 256);
const n = (i & 64) * 2;
gradline[i].addColorStop(0, "rgb(255,0,0)");
gradline[i].addColorStop(1, "rgb(255," + i + ",0)");
}
function DrawGraph() {
canvasctx.fillStyle = gradbase;
canvasctx.fillRect(0, 0, 256, 256);
const data = new Uint8Array(256);
if(mode == 0) analyser.getByteFrequencyData(data); //Spectrum Data
else analyser.getByteTimeDomainData(data); //Waveform Data
for(var i = 0; i < 256; ++i) {
canvasctx.fillStyle = gradline[data[i]];
canvasctx.fillRect(i, 256 - data[i], 1, data[i]);
}
}
setInterval(DrawGraph, 100);
});
</script>
</body>
</html>
テストページ:アナライザーの使い方