SonicStage は、音楽ファイルを再生しながら映像を出す音楽ビジュアライザです。WebAudio の解析ノードから周波数・波形・音量・テンポを取り出し、7 テーマで描き分けます。音声はどこにも送らず、端末内で再生・解析します。
#🎯 音を「4 つの数」に落とす
ビジュアライザは、テーマごとに描画をゼロから書くと大変です。7 テーマ書けば 7 通りの解析コードができて、直すときに 7 箇所いじることになります。
なので解析と描画を完全に分離しました。1 フレームごとに、音を決まった形の「解析結果」に落とします。
return { freq, wave, rms, bass, mid, treble, t: audioCurrentTime, bpm }描画側はこの箱だけを見ます。音がどう鳴っているかは知らなくていい。テーマを足すときは描画関数を 1 つ書くだけになりました。
#📊 周波数を 3 帯域にまとめる
getByteFrequencyData は 128 本の帯域を返しますが、そのまま使うと描画が細かくなりすぎます。実用上は低音・中音・高音の 3 つで足りることが多い。
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)の時系列を溜めて自己相関を取ります。
const rms = Math.sqrt(s / wave.length)
bpmTracker.push(rms)
const bpm = bpmTracker.compute()RMS は波形の二乗平均平方根 ── つまり「その瞬間どれくらい鳴っているか」です。曲にビートがあれば、この値が周期的に山を作ります。
自己相関は「時系列を τ だけずらして自分と掛け合わせ、いちばん一致する τ を探す」処理で、その τ が拍の間隔になります。鼻歌を楽譜にするサービスでピッチ検出に使ったのと同じ考え方を、時間スケールを変えて使っています。
BPM が取れると、テーマ側で「拍に合わせて色を変える」ができます。取れなくても(環境音のような曲では取れません)、描画は rms だけで成立するようにしてあります。取れないことがある値に依存させないのは、この手の解析では大事でした。
#🎨 7 テーマ
| テーマ | 見せ方 |
|---|---|
| Cyber | 周波数バー + 色収差 |
| Vinyl | 33⅓ 回転のレコード盤 |
| Aurora | sin 波を 5 重に重ねたカーテン |
| Sakura | 花びらの粒子が 80 個 |
| Vaporwave | パースのかかったグリッド + 反射 |
| Minimal | 中心円と多重リング |
| DJ | 波形とレベルメーター |
テーマごとに配色を CSS 変数として動的に注入しています。
--ss-bg / --ss-accent / --ss-inkCanvas の描画色だけでなく、周りの UI(ボタンや枠)も一緒に色が変わるので、テーマを切り替えたときに画面全体が変わります。Canvas だけ色を変えると、そこだけ浮きます。
#😓 苦労したところ
TypeScript の strict と WebAudio の型。 getByteFrequencyData は Uint8Array<ArrayBuffer> を要求しますが、素直に new Uint8Array(n) すると型が合わないことがあります。
analyser.getByteFrequencyData(freq as Uint8Array<ArrayBuffer>)キャストで通していますが、理由をコメントに残しました。なぜキャストしたかが書いていないと、後から見た人が消してしまいます。
フレームごとに配列を作らない。 freq と wave は毎フレーム使い回しています。60fps で毎回 new Uint8Array すると、ガベージコレクションが走って映像がカクつきます。解析結果の箱は 1 つを使い回す設計にしました。
音が鳴らないと何も出ない。 初回訪問で真っ黒だと離脱するので、ファイルを選ぶ前でもデモの動きが出るようにしています。
曲によって迫力が違う。 音圧の高い曲は派手に動き、静かな曲はほとんど動きません。感度のスライダーを付けて、正規化の強さを触れるようにしました。自動正規化も検討しましたが、静かな曲が静かに見えるのも情報なのでやめました。
#🔭 今後の拡張
- マイク入力(その場の音に反応させる)
- 動画としての書き出し
- テーマのパラメータ調整(色・粒子数)
- 歌詞ファイルの同期表示
#💡 このサービスから言えること
解析と描画の間に「決まった形の箱」を置くと、あとが楽になります。 7 テーマ書いても解析は 1 つ。テーマを足すコストがほぼ描画だけになりました。
そして取れないことがある値(BPM)を必須にしないこと。曲によっては拍が検出できませんが、そのときも rms だけで映像が成立するようにしてあります。解析系は「いつも取れる値」と「取れたら使う値」を分けておくと壊れません。
[ ./next_action ]
読んだら、 SonicStage を実際に動かす。
この開発ログは SonicStage をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。