フーリエ変換を実装せずに、Web Audio APIでスペクトラムを表示する

calender 2022.2.7   account Sena

ブラウザ上でオーディオを再生して、そのスペクトラムを表示させたい。

一般的にはスペクトラムを表示するには、波形に対してフーリエ変換をしなければならないのですが、実はフーリエ変換を実装せずに「Web Audio API」を利用することで、スペクトラムを表示させることが可能です。今回、このAPIを利用して実際にコードを動かしてみます。

Web Audio APIとは

Web Audio API を利用することで、音声にエコーやリバーブなどのエフェクトをかけることはもちろん、リアルタイム音声処理やゲーム効果音生成、立体音響生成など、複雑な音声処理も簡単に実装できるようになります。

この Web Audio APIの機能の一つに、オーディオソースから周波数、波形、その他のデータを抽出し、可視化する機能があります。この機能を利用してスペクトラム表示することができます。

Visualizations with Web Audio API - Web API | MDN

ftt_result作成したプログラムの実行結果のイメージ

実装上のポイント

オーディオファイルの用意

まず音声ファイルが必要になります。WAV, MP3, AAC, OGGおよびその他複数のフォーマットの音声ファイルがサポートされていますが、ブラウザごとにサポートする音声フォーマットは異なります。 今回は、audio.mp3というオーディオファイルを用意し、後述するhtmlファイルと同ディレクトリに配置します。

オーディオファイルの読み込み

オーディオファイルは、ArrayBufferで取得します。ArrayBufferとは、音声や動画, 画像などのバイナリデータの配列です。Arrayインスタンスの配列との違いは、バイナリデータを直接扱うためアクセスがより高速になります。

request.responseType = 'arraybuffer';
request.open('GET', 'audio.mp3', true);
request.send();

データの流れ(AudioNode)

データの流れは、source(入力) → analyzer → audio.destination(出力)となり、AudioNode.connect() で繋げていきます。これらはどれもAudioNodeを継承したクラスです。

AudioNode.connect() - Web APIs | MDN

const audio:    AudioContext          = new (window.AudioContext || window.webkitAudioContext)();
const analyser: AnalyserNode          = audio.createAnalyser();
const source:   AudioBufferSourceNode = audio.createBufferSource();
// ===略===
source.connect(analyser);
analyser.connect(audio.destination);

コード

これを踏まえて実装してみましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Play Audio</title>
</head>
<body>
<button>PLAY</button>
<canvas class="visualizer" width="1280" height="960"></canvas>
<script>
  
    const FFT_SIZE = 8192; // FFTのサイズ指定(大きくすれば棒の数が増える)

    document.querySelector('button').addEventListener('click', function () {
        // canvasの設定
        const canvas    = document.querySelector('.visualizer');
        const canvasCtx = canvas.getContext("2d");

        // グラフ描画
        const visualize = function () {
            const WIDTH  = canvas.width;
            const HEIGHT = canvas.height;

            analyser.fftSize      = FFT_SIZE;
            const bufferLengthAlt = analyser.frequencyBinCount;
            const dataArrayAlt    = new Uint8Array(bufferLengthAlt);

            canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

            const draw = function () {
                requestAnimationFrame(draw);
                
                //スペクトラムへ変換
                analyser.getByteFrequencyData(dataArrayAlt);

                canvasCtx.fillStyle = 'rgb(0, 0, 0)';
                canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);

                const barWidth = (WIDTH / bufferLengthAlt) * 2.5; // 適当な横幅の調整
                let barHeight;
                let x          = 0;

                for (let i = 0; i !== bufferLengthAlt; ++i) {
                    barHeight = dataArrayAlt[i];

                    canvasCtx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)'; // 色の変更
                    canvasCtx.fillRect(x, HEIGHT - barHeight * (HEIGHT / 150), barWidth, barHeight * (HEIGHT / 150)); // 棒の描画

                    x += barWidth + 1;
                }
            };

            draw();
        }

        // オーディオ取得・設定
        const audio                    = new (window.AudioContext || window.webkitAudioContext)();
        const analyser                 = audio.createAnalyser();
        analyser.minDecibels           = -90;
        analyser.maxDecibels           = -10;
        analyser.smoothingTimeConstant = 0.85;
        const request                  = new XMLHttpRequest();
        
        request.onload                 = function () {
            const buffer = request.response;
            audio.decodeAudioData(buffer, function (buffer) {
                const source     = audio.createBufferSource();
                source.buffer    = buffer;
                source.start     = source.start || source.noteOn;
                source.stop      = source.stop || source.noteOff;
                source.loop      = false;
                source.loopStart = 0;
                source.loopEnd   = buffer.duration;
                source.start(0);
                source.connect(analyser);
                analyser.connect(audio.destination);
                visualize();
            });
        };
        
        // オーディオファイルの読み込み
        request.responseType = 'arraybuffer';
        request.open('GET', 'audio.mp3', true);
        request.send();
    });
</script>
</body>
</html>

Web Audio API を使うことで、非常に簡単に実装することができますね。

以下は実際に動作するコードです。
音源はEhren Starksが作曲したPaper Lightsです。
音が再生されるので、ご注意下さい。

See the Pen spectrum-web-analyzer by FOURIER Inc. (@fourier-inc) on CodePen.

まとめ

今回は、Web Audio API のAnalyserNode(analyser)を使うことによってフーリエ変換を実装せずに、周波数領域のスペクトラムを表示することが出来ました。Web Audio API にはこの他にも様々なAPIが用意されています。上述したとおり、例えば、音源にエフェクトをかけたり、立体音響化するなど様々な表現が可能です。また機会があったら実装してみたいと思います。

Sena

Sena / Engineer

生涯に亘り技術を極めていきたい。

Company name and Fourier's iconic logo

株式会社フーリエは、インターネット軸にICT(情報通信技術)を通じて「生活をもっと便利に、企業をもっと活動的に」するため、デジタルと現実社会で流れる情報の『繋ぎ手』として、ICTを利活用した企業活動の調査分析から企画・プロモーション・開発・運営まで総合的に支援しています。

コーポレートサイトを見る