はじめに
音に合わせて動くビジュアライザーを作るとき、最初に必要になるのは「音を数値として取り出すこと」です。
たとえば、
- 音量が大きいときに画面を光らせる
- キックやベースに合わせてオブジェクトを膨らませる
- ハイハットやノイズに合わせて細かい粒子を動かす
- 波形を使って線やパーティクルを揺らす
といった表現を作るには、音声ファイルをただ再生するだけでは足りません。
そこで今回は、Web Audio API を使って、
- volume
- bass
- mids
- highs
- waveform
を取得する最小実装を作ってみます。
作るもの
ローカルの音声ファイルを読み込んで、ブラウザ上で解析します。
最終的には、以下のような値を取得できるようにします。
type AudioFeatures = {
volume: number
bass: number
mids: number
highs: number
waveform: Float32Array
}
それぞれの意味は以下です。
| 値 | 内容 |
|---|---|
volume |
音全体の大きさ |
bass |
低域の強さ |
mids |
中域の強さ |
highs |
高域の強さ |
waveform |
波形データ |
音声ビジュアライザーを作る場合、まずこの形まで持っていけると、その後の3D表現やUIに使いやすくなります。
全体の流れ
Web Audio API では、ざっくり以下のような流れで音を解析します。
audio element
↓
AudioContext
↓
MediaElementSource
↓
AnalyserNode
↓
time domain data / frequency data
↓
volume / bass / mids / highs / waveform
重要なのは AnalyserNode です。
AnalyserNode を使うと、再生中の音声から時間領域データと周波数領域データを取得できます。
HTML側
まずは音声ファイルを選択する input と、再生用の audio 要素を用意します。
<input id="fileInput" type="file" accept="audio/*" />
<audio id="audio" controls></audio>
<pre id="output"></pre>
選択した音声ファイルは、サーバーにアップロードせず、ブラウザ上で object URL として扱います。
const fileInput = document.querySelector<HTMLInputElement>('#fileInput')
const audio = document.querySelector<HTMLAudioElement>('#audio')
fileInput?.addEventListener('change', () => {
const file = fileInput.files?.[0]
if (!file || !audio) {
return
}
const url = URL.createObjectURL(file)
audio.src = url
})
これで、ローカルの音源をブラウザ上で再生できます。
AudioContextを作る
次に AudioContext を作成します。
const audioContext = new AudioContext()
const source = audioContext.createMediaElementSource(audio)
const analyser = audioContext.createAnalyser()
source.connect(analyser)
analyser.connect(audioContext.destination)
createMediaElementSource(audio) によって、HTMLAudioElement の音を Web Audio API の処理対象にできます。
その後、AnalyserNode につないで、さらに destination に接続します。
audio
↓
source
↓
analyser
↓
speakers
この接続を忘れると、音が鳴らなかったり、解析できなかったりします。
AnalyserNodeの設定
AnalyserNode にはいくつか設定があります。
analyser.fftSize = 2048
analyser.smoothingTimeConstant = 0.8
fftSize は周波数解析の細かさに関わります。
値を大きくすると細かく取れますが、そのぶん処理も重くなります。
smoothingTimeConstant は値のなめらかさです。
音に対して細かく反応させたい場合は低め、ゆったり反応させたい場合は高めにします。
データ用の配列を作る
時間領域データと周波数領域データを取得するために、配列を用意します。
const frequencyData = new Uint8Array(analyser.frequencyBinCount)
const timeDomainData = new Uint8Array(analyser.fftSize)
周波数データは getByteFrequencyData で取得します。
analyser.getByteFrequencyData(frequencyData)
時間領域データは getByteTimeDomainData で取得します。
analyser.getByteTimeDomainData(timeDomainData)
volumeを計算する
音量は、時間領域データから RMS を使って計算します。
function calculateVolume(timeDomainData: Uint8Array): number {
let sum = 0
for (const value of timeDomainData) {
const normalized = (value - 128) / 128
sum += normalized * normalized
}
return Math.sqrt(sum / timeDomainData.length)
}
getByteTimeDomainData で取得できる値は 0〜255 です。
中心が 128 なので、そこから引いて -1〜1 くらいの範囲に正規化します。
その後、二乗平均平方根を取ることで、音量のような値にできます。
周波数帯域を分ける
次に、低域・中域・高域を取ります。
今回は以下のように分けます。
bass: 20Hz - 250Hz
mids: 250Hz - 2000Hz
highs: 2000Hz - 8000Hz
まず、周波数から配列の index に変換する関数を作ります。
function frequencyToIndex(
frequency: number,
sampleRate: number,
fftSize: number
): number {
return Math.round((frequency / (sampleRate / 2)) * (fftSize / 2))
}
次に、指定した周波数帯域の平均値を取得します。
function getBandEnergy(
frequencyData: Uint8Array,
sampleRate: number,
fftSize: number,
minHz: number,
maxHz: number
): number {
const startIndex = frequencyToIndex(minHz, sampleRate, fftSize)
const endIndex = frequencyToIndex(maxHz, sampleRate, fftSize)
let sum = 0
let count = 0
for (let i = startIndex; i <= endIndex; i++) {
sum += frequencyData[i] ?? 0
count++
}
if (count === 0) {
return 0
}
return sum / count / 255
}
frequencyData の値は 0〜255 なので、最後に 255 で割って 0〜1 に近い値にしています。
bass / mids / highs を取得する
先ほどの関数を使うと、各帯域はこのように取得できます。
const bass = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
20,
250
)
const mids = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
250,
2000
)
const highs = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
2000,
8000
)
これで、音をざっくり低域・中域・高域に分けられます。
音楽的には、
- bass: キック、サブベース、低音
- mids: ボーカル、シンセ、コード、メロディ
- highs: ハイハット、ノイズ、クリック感
のような反応に使えます。
waveformを作る
波形データは、時間領域データを -1〜1 の値に変換して作ります。
function createWaveform(timeDomainData: Uint8Array): Float32Array {
const waveform = new Float32Array(timeDomainData.length)
for (let i = 0; i < timeDomainData.length; i++) {
waveform[i] = ((timeDomainData[i] ?? 128) - 128) / 128
}
return waveform
}
この waveform は、線を描いたり、パーティクルを揺らしたりするのに使えます。
音量だけで動かすと単調になりやすいですが、波形を混ぜると細かい揺れが出しやすくなります。
解析ループを作る
最後に、requestAnimationFrame で毎フレーム解析します。
function analyze() {
analyser.getByteFrequencyData(frequencyData)
analyser.getByteTimeDomainData(timeDomainData)
const volume = calculateVolume(timeDomainData)
const bass = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
20,
250
)
const mids = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
250,
2000
)
const highs = getBandEnergy(
frequencyData,
audioContext.sampleRate,
analyser.fftSize,
2000,
8000
)
const waveform = createWaveform(timeDomainData)
const features = {
volume,
bass,
mids,
highs,
waveform,
}
console.log(features)
requestAnimationFrame(analyze)
}
analyze()
これで、再生中の音からリアルタイムに特徴量を取得できます。
自動再生制限に注意する
Web Audio API を使うときに注意したいのが、ブラウザの自動再生制限です。
ユーザー操作なしに AudioContext を開始しようとすると、うまく動かないことがあります。
再生ボタンなど、ユーザー操作のタイミングで resume() するようにしておくと安全です。
await audioContext.resume()
await audio.play()
音声系のWebアプリで「音が鳴らない」「解析が始まらない」ときは、まずここを疑うとよいです。
まとめ
今回は、Web Audio API を使って、
- volume
- bass
- mids
- highs
- waveform
を取得する最小実装を作りました。
音声ビジュアライザーを作るときは、最初から派手な表現を作ろうとするよりも、まず音を数値として扱えるようにするのが大事です。
音を再生する
↓
AnalyserNodeで解析する
↓
volume / bass / mids / highs / waveform に分ける
↓
UIや3D表現に使う
この流れができると、あとは Three.js や Canvas、SVG などに渡して、音に反応する表現を作れるようになります。