Web Audio APIによるスペクトログラムの実装
概要
Web Audio APIを使ってスペクトログラムを実現した実例を紹介します。
ここで肝となるのはAnalyserNodeです。AnalyserNodeを音声波形データの流れの途中に挿入すると、そこを通過している波形データやそのフーリエ変換後の周波数データを任意のタイミングで取り出すことが可能になります。
この実例では、audio要素から作成したMediaElementAudioSourceNodeを波形データソースとして使用しています。
デモ
音声の再生を開始すると同時に。スペクトログラムの更新が開始されます。音源は京王7000系の一区間走行音です。「FFT Size」を変更すると、時間と周波数の解像度が変化します。「Min. decibels」と「Min. decibels」を変更すると、スペクトログラムのダイナミックレンジや明るさが変化します。
FFT Size:
Min. decibels:
-100
Max. decibels:
-30
ソース
HTML
<div>
<canvas class="canvas" width="640" height="480"></canvas>
</div>
<div>
<audio class="audio" controls>
<source src="keio7000.ogg" type="audio/ogg">
<source src="keio7000.mp4" type="audio/mp4">
<source src="keio7000.mp3" type="audio/mp3">
<source src="keio7000.wav" type="audio/wav">
</audio>
</div>
<div>
FFT Size:
<select class="fft-size">
<option value="512">512</option>
<option value="1024">1024</option>
<option value="2048" selected>2048</option>
<option value="4096">4096</option>
<option value="8192">8192</option>
</select>
</div>
<div>
Min. decibels:
<input class="min-decibels" type="range" min="-120" max="0" value="-100" step="1">
<span class="min-decibels-span">-100</span>
</div>
<div>
Max. decibels:
<input class="max-decibels" type="range" min="-120" max="0" value="-30" step="1">
<span class="max-decibels-span">-30</span>
</div>
JavaScript
/**
* 再生中か否かを表す
* @type {Boolean}
*/
let isPlaying = false;
/**
* 音声コンテンツ要素
* @type {HTMLAudioElement}
*/
const audio = document.querySelector('.audio');
/**
* スペクトログラム描画キャンバス
* @type {HTMLCanvasElement}
*/
const canvas = document.querySelector('.canvas');
// ---------- 描画準備 ----------
// 描画コンテキストの取得
const renderingContext = canvas.getContext('2d');
// 周波数強度と色のマッピングを作成
const colorMap = generateColorMap({ r: 255, g: 0, b: 0 }, { r: 255, g: 255, b: 0 });
// スペクトログラム描画キャンバスの塗りつぶし
renderingContext.fillStyle = colorMap[0];
renderingContext.fillRect(0, 0, canvas.width, canvas.height);
// ---------- オーディオ準備 ----------
// 音声コンテキストの作成
const audioContext = new AudioContext();
// ノード作成
const mediaElementSourceNode = audioContext.createMediaElementSource(audio);
const analyserNode = audioContext.createAnalyser();
// スムージングの無効
analyserNode.smoothingTimeConstant = 0;
// ノード接続
mediaElementSourceNode.connect(analyserNode);
analyserNode.connect(audioContext.destination);
// ---------- イベント ----------
audio.onplay = function () {
// スペクトログラムの更新開始
start();
};
audio.onpause = function () {
// スペクトログラムの更新停止
stop();
};
audio.onended = function () {
// スペクトログラムの更新停止
stop();
};
// FFTサイズの変更
document.querySelector('.fft-size').onchange = function (event) {
analyserNode.fftSize = event.target.value;
};
// スペクトログラムに表示可能な周波数強度下限の指定
document.querySelector('.min-decibels').onchange = function (event) {
analyserNode.minDecibels = event.target.value;
document.querySelector('.min-decibels-span').textContent = event.target.value;
};
// スペクトログラムに表示可能な周波数強度上限の指定
document.querySelector('.max-decibels').onchange = function (event) {
analyserNode.maxDecibels = event.target.value;
document.querySelector('.max-decibels-span').textContent = event.target.value;
};
// ---------- 関数 ----------
/**
* スペクトログラムの更新開始
*/
function start() {
if (!isPlaying) {
isPlaying = true;
requestAnimationFrame(function mainLoop() {
// 音声を再生している間、スペクトログラムを更新
if (isPlaying) {
// スペクトログラム更新処理
feedSpectrogram();
requestAnimationFrame(mainLoop);
}
});
}
}
/**
* スペクトログラムの更新停止
*/
function stop() {
isPlaying = false;
}
/**
* 周波数強度と色のマッピングの作成
* @param {{r: Number, g: Number, b: Number}[]} dark - スペクトログラムの暗部色
* @param {{r: Number, g: Number, b: Number}[]} light - スペクトログラムの明部色
* @returns {String[]} - スタイルシート色設定文字列の配列
*/
function generateColorMap(dark, light) {
const result = [];
for (let i = 0; i < 256; i++) {
let rate = i / (256 - 1);
rate = rate * rate;
let r, g, b;
if (rate < 0.33) {
const coef = (rate - 0) / (0.33 - 0);
r = 0 + dark.r * coef;
g = 0 + dark.g * coef;
b = 0 + dark.b * coef;
} else if (rate < 0.66) {
const coef = (rate - 0.33) / (0.66 - 0.33);
r = dark.r * (1 - coef) + light.r * coef;
g = dark.g * (1 - coef) + light.g * coef;
b = dark.b * (1 - coef) + light.b * coef;
} else {
const coef = (rate - 0.66) / (1 - 0.66);
r = light.r * (1 - coef) + 255 * coef;
g = light.g * (1 - coef) + 255 * coef;
b = light.b * (1 - coef) + 255 * coef;
}
// 計算したRGB値をCSSの<color>データ型に変換
result[i] = 'rgb(' + r + ', ' + g + ', ' + b + ')';
}
return result;
}
/**
* スペクトログラムの更新
*/
function feedSpectrogram() {
// スペクトログラム画像を1ピクセル左にずらす
renderingContext.drawImage(canvas, -1, 0);
// 0から255の間に正規化された周波数強度データを取得
const frequencyData = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(frequencyData);
// 右端の幅1ピクセルの領域に、周波数強度を色に変換して描画
for (let i = 0; i < canvas.height; i++) {
// 描画色指定
if (i < frequencyData.length) {
renderingContext.fillStyle = colorMap[frequencyData[i]];
} else {
renderingContext.fillStyle = colorMap[0];
}
// 周波数強度を1ピクセル描画
renderingContext.fillRect(canvas.width - 1, canvas.height - 1 - i, 1, 1);
}
}