• #tokyo-soundscape
  • #leaflet
  • #python
  • #librosa
  • #panns
  • #audio
  • #nuxt3
  • #cloudflare
  • #vue3
開発tokyo-onkeiメモ

浅草寺サウンドスケーププレイヤーの開発

GPXデータから歩行ルートを地図上に描画し、録音した環境音をリアルタイムで解析・可視化するプレイヤーを作った。UIはMGSのソリトンレーダーをモチーフにしたミリタリーテック調。JSXデモから始めて、最終的にNuxt 3のVueコンポーネントとしてデプロイするところまで進めた。

JSXデモからHTMLデモへの変換

出発点はReact/JSXで書かれたプロトタイプだった。GPXデータのパース、Leaflet地図上へのルート描画、音声再生の3つを統合する必要があり、まずは依存を減らすためにプレーンなHTMLデモに変換した。

Leafletの地図タイルはCartoDB Dark Matterを継続使用。GPXデータからポリラインを描画し、再生位置に連動してマーカーが移動する仕組みを組んだ。

Python音声解析パイプラインの構築

録音データの特徴量を事前に抽出するパイプラインをPythonで構築した。

librosaによる基本解析

  • dB計算: RMS値からデシベル値を算出し、時系列データとしてJSON出力
  • スペクトログラム生成: mel spectrogramを画像として書き出し

PANNsによる音分類

大規模事前学習済みの音響イベント検出モデル(PANNs)を導入した。AudioSetの527カテゴリで音を分類できる。人声、車両、鳥、風など環境音の構成要素を時間軸に沿って可視化するのが目的。

モデルファイルは312MBあり、ダウンロードで一手間かかった。Windows環境にはwgetがなく、urllibで手動ダウンロードして対処した。

# PANNs推論の核心部分
# 527カテゴリの確率を時間窓ごとに出力
framewise_output = model(waveform)['framewise_output']

解析結果はすべてJSONファイルとして書き出し、フロントエンドで読み込む構成にした。リアルタイム解析ではなく事前計算方式を選んだのは、ブラウザ側の負荷を最小限にしたかったため。

MGSソリトンレーダー風UI

UIのコンセプトはMetal Gear Solidのソリトンレーダー。回転するスキャンライン、同心円のソナーリング、PANNsで検出した人声に連動するアラート表示を組み合わせた。

コックピットレイアウト

画面を左右に分割し、地図が2/3計器パネルが1/3のコックピットレイアウトにした。計器パネル側にレーダー、dBゲージ、スペクトログラム、タイムラインを縦に配置する。

フロー型情報(リアルタイムに変化するもの)とストック型情報(全体の概要)を意識的に分けて配置した。

  • フロー型: dBゲージ、レーダー(現在の音環境)
  • ストック型: スペクトログラム全体像、サウンドレイヤータイムライン

dBアークゲージ

当初はバー型のdBメーターだったが、コックピット感を出すためにSVGアークゲージに変更した。stroke-dasharraystroke-dashoffsetで弧の長さを制御する方式。

スペクトログラムの表示方式

最初はリアルタイムに描画していたが、全体のプリレンダリング画像 + 白線カーソルの方式に変更した。音声全体の構造が一目で分かるほうが、散歩の記録として見返すときに有用だと判断した。

サウンドレイヤータイムライン

PANNsの分類結果を横軸に時間、縦軸にカテゴリで並べたタイムライン。2つの表示モードを実装した。

  • 全体表示: 録音全体の音構成を俯瞰(白線カーソルが現在位置を示す)
  • コンポジション: 現在位置周辺のリアルタイムバー表示

アニメーションの調整

レーダーのスキャンラインやdBゲージの針など、生データをそのまま反映すると動きがカクつく問題があった。lerp(線形補間)で値をスムージングし、CSS transitionと組み合わせることで滑らかな動きにした。

// lerp補間でスムージング
const smoothed = prev + (target - prev) * 0.15;

音声シーク問題の解決

開発中にシーク(再生位置の変更)が効かない問題にはまった。原因はサーバーがHTTP Range Requestsに対応していなかったこと。音声ファイルをfetchしてBlob URLに変換する方式で解決した。

const response = await fetch(audioUrl);
const blob = await response.blob();
audio.src = URL.createObjectURL(blob);

全体をメモリに読み込むので大きなファイルには向かないが、数分程度の散歩録音なら問題ない。

MGS風HUDオーバーレイ

画面四隅にMGS風のHUD要素を追加した。

  • SIGINT: 音声信号の検出状態
  • NAV: GPSナビゲーション情報(座標、速度)
  • CONTACT: PANNsで検出した音源のコンタクト情報

ステータス表示は「トランスポート状態」(再生/停止/シーク)と「音環境状態」(dB、検出カテゴリ)を分離した。再生操作と環境情報が混在すると視認性が下がるため。

配色の統一

全体をMGS風のミリタリーテック色調に統一した。暗い背景にグリーン系のアクセント、走査線風のオーバーレイ。Leafletの地図タイルもDark Matterなので全体のトーンが揃う。

Nuxt 3 Vueコンポーネントへの移植

HTMLデモで固まった実装を Player.client.vue としてNuxt 3に移植した。.clientサフィックスを付けることでSSR時のLeaflet/Audio APIエラーを回避している。

移植時のポイントは、解析済みJSONファイルをR2(Cloudflare R2)に配置し、プレイヤーが非同期で読み込む構成にしたこと。音声ファイルもR2にアップロードした。

conceptサイトへのデプロイ

R2への音声ファイル・解析JSONのアップロードを行い、conceptサイトに統合してデプロイした。浅草寺の散歩録音をサンプルデータとして使用。

ふりかえり

事前計算方式にしたことで、フロントエンドはJSONの読み込みと描画に集中でき、実装がシンプルになった。PANNsの527カテゴリは粒度が細かすぎるので、表示時にグルーピングする処理が今後必要になりそう。

MGS風UIは遊びで始めたが、環境音の監視・解析という文脈に意外とマッチした。ソリトンレーダーの「周囲を探知する」というメタファーが、環境音プレイヤーの目的と重なる。

Blob URL方式でのシーク対応は、Range Requests非対応環境でのワークアラウンドとして覚えておきたい。

次のステップ

  • PANNsカテゴリのグルーピング(527 → 10~15の大分類)
  • 複数スポットの切り替え対応
  • モバイル表示の最適化(コックピットレイアウトの縦積み化)
  • 歩行速度と音環境の相関分析