🎐
音栞(otoshiori)
消えていく日本の音風景(商店街、廃止前の踏切、木造駅舎、祭りなど)を、一次体験の文章とともに記録・アーカイブするWebサービス。
本番環境: otoshiori.chibiham.app
コンセプト
環境音を「記憶の栞」として集め、聴き、残す。
- 本物の場所の音であることが必須。生成AIによる音の修復・生成は採用しない
- 自分で録音した「消えゆく」音と、Wikimedia CommonsやFreesoundなどのオープン素材による「既に消えた」音の両方をアーカイブする
- 音源には録音者の一次体験の文章を添え、単なる音素材集ではなく「その場所の記憶」として残す
アーキテクチャ
- Cloudflare Workers + OpenNext: 全ページSSGのNext.js 15を
@opennextjs/cloudflareでWorkersにデプロイ - Cloudflare R2: 音源配信。カスタムドメインを設定し、Rangeリクエスト対応のCORSを構成
- Cloudflare D1: 再生回数の集計と寄稿の台帳管理。サーバーレスSQLiteで運用コストゼロ
- Terraform + Terraform Cloud: R2バケット・カスタムドメイン・D1をIaCで管理。CDパイプラインで
terraform apply→D1マイグレーション→デプロイまで自動化 - モノレポ構成:
apps/web(アプリ)/packages/content(コンテンツ層)/tools(Python音声処理)/terraform(IaC)をpnpm workspaceで管理
技術剪定のポイント
- Cloudflareフルスタック
- 音声ファイル(2〜4MB/本)の配信コストが最大の懸念だったため、エグレス無料のR2を軸に選定
- 全ページSSGで動的処理は再生カウントと寄稿受付のみ。Workers + D1で十分であり、サーバー運用が不要
@opennextjs/cloudflare@cloudflare/next-on-pagesはdeprecatedのため採用せず。全ページSSGなのでincremental cacheはstatic-assets(読み取り専用)で足りる
- Content-as-Code(DBレス設計)
- Phase 1では音源数が少ないため、DBではなくMDXファイル + Zodスキーマで管理
- コンテンツ取得は
SoundRepositoryパターンで抽象化し、将来UGCが増えたらD1/Supabase実装に差し替え可能な設計
- Web Audio API(標準API)
- ライブラリを使わず標準APIで実装。環境音のループ再生に必要な「サンプル精度」と「クロスフェード制御」は
<audio>要素では実現できないため
- ライブラリを使わず標準APIで実装。環境音のループ再生に必要な「サンプル精度」と「クロスフェード制御」は
特徴的な実装
サンプル精度のループ再生エンジン
<audio loop>はエンコーダ遅延により末尾にギャップが生じるため、AudioBufferSourceNodeのloopStart/loopEndによるサンプル精度ループを実装。
さらに「ながく聴く」モードでは、ゲームオーディオの環境音処理技術を応用したデュアルレイヤー再生を実装している。
- 同一バッファから再生位置を半周ずらした2つのレイヤーを生成
- 90秒周期のequal-powerクロスフェードで入れ替え、固定ループ特有の反復感を消す
- equal-powerフェード曲線は「クロスフェード中の音圧(二乗和)が一定」であることをユニットテストで検証
ミックス機能とURL共有
複数の音源とノイズベッド(ホワイト/ピンクノイズ)を音量調整して重ねられるミキサーを実装。ミックス構成はquery stringにシリアライズされ、URLだけで共有・復元できる。
/sounds?m=slug1:0.8,slug2:0.5&n=pink:0.2&master=0.8&loop=dual
Python音声前処理パイプライン
録音素材を配信用音源に加工するCLIツールを自作。
- 安定区間検出: librosaでRMSの移動分散とオンセット密度を解析し、突発音の少ない連続区間を自動選定
- ループ化: 区間末尾にequal-powerクロスフェードを適用し、末尾→先頭が無音で繋がるよう加工
- 整音: ハイパスフィルタ + EBU R128ラウドネス正規化(ループ境界の連続性を保つため線形ゲインのみ、圧縮・EQは不使用)
- エンコード: Opus 96kbpsで2〜4MBに圧縮(例: 17分/19MBの録音 → 195秒/2.1MB)
モバイルでのデコード後メモリ(70〜110MB)を考慮して音源長の上限を設計している。
ビルド時コンテンツ検証
MDXのfrontmatterをZodスキーマで検証し、不正なメタデータ(loopEnd > duration、未定義の都道府県・シーンタグ、外部素材のライセンス表記漏れなど)をビルド失敗として即座に検出する。
寄稿フロー
訪問者からの音源寄稿を受け付けるフォームを実装。
- honeypotフィールドによるbot対策、ファイル形式・サイズ(95MB上限)の検証
- 音源とメタデータは非公開のR2 stagingバケットに保存し、D1の台帳で審査ステータスを管理
- 採用時はPythonパイプラインで整音してから本番バケットへ
その他
- 地図表示: MapLibre GLで音源の録音地点を地図上に表示
- 再生回数: 30秒以上の再生でセッション単位のビーコンを送信し、D1にUPSERTで集計(個人情報なし)
- SEO: JSON-LD(WebSite / AudioObject / BreadcrumbList)、動的OGP画像生成、
og:audioメタタグ
開発・運用
- CI: format / lint / typecheck / test(Vitest)/ build / terraform validateをPRごとに実行
- CD: mainへのpushで
terraform apply→D1マイグレーション→OpenNextビルド→wrangler deployまで自動化 - テスト: DSP(フェード曲線の音圧一定性)、ミックスURLシリアライズ、コンテンツローダなどロジック層に注力
