ChibihamChibiham
音栞(otoshiori)
🎐

音栞(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>要素では実現できないため

特徴的な実装

サンプル精度のループ再生エンジン

<audio loop>はエンコーダ遅延により末尾にギャップが生じるため、AudioBufferSourceNodeloopStart/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ツールを自作。

  1. 安定区間検出: librosaでRMSの移動分散とオンセット密度を解析し、突発音の少ない連続区間を自動選定
  2. ループ化: 区間末尾にequal-powerクロスフェードを適用し、末尾→先頭が無音で繋がるよう加工
  3. 整音: ハイパスフィルタ + EBU R128ラウドネス正規化(ループ境界の連続性を保つため線形ゲインのみ、圧縮・EQは不使用)
  4. エンコード: 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シリアライズ、コンテンツローダなどロジック層に注力