はじめに
はじめまして、Nagisa Dozonoです。
Web開発と音楽制作の両方を行っています。個人サイトは ndzn.me です。
音楽方面では、7g3(@7g3_na) としてもコンポーザー・作詞家として活動しています。
そのため、自分にとってWeb開発は単なる画面実装ではなく、音楽や映像表現を拡張するための手段でもあります。
今回は、音源をブラウザ上で解析し、その結果を3Dオブジェクト、パーティクル、波形、画像エフェクトへ反映するWebアプリ Audio Reactive 3D Visualizer を作った話を書きます。
リポジトリはこちらです。
作ったもの
Audio Reactive 3D Visualizer は、ローカルの音源ファイルをブラウザに読み込み、その音に合わせて3Dビジュアルを生成するWebアプリです。
大きく分けると、以下のようなことができます。
- 音源ファイルをブラウザ内で読み込む
- 音量、周波数、波形、BPMなどを解析する
- 解析結果を3Dオブジェクトやパーティクルへ反映する
- 波形ビジュアライザーを表示する
- 画像にグロー、ブラー、RGBシフト、ノイズなどのエフェクトをかける
- Live / VJ向けにフルスクリーン表示する
- 作成した映像をMP4として書き出す
想定している用途は、音楽リリース用の告知動画、MVの下書き、VJ素材、音声反応型アート、あるいはThree.js / Web Audio APIの学習用プロジェクトです。
なぜ作ったか
自分は音楽を作る側でもあるので、曲を作ったあとに毎回こう思っていました。
音源を出すだけではなく、音に合った映像もすぐ作りたい
ただ、映像制作ソフトを開いて、素材を並べて、エフェクトを調整して、書き出して……という流れは、曲をたくさん作る人間にとってはかなり重いです。
特にSoundCloud、YouTube、Xなどへ投稿する時、簡単なビジュアライザーがあるだけでも見え方はかなり変わります。
そこで、以下のようなものを作りたいと思いました。
- ブラウザだけで動く
- 音源をアップロードせずローカルで解析できる
- 音の強弱や周波数に合わせて3Dが動く
- そのまま動画として書き出せる
- 音楽制作者がすぐ使える
これがAudio Reactive 3D Visualizerを作り始めた理由です。
技術スタック
主な技術スタックは以下です。
- React
- TypeScript
- Vite
- Three.js
- WebGL
- Web Audio API
- Canvas 2D
- Zustand
- Material UI
- WebCodecs
- mp4-muxer
- ffmpeg.wasm
ReactでUIを作り、Three.jsで3Dシーンを描画し、Web Audio APIで音声解析を行っています。
状態管理にはZustandを使い、音源、解析結果、再生状態、現在時間、表示モード、プリセット、エフェクト設定などを共有しています。
全体構成
ざっくりした構成は以下のようになっています。
src/
audio/
analyze.ts
bpm.ts
fft.ts
visual/
scene.ts
particles.ts
presets.ts
ui/
Uploader.tsx
VisualizerCanvas.tsx
WaveVisualizer.tsx
ImageFXVisualizer.tsx
export/
webcodecs.ts
ffmpeg.ts
recorder.ts
store.ts
主な責務はこんな感じです。
| ファイル | 役割 |
|---|---|
Uploader.tsx |
音源・画像の読み込み |
analyze.ts |
音声解析の入口 |
fft.ts |
スペクトログラム、波形生成 |
bpm.ts |
BPM検出 |
VisualizerCanvas.tsx |
Web Audio APIと3D描画の接続 |
scene.ts |
Three.jsシーン管理 |
particles.ts |
Shaderベースのパーティクル |
presets.ts |
3Dプリセット定義 |
webcodecs.ts |
WebCodecsでのMP4書き出し |
ffmpeg.ts |
ffmpeg.wasmフォールバック |
音源の読み込み
音源はブラウザ内で読み込みます。
対応している形式は、主に以下です。
- wav
- mp3
- flac
- ogg
- aac
音源ファイルは、File から ArrayBuffer に変換し、AudioContext.decodeAudioData() で AudioBuffer に変換します。
イメージとしては以下の流れです。
const arrayBuffer = await file.arrayBuffer();
const ctx = new AudioContext();
const buffer = await ctx.decodeAudioData(arrayBuffer);
setAudioBuffer(buffer);
setDuration(buffer.duration);
const analysis = await analyzeAudio(buffer);
setAnalysis(analysis);
ここで大事にしたのは、音源をサーバーへ送信しないことです。
音楽制作者が使うツールとして考えた時、制作中の音源を外部へアップロードしないで済むことはかなり重要です。
このアプリでは、選択した音源や画像はブラウザ内で処理されます。
音声解析
音声解析では、主に以下を取得しています。
- BPM
- RMS
- 簡易的なLUFS相当値
- 波形
- スペクトログラム
- トランジェント
- ステレオ幅
- 曲のムード
- エネルギー分類
解析の入口は analyzeAudio() です。
処理のイメージはこんな感じです。
export async function analyzeAudio(buffer: AudioBuffer) {
const left = buffer.getChannelData(0);
const right = buffer.numberOfChannels > 1
? buffer.getChannelData(1)
: left;
const bpm = detectBPM(left, buffer.sampleRate);
const waveform = computeWaveform(left, 512);
const spectrum = computeSpectrogram(left, buffer.sampleRate);
const transientMap = detectTransients(left, buffer.sampleRate);
const stereoWidth = computeStereoWidth(left, right);
return {
bpm,
waveform,
spectrum,
transientMap,
stereoWidth,
duration: buffer.duration,
};
}
厳密な音楽解析ライブラリを使っているわけではなく、まずはビジュアル表現に使いやすい形へ変換することを優先しています。
たとえばLUFSも、EBU R128準拠の厳密なラウドネス計測というより、RMSをもとにした簡易的な値として扱っています。
FFTとスペクトログラム
スペクトログラム生成では、FFTサイズを 2048 にしています。
export const FFT_SIZE = 2048;
export const HOP_SIZE = 512;
音源を一定間隔で切り出し、Hanning windowをかけてFFTし、周波数成分を取り出します。
このアプリでは映像側の扱いやすさを考えて、30fpsに近い単位で解析フレームを作っています。
const stepSamples = Math.floor(sampleRate / 30);
つまり、音声解析結果をそのまま映像フレームへ対応させやすくしています。
これはMP4書き出し時にも重要です。
リアルタイム表示だけでなく、動画として書き出す場合も、同じ時間に同じ解析結果を使って描画できるからです。
リアルタイム再生時の解析
事前解析とは別に、再生中は AnalyserNode を使ってリアルタイムに周波数データを取得しています。
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.75;
再生中の周波数データから、ざっくり以下の3帯域に分けています。
bass: 低域
mid : 中域
high: 高域
実装上は、周波数binを割合で分割しています。
const bassEnd = Math.floor(binCount * 0.05);
const midEnd = Math.floor(binCount * 0.35);
そして、それぞれの平均値を 0〜1 に正規化します。
bass = bass / bassEnd / 255;
mid = mid / (midEnd - bassEnd) / 255;
high = high / (binCount - midEnd) / 255;
この bass / mid / high を3Dシーン側へ渡して、オブジェクトのスケール、揺れ、発光、粒子の動きに変換します。
音を3Dに反映する
Audio Reactive 3D Visualizerでは、音をそのまま数値として表示するのではなく、身体感覚に近い形でビジュアルへ変換しています。
たとえば、自分の中では以下のような対応にしています。
bass → 大きな動き、スケール、押し出し
mid → 回転、うねり、流れ
high → 細部、粒子、発光、微細な振動
キックや低域が鳴ったら、画面全体が押し出されるように動く。
中域が鳴ったら、形がうねる。
高域が鳴ったら、細かい粒子や発光が反応する。
この対応にすると、単なる音量メーターではなく、「音楽が空間として動いている」ような見え方になります。
Three.jsシーン
3D描画は VisualizerScene クラスで管理しています。
主に以下を持っています。
THREE.WebGLRendererTHREE.SceneTHREE.PerspectiveCamera- ParticleSystem
- Geometry Mesh
- Waveform Line
- Background Plane
- Post-processing用RenderTarget
- ShaderMaterial
Rendererは以下のような設定にしています。
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: false,
alpha: false,
powerPreference: 'high-performance',
preserveDrawingBuffer: true,
});
preserveDrawingBuffer は、録画やフレーム書き出しとの相性を考えて有効にしています。
カメラはPerspectiveCameraです。
this.camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
);
音に合わせてただMeshを揺らすだけではなく、カメラ距離、モーフ量、エフェクト、波形、パーティクルなどを組み合わせて、1つのビジュアルとして成立するようにしています。
ParticleSystem
パーティクルは THREE.Points と ShaderMaterial を使っています。
各粒子には以下のような属性を持たせています。
- position
- phase
- size
phase を持たせることで、すべての粒子が同じ動きをするのではなく、少しずつずれた動きを作れます。
頂点Shader側では、音の値を使って位置を変化させています。
// bassで外側へ広がる
pos += normalize(pos) * uBass * 1.5;
// transientで爆発的に広がる
pos += normalize(pos) * uTransient * 2.0;
// highで細かく揺れる
pos.y += sin(pos.x * 5.0 + uTime * 2.0 + aPhase) * uHigh * 0.5;
ここでやりたかったのは、ただ音量に合わせて粒子サイズを変えるだけではなく、音の帯域ごとに違う身体性を与えることです。
低域は空間全体を押す。
高域は表面を震わせる。
トランジェントは瞬間的に爆発させる。
こうすると、音の構造がそのまま動きの構造になります。
プリセット
3D表示にはいくつかのプリセットを用意しています。
たとえば以下のようなものがあります。
- Minimal Geometry
- Neon Particle Storm
- Glitch Industrial
- Ambient Organic
各プリセットは、粒子数、背景色、粒子色、アクセントカラー、Geometryモード、ノイズスケール、反応速度などを持っています。
export interface PresetConfig {
particleCount: number;
backgroundColor: THREE.Color;
particleColor: THREE.Color;
accentColor: THREE.Color;
geometryMode: 'particles' | 'sphere' | 'torus' | 'wave';
noiseScale: number;
reactionSpeed: number;
}
自分は音楽を作るときも、曲ごとに質感がかなり変わります。
そのため、1つの見た目に固定するより、曲の雰囲気に合わせてプリセットを切り替えられる方が自然でした。
激しい曲にはGlitch系。
空間的な曲にはOrganic系。
シンプルに見せたい時はMinimal系。
そういう使い分けを想定しています。
Wave VisualizerとImage FX
3Dだけではなく、Wave VisualizerとImage FXも用意しています。
Wave Visualizerでは、横波形、円形波形、バー表示などを切り替えられるようにしています。
Image FXでは、画像に対して以下のような効果を加えられます。
- glow
- blur
- RGB shift
- noise
- distortion
- pulse
これは、ジャケット画像やアートワークを読み込んで、音に合わせて少し動かす用途を想定しています。
音楽投稿では、必ずしも派手な3Dが必要とは限りません。
ジャケットを中心にして、少しだけ揺らす。
波形だけ表示する。
背景として抽象的な3Dを使う。
そういった複数の使い方に対応できるようにしました。
Live / VJ Mode
Live / VJ Modeでは、UIを隠してフルスクリーン表示できるようにしています。
音源に合わせてリアルタイムに動く画面を、そのままOBSなどで取り込む想定です。
この機能は、実際にオンラインイベントや簡易VJ用途を考えた時に必要だと思って入れました。
エクスポート用のツールとしてだけでなく、ライブパフォーマンスにも使える形を目指しています。
MP4書き出し
このアプリでは、作成したビジュアルをMP4として書き出せます。
基本方針は以下です。
- WebCodecsが使える場合はWebCodecsで高速に書き出す
- 使えない場合や失敗した場合はffmpeg.wasmへフォールバックする
- 1920×1080 / 30fpsで書き出す
WebCodecs側では、Canvasから VideoFrame を作成し、VideoEncoder でエンコードします。
const videoFrame = new VideoFrame(canvas, {
timestamp: Math.round(time * 1_000_000),
duration: Math.round(1_000_000 / fps),
});
音声側は AudioEncoder を使い、mp4-muxer で映像と音声をMP4へまとめます。
WebCodecsが使えない環境では、Canvasフレームを画像として保存し、ffmpeg.wasmでMP4化する流れにしています。
この部分はかなり重い処理なので、今後も改善余地があります。
作っていて難しかったところ
1. 音の反応を気持ちよくすること
単純に音量に合わせてオブジェクトを拡大縮小するだけだと、かなり単調になります。
音楽には、低域、中域、高域、トランジェント、余韻があります。
それぞれを違う種類の動きに割り当てることで、ようやく「音楽に反応している」感じが出ます。
この調整は、コードというよりも音楽的な感覚に近かったです。
2. リアルタイム表示と書き出しの両立
リアルタイム再生では AnalyserNode から現在の音を取得できます。
一方、MP4書き出しでは、任意の時刻のフレームを順番に描画する必要があります。
つまり、リアルタイム表示とエクスポートでは、音声解析の扱い方が少し変わります。
そのため、事前解析した AudioAnalysis を使って、指定時刻の解析結果をサンプリングし、エクスポート時にも同じように描画できるようにしています。
3. ブラウザだけで完結させること
音源解析、3D描画、動画書き出しをすべてブラウザ内で完結させるのは便利ですが、当然負荷も大きいです。
特にMP4書き出しでは、長い音源や重いシーンだとメモリやCPUをかなり使います。
そのため、WebCodecsを優先しつつ、必要に応じてffmpeg.wasmへフォールバックする構成にしました。
4. 見た目とパフォーマンスのバランス
パーティクルは増やせば増やすほど派手になりますが、当然重くなります。
プリセットごとに粒子数やノイズ量、反応速度を分けて、見た目と負荷のバランスを取っています。
また、Shaderでまとめて動かせる部分はCPU側で毎フレーム大量計算しないようにしています。
反省点
作ったあとに見えてきた改善点もあります。
- モバイル対応はまだ弱い
- 長尺音源の書き出しは重い
- 自動テストがまだ少ない
- UIのアクセシビリティ改善余地がある
- プリセットの保存・共有機能があると便利
- ブラウザ互換性の検証をもっと増やしたい
特に、音楽制作者向けツールとして考えるなら、プリセット保存や共有機能はかなり重要だと思っています。
今後やりたいこと
今後は以下を改善していきたいです。
- プリセット保存機能
- 共有可能なURL
- レスポンシブ対応
- より軽いMP4書き出し
- 音声解析の精度向上
- ステレオ情報を使った空間表現
- HPL / binaural的な表現への対応
- VJ向けショートカット強化
- UI全体の整理
- アクセシビリティ改善
個人的には、単なるビジュアライザーではなく、音楽制作者が自分の曲の世界観をすぐに映像化できるツールにしていきたいです。
まとめ
Audio Reactive 3D Visualizerは、自分の音楽制作とWeb開発の関心がかなり自然に混ざったプロジェクトです。
Web Audio APIで音を解析し、Three.js / WebGLで空間として表現し、それをそのまま動画として書き出す。
やっていることは技術的にはWebアプリですが、自分の中ではかなり音楽制作に近い感覚でした。
音をどう動きに変換するか。
低域をどう見せるか。
高域をどう光らせるか。
曲の空気感をどう画面に出すか。
そういうことを考えながら実装していました。
Web技術は、音楽や映像表現のかなり近いところまで来ていると思います。
このプロジェクトが、音楽制作者やクリエイティブコーダーの参考になれば嬉しいです。
リポジトリはこちらです。