はじめに
音声に反応する映像をブラウザ上で作れるWebアプリ Audio Reactive 3D Visualizer の内部構造を大幅にリファクタリングしました。
Audio Reactive 3D Visualizerはもともと、音源を読み込んで、音に反応するビジュアルを生成し、MP4として書き出せるWebアプリです。
ただ、機能が増えるにつれて、単なる「ビジュアライザーアプリ」として作り続けるよりも、内部的には ブラウザネイティブな音声ビジュアルエンジン として整理した方が今後拡張しやすいと感じました。
今回のリファクタリングでは、見た目を大きく変えるのではなく、以下のような拡張に耐えられる内部設計を目指しました。
- 新しいビジュアルモードを追加しやすくする
- プリセットをJSONとして管理・共有しやすくする
- Live/VJモードを保守しやすくする
- MP4書き出し処理をUIから切り離す
- OSSとして読みやすく、触りやすい構造にする
なぜリファクタリングしたのか
最初は「音楽に反応する映像を作るWebアプリ」として作っていました。
しかし、機能が増えるにつれて、次のような問題が出てきました。
-
App.tsxが大きくなりすぎる - 状態管理が1つのstoreに集まりすぎる
- 音声解析、描画、書き出し、UIの責務が混ざる
- 新しいビジュアルモードを追加すると既存部分に影響しやすい
- プリセットや設定の互換性を保つのが難しくなる
特に、音声解析・描画・MP4書き出しはそれぞれ独立した大きな責務です。
そのため、内部構造を以下のように分ける方針にしました。
Audio Engine
Visual Engine
Preset System
Export Engine
UI Shell
つまり、Audio Reactive 3D Visualizerを単なるアプリではなく、
ブラウザ上で動く、音声反応型ビジュアル制作エンジン
として扱えるようにするのが今回の目的です。
目指した構成
今回のリファクタリングでは、最終的に以下のような構造を目指しました。
src/
app/
App.tsx
AppShell.tsx
AppLayout.tsx
CanvasStage.tsx
LeftPanel.tsx
RightPanel.tsx
Header.tsx
engine/
audio/
AudioEngine.ts
AudioFrame.ts
OfflineAnalyzer.ts
RealtimeAnalyzer.ts
AudioFeatureStream.ts
visual/
VisualEngine.ts
VisualMode.ts
VisualLayer.ts
RenderContext.ts
registerVisualMode.ts
preset/
PresetSchema.ts
PresetRegistry.ts
builtinPresets.ts
presetMigration.ts
export/
ExportEngine.ts
ExportJob.ts
WebCodecsEncoder.ts
FfmpegWasmEncoder.ts
modes/
visualizer3d/
wave/
imageFx/
state/
audioSlice.ts
playbackSlice.ts
visualSlice.ts
presetSlice.ts
liveSlice.ts
exportSlice.ts
projectSlice.ts
index.ts
features/
uploader/
controls/
live/
export/
presetManager/
ui/
components/
panels/
layout/
utils/
ポイントは、app と engine を分けたことです。
app はユーザーが触る画面の構成を担当し、engine は音声解析・描画・プリセット・書き出しなどの中核処理を担当します。
Zustand storeの分割
まず、状態管理を整理しました。
以前はグローバルstoreに多くの状態が集まっていました。
- 音源ファイル
- 再生状態
- 解析結果
- 表示モード
- ビジュアル設定
- プリセット
- Live/VJモード
- 書き出し状態
これらを1つのstoreで持つと、機能追加のたびにstoreが肥大化していきます。
そこで、責務ごとにsliceへ分割しました。
state/
audioSlice.ts
playbackSlice.ts
visualSlice.ts
presetSlice.ts
liveSlice.ts
exportSlice.ts
projectSlice.ts
index.ts
例えば、音声まわりは audioSlice、再生状態は playbackSlice、書き出し状態は exportSlice に分けています。
これにより、どの状態がどの機能に属しているのかがかなり分かりやすくなりました。
App.tsxを小さくする
次に、App.tsx の責務を減らしました。
リファクタリング前は、App.tsx が以下のような多くの役割を持っていました。
- 全体レイアウト
- ヘッダー
- サイドパネル
- Canvas表示
- Live/VJモードのUI
- 書き出しボタン
- エラー表示
- 状態管理との接続
この状態だと、ちょっとしたUI変更でも App.tsx を触る必要があり、見通しが悪くなります。
そこで、画面を以下のように分解しました。
app/
App.tsx
AppShell.tsx
AppLayout.tsx
Header.tsx
LeftPanel.tsx
RightPanel.tsx
CanvasStage.tsx
理想的には、App.tsx はこのくらい薄くなります。
export default function App() {
return <AppShell />;
}
これにより、アプリ全体の構成と個別機能の実装が分離され、UIの変更もしやすくなりました。
AudioFrameを導入する
音声解析結果を各ビジュアルモードがバラバラに参照すると、モード追加時に実装が複雑になります。
そこで、各モードが共通して使える AudioFrame という型を用意しました。
export type AudioFrame = {
time: number;
progress: number;
volume: number;
low: number;
mid: number;
high: number;
transient: number;
beat: number;
stereoWidth?: number;
waveform?: Float32Array;
spectrum?: Float32Array;
};
この型を通すことで、3D Visualizer、Wave Visualizer、Image FX、Live/VJモード、MP4書き出し処理が、同じ形式の音声情報を扱えるようになります。
ビジュアル側は「音声解析の実装」を意識せず、AudioFrame を受け取って描画に使うだけで済みます。
VisualModeを登録制にする
今後、Audio Reactive 3D Visualizerに新しいビジュアルモードを追加しやすくするために、VisualModeDefinition のような仕組みを用意しました。
export type VisualModeDefinition<TConfig = unknown> = {
id: string;
name: string;
description?: string;
defaultConfig: TConfig;
render: (context: RenderContext<TConfig>) => React.ReactNode;
};
これにより、ビジュアルモードを「アプリ内部に直書きする」のではなく、「登録できるモジュール」として扱えるようになります。
将来的には、以下のような形が可能になります。
registerVisualMode({
id: 'my-visual-mode',
name: 'My Visual Mode',
defaultConfig: {},
render: ({ audioFrame, config }) => {
return <MyVisual audioFrame={audioFrame} config={config} />;
},
});
この仕組みがあると、外部コントリビューターが新しい表示モードを追加しやすくなります。
OSSとしても、「どこを触ればいいか」が分かりやすくなるのが大きいです。
プリセットをJSONフレンドリーにする
Audio Reactive 3D Visualizerでは、見た目の設定や音声反応のパラメータをプリセットとして扱いたいです。
そこで、プリセットを単なるオブジェクトではなく、バージョン管理可能なスキーマとして整理しました。
export type AudioReactivePreset = {
schemaVersion: number;
id: string;
name: string;
author?: string;
description?: string;
mode: string;
visual: Record<string, unknown>;
audioMapping?: Record<string, unknown>;
effects?: Record<string, unknown>;
export?: Record<string, unknown>;
};
これにより、以下のような拡張がしやすくなります。
- 組み込みプリセット
- ユーザープリセット
- JSON import/export
- プリセット共有
- プリセットのバージョン移行
- コミュニティ製プリセット
将来的に「このプリセットかっこいいから共有する」といった使い方ができるように、内部的な土台を整えました。
ExportEngineを分離する
MP4書き出し処理は、かなり重い責務です。
Audio Reactive 3D Visualizerでは、ブラウザ上で映像を書き出すために、WebCodecsやffmpeg.wasmのような仕組みを使います。
この処理がUIコンポーネントに混ざると、保守がかなり大変になります。
そこで、書き出し処理を ExportEngine として分離する方針にしました。
engine/
export/
ExportEngine.ts
ExportJob.ts
WebCodecsEncoder.ts
FfmpegWasmEncoder.ts
UI側は、書き出し処理の詳細を知らなくてよくなります。
const job = exportEngine.createJob({
audioBuffer,
preset,
visualConfig,
exportConfig,
});
await job.run({
onProgress,
onStatus,
onError,
});
UIは進捗やエラーを表示するだけです。
これにより、WebCodecs優先、ffmpeg.wasm fallback、キャンセル処理、進捗通知などをUIから切り離せます。
Live/VJモードのロジックを分離する
Live/VJモードでは、キーボードショートカットやリアルタイム操作が重要になります。
ただ、これも画面コンポーネントに直接書くと、後から変更しづらくなります。
そこで、Live/VJ関連の処理は features/live に分離しました。
features/
live/
useLiveShortcuts.ts
LiveOverlay.tsx
LiveControls.tsx
キーボード操作は useLiveShortcuts に閉じ込め、UI側は必要な状態だけを表示するようにしています。
OSSとして分かりやすい構造にする
今回のリファクタリングでは、単にコードを整理するだけではなく、OSSとして見たときに分かりやすい構造にすることも意識しました。
特に重要だと思ったのは以下です。
- 初見でフォルダ構成が分かる
- 新しいビジュアルモードの追加場所が分かる
- プリセットの仕組みが分かる
- Export処理の流れが分かる
- READMEでプロジェクトの方向性が伝わる
READMEでは、Audio Reactive 3D Visualizerを以下のように説明できるようにしました。
Audio Reactive 3D Visualizer is a browser-native audio visual engine for creating, performing, and exporting reactive music visuals.
また、OSSとしての魅力が伝わるように、以下のような特徴も整理しました。
## Why Audio Reactive 3D Visualizer?
- Browser-native audio analysis
- Real-time VJ mode
- MP4 export
- JSON-shareable presets
- Modular visual modes
- Built for musicians, VJs, and creative coders
「ただのWebアプリ」ではなく、「拡張できるクリエイティブエンジン」として見えることを意識しています。
今回のリファクタリングで得られたメリット
今回のリファクタリングで、以下のようなメリットがありました。
1. 新しい機能を追加しやすくなった
状態管理、UI、音声解析、描画、書き出しが分かれたことで、新機能追加時の影響範囲が小さくなりました。
2. ビジュアルモードを増やしやすくなった
VisualModeDefinition を用意したことで、今後はモード単位で追加・管理しやすくなります。
3. プリセット共有の土台ができた
プリセットをJSONフレンドリーなスキーマにしたことで、将来的なimport/exportや共有機能に繋げやすくなりました。
4. 書き出し処理を保守しやすくなった
MP4書き出し処理を ExportEngine に寄せることで、UIとエンコード処理の責務を分離できました。
5. OSSとして触りやすくなった
フォルダ構成と責務が整理されたことで、初見の人でもコードを追いやすくなりました。
今後やりたいこと
今後は、この内部構造を活かして、以下のような機能を追加していきたいです。
- プリセットのJSON import/export
- コミュニティプリセット
- 新しいビジュアルモードの追加
- 画像・映像素材との合成強化
- Live/VJ向けの操作性向上
- Export設定の拡張
- SDK的に使えるAPIの整備
特に、プリセット共有とVisualMode登録制は、OSSとしてかなり面白くできそうだと思っています。
まとめ
今回、Audio Reactive 3D Visualizerの内部構造を大幅にリファクタリングしました。
目的は、単にコードを綺麗にすることではなく、
音声に反応する映像を作るWebアプリから、ブラウザネイティブな音声ビジュアルエンジンへ近づけること
でした。
リファクタリングによって、状態管理、UI、音声解析、描画、プリセット、書き出し処理の責務が整理され、今後の機能追加やOSSとしての拡張がしやすくなりました。
Audio Reactive 3D Visualizerは、音楽制作者、VJ、クリエイティブコーダーがブラウザだけで音声反応型ビジュアルを作れるツールとして、さらに育てていきたいです。