[Noise 入門 #56] Audio Analysis & FFT — 音響解析の基礎とノイズへの接続
はじめに
いよいよ第6集の中核、「音とノイズの融合(Audio-Reactive)」の世界へ足を踏み入れます。
これまでは「時間(Time)」という一定のペースで流れるパラメータでノイズを動かしてきましたが、ここからは「音響(Audio)」という予測不能な波をノイズにぶつけます。
今回はその土台作りとして、ブラウザの Web Audio API を用いて音の波を解析し、そのデータをThree.jsのShader(GLSL)に送り込むための「架け橋」を構築します。
前回の記事:
[Noise 入門 #55] 魔法文字とグリフ(Procedural Alien Runes)— 極座標とVoronoiが描く未知の言語
Three.jsとGLSLを用いて、数式から意味ありげなシンボルを絞り出すアプローチ。Voronoiのセル境界と空間の円環的な歪みを掛け合わせ、インタラクティブな魔法文字を錬成するプロセスを直感的に学びます。
humanxai.infoYoutube:
Audio Reactive Domain Warping (Three.js / GLSL)
Audio-reactive visualizer built with #Threejs and #GLSL.Real-time frequency analysis using Web Audio API (FFT).Tech Stack:- Three.js (WebGL)- GLSL Shaders (S...
www.youtube.com1. 音を「数式」に変換する魔法:FFT(高速フーリエ変換)
FFT(高速フーリエ変換)は、オーディオビジュアル表現において最も重要な「心臓部」と言っても過言ではありません。
ここでは、ただの「波」でしかない音のデータを、Shaderで扱える「数値(0.0〜1.0)」に変換するまでの具体的なプロセスと実装ロジックを深掘りします。
「時間領域」と「周波数領域」の違いを理解する
まず、私たちが普段耳にしている音や、マイクから入力される生データは「時間領域(Time Domain)」のデータです。これは「1秒間に空気がどう振動したか」を示す、ぐちゃぐちゃに混ざり合った1本の複雑な波(ミックスジュース)です。
しかし、Shaderで「低音が鳴った時だけ画面を光らせたい」と思った場合、この複雑な1本の波からは「今、低音が鳴っているかどうか」を判断できません。
そこでFFT(高速フーリエ変換)の出番です。 FFTは、この複雑な波を瞬時に計算し、「周波数領域(Frequency Domain)」のデータに変換してくれます。これにより、「この瞬間、20Hz(低音)の波がこのくらい、1000Hz(中音)の波がこのくらい含まれている」という、成分ごとの棒グラフ(スペクトログラム)のような配列データを得ることができます。
Web Audio APIでのデータの取り出し方
ブラウザの Web Audio API には、このFFT計算を自動でやってくれる AnalyserNode という便利なモジュールが用意されています。
設定する重要なプロパティは fftSize です。これは「波をどれくらい細かく切り刻んで解析するか」という解像度(分解能)を表します。
- fftSize は2の累乗(256, 512, 1024, 2048など)で指定します。
- 解析結果として得られるデータの配列の長さ(frequencyBinCount)は、常に fftSize の半分になります。
- 例:fftSize = 512 に設定すると、0 〜 255 までの256個の配列が得られます。
配列のインデックス [0] に近いほど低い音(Bass)、配列の後ろに行くほど高い音(High)の音量(0〜255の数値)が入っています。
Bass, Mid, Highを抽出する実装ロジック
得られた配列データを切り分けて、GLSLに送るための 0.0 ~ 1.0 の数値(Multiplier)を作る具体的なコードの流れは以下のようになります。
// アニメーションループ内で毎フレーム実行する処理
// 1. 解析結果を格納する配列(Uint8Array)を用意
const dataArray = new Uint8Array(analyser.frequencyBinCount);
// 2. 現在の瞬間の周波数データを配列に書き込む(0〜255の数値が入る)
analyser.getByteFrequencyData(dataArray);
// 3. Bass(低音)の抽出
// 例:配列の最初の数個(インデックス 0〜5 あたり)がキックドラムなどの低音域
let bassSum = 0;
for (let i = 0; i < 5; i++) {
bassSum += dataArray[i];
}
// 平均を出して、255で割ることで 0.0 ~ 1.0 の範囲に正規化する
const bassNormalized = (bassSum / 5) / 255.0;
// 同様に、Mid(中音域)、High(高音域)も任意のインデックス範囲で計算する
// ※どこからどこまでをMidとするかは、曲のジャンルや演出によって調整します。
このように抽出した bassNormalized などの数値を、Three.jsの ShaderMaterial の uniforms に渡し、GLSL内でノイズの歪み係数や色の明るさに掛け合わせることで、音が視覚に直結する表現が生まれます。
2. JS側の実装:音を解析してShaderへ送る
理論がわかったところで、いよいよThree.js側のJavaScriptコードを書いていきます。 やるべきことは非常にシンプルで、「毎フレーム音を解析し、その数値をShaderに渡し続ける」というループを作ることです。
実装は大きく3つのステップに分かれます。
① Web Audio API のセットアップと落とし穴
まずはブラウザの音声エンジンである AudioContext を立ち上げ、解析器(AnalyserNode)を準備します。
// ==========================================
// 1. Web Audio API のセットアップ
// ==========================================
// ブラウザ間の互換性を吸収しつつAudioContextを生成
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioCtx.createAnalyser();
// fftSize(分解能)を設定。2の累乗である必要があります。
analyser.fftSize = 256;
// 解析データを受け取るための配列を準備(長さはfftSizeの半分=128になります)
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
// ※ 実際の運用では、ここで <audio> タグやマイク入力(MediaStream)をanalyserに接続します。
// source.connect(analyser);
⚠️ 重要な注意点(ブラウザの自動再生ポリシー) 現代のブラウザは、ユーザーが画面をクリックしたりタップしたりする前に、勝手に音を鳴らすこと(AudioContextの起動)を禁止しています。そのため、実際のアプリケーションでは「Start」や「Play」といったボタンを用意し、そのクリックイベントの中でこのセットアップを行うのが鉄則です。
② ShaderMaterial の Uniforms 定義
次に、抽出した音のデータをGLSLへ送るための「受け皿」を用意します。
// ==========================================
// 2. ShaderMaterial の Uniforms 定義
// ==========================================
const uniforms = {
u_time: { value: 0.0 },
u_audio_bass: { value: 0.0 }, // 低音の強さ(0.0 ~ 1.0)
u_audio_high: { value: 0.0 } // 高音の強さ(0.0 ~ 1.0)
};
const material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: `...`,
fragmentShader: `...`
});
u_audio_bass と u_audio_high という変数を定義しました。初期値は 0.0(無音)です。この値をアニメーションループの中で毎フレーム書き換えていきます。
③ アニメーションループでの抽出と正規化(ここが心臓部!)
ここが Audio-Reactive 表現の最も面白い部分です。 getByteFrequencyData() を呼ぶと、先ほど用意した dataArray(長さ128の配列)の中に、現在鳴っている音の周波数ごとのボリューム(0 〜 255)が瞬時に書き込まれます。
// ==========================================
// 3. アニメーションループでの更新(毎フレーム実行)
// ==========================================
function animate() {
requestAnimationFrame(animate);
// 現在の周波数データを配列(dataArray)に取得
analyser.getByteFrequencyData(dataArray);
// ----------------------------------------
// 【低音域(Bass)の抽出】
// 配列の先頭付近(インデックス 0〜9)の平均値を取得
// ----------------------------------------
let bassSum = 0;
for(let i = 0; i < 10; i++) {
bassSum += dataArray[i];
}
// 平均を出した後、最大値である255で割って 0.0〜1.0 に正規化
const bassLevel = (bassSum / 10) / 255.0;
// ----------------------------------------
// 【高音域(High)の抽出】
// 配列の後半付近(インデックス 100〜109)の平均値を取得
// ----------------------------------------
let highSum = 0;
for(let i = 100; i < 110; i++) {
highSum += dataArray[i];
}
const highLevel = (highSum / 10) / 255.0;
// ----------------------------------------
// Shaderへデータを送信!
// ----------------------------------------
uniforms.u_audio_bass.value = bassLevel;
uniforms.u_audio_high.value = highLevel;
uniforms.u_time.value += 0.01;
renderer.render(scene, camera);
}
抽出のテクニックと「正規化」の理由
インデックスはどの音を表しているのか? 配列の [0] に近いほど低い音、末尾(今回は [127])に近いほど高い音が入っています。 キックドラムの「ドン!」という強い低音を拾いたい場合は [0]〜[5] あたりを、ハイハットの「チキチキ」という高音を拾いたい場合は [100] 付近をサンプリングします。(※正確なHz数はオーディオのサンプリングレートに依存しますが、視覚表現においては「配列の最初の方=低音」という直感的な理解で十分機能します)
なぜ 255.0 で割るのか?(正規化) getByteFrequencyData() が返す値は 0(無音)から 255(最大音量)までの整数です。 しかし、GLSLのShaderは基本的に 0.0 ~ 1.0 の数値を扱うのが大得意です。そこで、取得した値を 255.0 で割ることで、「音の強さを 0.0(0%)〜 1.0(100%)の係数(Multiplier)」に変換しています。
この 0.0 ~ 1.0 の係数さえ作ってしまえば、あとはGLSL側でノイズの強さに掛け算するだけで、すべてが音に連動し始めます。
3. GLSL側の実装:ノイズが音で「脈打つ」
JavaScriptのパイプラインが開通し、無事に u_audio_bass(低音の強さ)というデータがShaderに毎フレーム送られてくるようになりました。
あとは、これまでに培った Domain Warping(空間のねじれ) の数式に、この音の係数を組み込むだけです。世界が音に反応して脈打ち始める、最も楽しい瞬間のコードを見ていきましょう。
uniform float u_time;
uniform float u_audio_bass; // JSから送られてくる低音データ (0.0 ~ 1.0)
// ※ ここにいつもの snoise(vec2 v) 関数がある想定
void main() {
// uv座標の正規化
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
// --------------------------------------------------
// ① Audio-Reactive Domain Warping(音で空間を歪ませる)
// --------------------------------------------------
// ベースとなる歪み用のノイズを生成し、その「強さ(振幅)」に音量を掛ける
float warpNoise = snoise(uv * 5.0 + u_time);
vec2 warpedUV = uv + warpNoise * (u_audio_bass * 0.5);
// --------------------------------------------------
// ② 歪んだ座標を使ってメインのノイズを生成
// --------------------------------------------------
float n = snoise(warpedUV * 10.0 - u_time);
// --------------------------------------------------
// ③ Audio-Reactive Emission(音で発光させる)
// --------------------------------------------------
// 基本の明るさ(1.0) に、音量に比例した発光エネルギーを足す
vec3 color = vec3(n) * (1.0 + u_audio_bass * 2.0);
gl_FragColor = vec4(color, 1.0);
}
コードの解説:何が起きているか?
この短いコードの中で、音(u_audio_bass)は 2つの異なる役割 を果たしています。
1. 空間の破壊(Domain Warpingへの干渉)
warpNoise * (u_audio_bass * 0.5) の部分が核です。 平常時(無音時)、u_audio_bass は 0.0 に近い値になります。つまり歪み係数がゼロになるため、ただの穏やかなノイズが表示されます。 しかし、キックドラムが「ドン!」と鳴った瞬間、値が 0.8 や 1.0 に跳ね上がります。その瞬間だけ空間を歪める力が働き、ノイズ全体が「ビクッ!」と激しくねじ曲がる視覚効果(Glitch / Impact)が生まれます。
2. エネルギーの放出(色と明るさへの干渉)
(1.0 + u_audio_bass * 2.0) の部分です。 なぜただの掛け算ではなく 1.0 + にしているのでしょうか? もし color * u_audio_bass にしてしまうと、無音の時に画面が真っ暗(0.0)になってしまいます。 1.0(平常時の明るさ)をベースとして確保しつつ、音が鳴った時だけ + 2.0(200%の追加発光)のエネルギーを上乗せすることで、ビートに合わせて画面がフラッシュするようなダイナミックな演出になります。
まとめ:音を「見える化」する第一歩
今回は、目に見えない「音」という振動を数学の力で分解し、GPU(Shader)へと繋ぐ「情報の神経系」を構築しました。
- FFT (高速フーリエ変換): 複雑な波を周波数(低音・高音)に分解する魔法。
- Web Audio API: ブラウザ上でリアルタイムに音を解析し、数値化するエンジン。
- Normalization(正規化): 解析データを 0.0 ~ 1.0 に変換し、Shaderが最も扱いやすい形に加工する技術。
このパイプラインが通ったことで、あなたの書くノイズは「ただ動くもの」から、「周囲の世界に反応して躍動する生命体」へと進化しました。
次回への布石:空間の破壊と再構築
これが「音」と「Shader」を繋ぐ最も基礎的なパイプラインです。 データを抽出して uniform に流し込む。たったこれだけのことですが、この u_audio という変数はノイズのあらゆるパラメータ(スケール、速度、歪みの強さ、色の明るさ)に掛け合わせることができます。
次回([Noise 入門 #57] Audio-Reactive Domain Warping — 音で歪む空間)では、この基盤の上で空間のねじれを本格的にチューニング。 キックドラムの音圧で空間自体が破壊されるような、より実践的でサイケデリックなVFXを作り上げます。
数式に「リズム」を宿す旅は、まだ始まったばかりです。
実装内容
今回のプロジェクトでは、単に音に反応させるだけでなく、ブラウザの制約を回避しつつ、数学的な「うねり」を視覚化することに注力しました。
1. 安定したオーディオ解析パイプライン
- AudioBufferによるデコード: ブラウザのセキュリティ(CORS)制限を回避し、波形データを100%確実に取得するため、
<audio>タグではなくバイナリデータからの直接デコードを採用しました。 - FFT(高速フーリエ変換):
analyser.fftSize = 256に設定し、低音域(Bass)と高音域(High)を特定のインデックスから抽出しました。 - データの正規化とスムージング: 取得した
0〜255の値を0.0〜1.0に正規化し、線形補間(Lerp)を用いて滑らかな脈動を実現しました。
2. Audio-Reactive Domain Warping のロジック
- 低音(Bass)の役割: 低音のエネルギーを Domain Warping の 「歪みの強さ(Warp Strength)」 と 「時間の進行度」 に割り当て、キックに合わせて空間がドロドロと溶けるような変形を実装しました。
- 高音(High)の役割: 高音の鋭さをノイズの 「粒子のざわつき(周波数)」 に反映させ、シンバルやハイハットの音に合わせて細部が激しく振動するように設計しました。
- 発光エフェクト(Emission): 単純な加算ではなく、ノイズの密度(
n)に基づいた指数的な発光処理を行い、色が飽和しすぎない「深みのある発光」を追求しました。
3. インタラクティブなデバッグ環境
- FFTビジュアライザ: 解析中の生データを画面左下にリアルタイム描画し、どの周波数がShaderに影響を与えているか視覚的に確認できるモニターを搭載しました。
- lil-gui による動的制御: ノイズのスケールや歪みの強度、感度などを、音楽を再生しながらリアルタイムに微調整できるインスペクターを実装しました。
COMM_LOG: noise-intro-56-web-audio-api