AI Dev Lab
SonicStage

SonicStage ができるまで — 音楽を 6 テーマの映像にする

音楽ビジュアライザの開発ログ。 WebAudio の解析ノードから周波数・波形・音量・テンポを取り出し、 Canvas に 6 種類のテーマで描き分ける構成と、 テーマごとに配色を差し替える仕組みを記録しています。

SonicStage は、音楽ファイルを再生しながら映像を出す音楽ビジュアライザです。WebAudio の解析ノードから周波数・波形・音量・テンポを取り出し、7 テーマで描き分けます。音声はどこにも送らず、端末内で再生・解析します。

#🎯 音を「4 つの数」に落とす

ビジュアライザは、テーマごとに描画をゼロから書くと大変です。7 テーマ書けば 7 通りの解析コードができて、直すときに 7 箇所いじることになります。

なので解析と描画を完全に分離しました。1 フレームごとに、音を決まった形の「解析結果」に落とします。

ts
return { freq, wave, rms, bass, mid, treble, t: audioCurrentTime, bpm }

描画側はこの箱だけを見ます。音がどう鳴っているかは知らなくていい。テーマを足すときは描画関数を 1 つ書くだけになりました。

#📊 周波数を 3 帯域にまとめる

getByteFrequencyData は 128 本の帯域を返しますが、そのまま使うと描画が細かくなりすぎます。実用上は低音・中音・高音の 3 つで足りることが多い。

ts
const bassEnd = Math.floor(len * 0.08)
const midEnd = Math.floor(len * 0.4)
// それぞれの平均を 0〜1 に正規化
const bass = bSum / (bassEnd * 255 || 1)

区切りを 8% と 40% にしているのは、FFT の出力が線形周波数だからです。人の聴覚は対数的で、低音は狭い範囲に集中しています。均等に 3 分割すると、低音が 1/3 の帯域を占めることになり、実際の聞こえ方と合いません。

|| 1 はゼロ除算の保険です。極端に小さい FFT サイズを渡されても落ちません。

「キックで画面が揺れる」「シンバルで粒が散る」といった演出は、この bass / treble を見るだけで作れます。

#🥁 テンポは音量の自己相関で

BPM は、音量(RMS)の時系列を溜めて自己相関を取ります。

ts
const rms = Math.sqrt(s / wave.length)
bpmTracker.push(rms)
const bpm = bpmTracker.compute()

RMS は波形の二乗平均平方根 ── つまり「その瞬間どれくらい鳴っているか」です。曲にビートがあれば、この値が周期的に山を作ります。

自己相関は「時系列を τ だけずらして自分と掛け合わせ、いちばん一致する τ を探す」処理で、その τ が拍の間隔になります。鼻歌を楽譜にするサービスでピッチ検出に使ったのと同じ考え方を、時間スケールを変えて使っています。

BPM が取れると、テーマ側で「拍に合わせて色を変える」ができます。取れなくても(環境音のような曲では取れません)、描画は rms だけで成立するようにしてあります。取れないことがある値に依存させないのは、この手の解析では大事でした。

#🎨 7 テーマ

テーマ見せ方
Cyber周波数バー + 色収差
Vinyl33⅓ 回転のレコード盤
Aurorasin 波を 5 重に重ねたカーテン
Sakura花びらの粒子が 80 個
Vaporwaveパースのかかったグリッド + 反射
Minimal中心円と多重リング
DJ波形とレベルメーター

テーマごとに配色を CSS 変数として動的に注入しています。

--ss-bg / --ss-accent / --ss-ink

Canvas の描画色だけでなく、周りの UI(ボタンや枠)も一緒に色が変わるので、テーマを切り替えたときに画面全体が変わります。Canvas だけ色を変えると、そこだけ浮きます。

#😓 苦労したところ

TypeScript の strict と WebAudio の型。 getByteFrequencyDataUint8Array<ArrayBuffer> を要求しますが、素直に new Uint8Array(n) すると型が合わないことがあります。

ts
analyser.getByteFrequencyData(freq as Uint8Array<ArrayBuffer>)

キャストで通していますが、理由をコメントに残しました。なぜキャストしたかが書いていないと、後から見た人が消してしまいます。

フレームごとに配列を作らない。 freqwave は毎フレーム使い回しています。60fps で毎回 new Uint8Array すると、ガベージコレクションが走って映像がカクつきます。解析結果の箱は 1 つを使い回す設計にしました。

音が鳴らないと何も出ない。 初回訪問で真っ黒だと離脱するので、ファイルを選ぶ前でもデモの動きが出るようにしています。

曲によって迫力が違う。 音圧の高い曲は派手に動き、静かな曲はほとんど動きません。感度のスライダーを付けて、正規化の強さを触れるようにしました。自動正規化も検討しましたが、静かな曲が静かに見えるのも情報なのでやめました。

#🔭 今後の拡張

  • マイク入力(その場の音に反応させる)
  • 動画としての書き出し
  • テーマのパラメータ調整(色・粒子数)
  • 歌詞ファイルの同期表示

#💡 このサービスから言えること

解析と描画の間に「決まった形の箱」を置くと、あとが楽になります。 7 テーマ書いても解析は 1 つ。テーマを足すコストがほぼ描画だけになりました。

そして取れないことがある値(BPM)を必須にしないこと。曲によっては拍が検出できませんが、そのときも rms だけで映像が成立するようにしてあります。解析系は「いつも取れる値」と「取れたら使う値」を分けておくと壊れません。

[ ./next_action ]

読んだら、 SonicStage を実際に動かす。

この開発ログは SonicStage をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →