【WebAudio】スペクトラムアナライザーを作る

2023-03-11

音楽に合わせてバーが動く、スペクトラムアナライザー(またの名をグラフィックイコライザー、通称グライコ)をずっとやってみたかった。 WebAudioを使えば音楽の再生やスペクトラム解析に必要なFFT計算も簡単に利用できるのでやってみた (ハードウェアではなくソフトウェアで)。

デモ

もっとも重要なこと:横軸を対数でプロットする

WebAudioを使ってスペアナを作ろうとしてググった時に出てくるサンプルサイトなどを参考に作ってみても「なんかそれっぽくない…」となってしまう。 問題はAnalyserNode.getByteFrequencyData()で取得した周波数ごとのデータをそのまま横軸にプロットしていくところ。

フーリエ変換した結果を配列に受け取れるが、各要素はサンプリング周波数の半分を等間隔に区切った周波数になる。 しかし人が音を聞き取る感覚としては周波数を対数で感じる。 可聴範囲が20Hz〜20,000Hzとのことでそれを線形にプロットすると低音部分が潰れてしまい、ほとんど見えなくなってしまう。

なので横軸(周波数)を対数となるようにプロットする必要がある。

具体的には、最低20Hzと最高20000Hzをそれぞれ対数log10を取って、横軸に沿って線形補間したものを指数にして周波数に戻して、それに応じたFFT内のデータを取得する:

let analyserNode = null
let dataArray = null

function init(audioCtx) { // AudioContext
analyserNode = audioCtx.createAnalyser()
// fftSizeを設定
const bufferLength = analyserNode.frequencyBinCount
dataArray = new Uint8Array(bufferLength)
}

function update() {
analyserNode.getByteFrequencyData(dataArray) // FFTの結果を取得

const WIDTH = 512
const minHz = 20
const maxHz = 20000
const minHzVal = Math.log10(minHz)
const maxHzVal = Math.log10(maxHz)
const bufferLength = analyserNode.frequencyBinCount
const sampleRate = analyserNode.context.sampleRate
for (let i = 0; i < WIDTH; ++i) {
const e = i / WIDTH * (maxHzVal - minHzVal) + minHzVal // 対数を線形補間
const freq = 10 ** e // 周波数に戻す
const bin = (freq * bufferLength / (sampleRate * 0.5)) | 0 // 対応するインデクス
let v = dataArray[bin]
// vに応じてバーを描画
}
}

LED式の表示のように横をブロックに分けて表示したい場合には対応する範囲の最大値を用いるとよい。

AnalyserNodeの問題:遅延回避にsmoothingTimeConstantを下げる

上の対策だけでそれっぽく動くのだけど、よく見ると少し音より遅れているように見えた。 音と同期してないと気持ちよくないのでなんとかしたかった。

しかし理由が分からなくて悩んだ。 getByteFrequencyData で取得できる値が少し遅れてしまっているのか、 AudioSource から context.destinationAnalyserNode に分けて入れてるのが悪いのか、 requestAnimationFrame に問題があるのか、 そもそもfftSize 分だけ遅れた状態になってしまうからまずいのか、 などとあれこれ推測したが分からなかった。

ひょんな拍子に解決法がわかった。 AnalyserNode.smoothingTimeConstant を下げてやれば解決する。 これは結果があまりブレて見えないように、前回の取得結果との補間をしているっぽい。 デフォルトが0.8で、「ほとんどのケースで十分」などと書かれているが、音のスペアナとして利用する場合にはレスポンスが悪く見えてしまうので下げてやる必要がある(体感0.3以下、実際にはブラックマン窓がどうとかこうとか…)。

細かなこと

デシベル範囲の調整

  • AnalyserNodemaxDecibelsminDecibelsプロパティがあって、得られる値はその範囲に影響する。普通高周波部分は振幅が低いので、見栄え的には別途持ち上げたほうがいいかもしれない

直流成分

  • 普通は問題はないと思うが、場合によってはAnalyserNodeの前に直流成分を取り除いたほうがいいかもしれない。FFTする場合、直流成分が含まれていると結果が劣化するとのことなので、防止のため
    • 「DCオフセット除去」で検索
  • WebAudioでやる場合にはAudioWorkletで移動平均を求めて引いてやればよいでしょう

AudioBufferSourceNodeは不便

  • WebAudioで曲を鳴らすのに最初AudioBufferSourceNodeを使ってみたが、再生中の位置の取得やシークができないので、htmlのaudioメディアタグを使った方がいい
  • audioタグにcontrolsを指定すると自動的にUIも表示されるのでこういう用途に便利

リンク