KANADE — 要件定義書 兼 Claude Code 実装指示書
版数 1.0(DBレス構成 / 記事埋め込み型デモ)/ 作成日 2026-08-17 / 発行 ソウゾウ合同会社 用途 SEO記事「【デモで試せる】Claude Codeで音楽アプリを開発する方法と費用|動画つき資料【2026年】」に組み込む実演デモ
このドキュメントの使い方(Claude Code へ)
これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に CLAUDE.md として置き、毎回読み込むこと。
- 実装は「13. 実装タスク」の順に進める。フェーズを飛ばさない。
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- 仕様がここに書かれていない場合は、推測で実装せず質問する。
- スコープ外(4章 Won't have)は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。
姉妹プロジェクトとの関係 HIREBASE(求人)/ RELATE(顧客管理)/ CASTA(動画配信)/ FIELDPIN(位置情報)に続く5本目。並べて「同じテンプレ」と思われた時点で失敗。 HIREBASE=明朝・白・余白(読ませる)/ RELATE=ゴシック・白・高密度(操作させる)/ CASTA=ダーク・無彩色・モーション抑制(見せる)/ FIELDPIN=地図全画面・大型UI(現場で使わせる)/ KANADE=アートワークの色に画面が染まる・モーションが音に同期する(浸らせる)。 **特に CASTA との差別化に注意。**同じ「ダーク基調のメディアアプリ」だが、CASTA は「色を持たない・動かさない」、KANADE は「色が毎回変わる・音に合わせて動く」。真逆に振る。
目次
- プロジェクト概要
- 権利の壁 — この記事の核心
- 記事への組み込み設計
- アーキテクチャ方針
- スコープ定義
- ロールとデモ切替
- 画面一覧とユーザーフロー
- 機能要件
- データ設計
- デザイン要件
- 技術要件・ディレクトリ構成
- 非機能要件
- 費用設計
- 実装タスク
- 受入基準
1. プロジェクト概要
背景と目的
「音楽アプリを作りたい」という相談の8割は、頭の中に Spotify / Apple Music がある。しかしそれは個人や中小企業には作れない。技術の問題ではなく、権利の問題として作れない(2章)。
一方で、実際に作れて、かつ需要があるものははっきりしている。
- 音楽教室・ボイストレーニングのレッスン音源配信
- ヨガ・瞑想・睡眠・作業用のBGM配信
- インディーレーベル・アーティストの自主配信プラットフォーム
- 社内向けの研修音声・ポッドキャスト
- 効果音・BGM素材の販売プラットフォーム
共通するのは、**「自分たちが権利を持つ音源を、会員に配信する」**という構造。KANADE はこの構造の実装デモとして作る。
| 目的 | 内容 |
|---|---|
| 実装力の証明 | ギャップレス再生・クロスフェード・キュー管理・オフライン再生・ビジュアライザーまで実装した本物のプレーヤーを見せる |
| 期待値の正常化 | 「Spotifyは作れない。作れるのはこれ」を、実物で示す。 これが最大の価値 |
| Webの実力の証明 | ロック画面での操作・バックグラウンド再生がWebでもできることを実演する |
| 費用説明の裏付け | 音源のライセンス費用という、最も見落とされるコストを説明する |
| リード獲得 | デモ内から記事・問い合わせへの導線を常設し、GA4で計測する |
プロダクト定義
| 項目 | 内容 |
|---|---|
| プロダクト名 | KANADE(奏)※仮称 |
| 一言定義 | 自社の音源を会員に配信する、サブスクリプション型の音楽配信アプリ |
| 想定業態 | 音楽教室、レーベル、ヨガ・瞑想スタジオ、作業用BGM、音声メディア、素材販売 |
| 提供形態 | レスポンシブWebアプリ(PWA)。フロントエンド完結(サーバー側の永続化なし)+ 静的音源配信 |
| 主戦場 | スマホ。画面を消したまま聴かれる。 バックグラウンド再生とロック画面操作が成立しないと使い物にならない |
| 想定利用者 | 記事の読者。登録なしで即座に再生でき、会員機能もロール切替で体験できる |
プロダクトコンセプト
「聴いている間、アプリは邪魔をしない。」
音楽アプリの大半の時間、ユーザーは画面を見ていない。だからKANADEは、画面を見ていないときに強い——ロック画面で操作でき、通信が切れても止まらず、次の曲が途切れずに始まる。そして画面を見たときには、その曲の色に染まっている。
デモとしての成功条件
- 記事の読者が、1タップで音を出せる
- 画面を消しても再生が続き、ロック画面で操作できる — Webでここまでできることを体験させる
- 曲間が途切れない — ギャップレス/クロスフェードが効いている
- 画面遷移しても再生が止まらない — ミニプレーヤーが常駐している
- 管理側も見られる — 音源のアップロード・配信管理・再生分析まで確認できる
- 記事に戻れる/問い合わせできる — デモが行き止まりにならない
2. 権利の壁 — この記事の核心
この章は、実装要件であると同時に、記事本文の中核原稿である。 曖昧にしない。
なぜ個人・中小企業に Spotify は作れないのか
音楽を配信するには、2つの別々の権利の許諾が必要になる。
| 権利 | 対象 | 管理・許諾の窓口 |
|---|---|---|
| 著作権(作詞・作曲) | メロディと歌詞そのもの | JASRAC・NexTone 等の管理団体。手続きは可能 |
| 著作隣接権(原盤権) | その「録音物」そのもの | レコード会社・原盤権者との個別交渉 |
問題は後者。 管理団体に申請すれば済む話ではなく、レコード会社1社ずつと個別に契約する必要がある。大手配信サービスがこれを実現できているのは、事業規模と交渉力があるから。
つまり「既存のヒット曲を配信するアプリ」は、技術ではなく事業交渉の問題であり、開発費の見積もり以前の話。 ここを最初に伝えないと、依頼者は数百万円を払った後に「曲が入れられない」という現実に直面する。
では何が作れるのか
| 音源の種類 | 実現可能性 | 備考 |
|---|---|---|
| 自社・自分たちが制作した音源 | ◎ | 最も現実的。教室のレッスン音源、自主制作、社内音声 |
| 所属アーティストの音源(レーベル) | ◎ | 契約で原盤権を持っていれば可能 |
| ロイヤリティフリー/CC0 の音源 | ○ | ライセンス条件の確認が必須。「フリー」の意味は提供元ごとに違う |
| サブスク型の音源ライブラリ(商用利用可) | ○ | 月額でライブラリを利用。配信可否は規約次第。要確認 |
| 既存の商用楽曲 | × | 原盤権の個別交渉が必要。実質不可能 |
| YouTube等からの抽出 | × | 論外。規約違反かつ著作権侵害 |
歌詞の扱い(見落とされやすい)
歌詞の表示にも、別途許諾が必要。 歌詞は著作物であり、アプリ内での表示は複製・公衆送信にあたる。歌詞表示サービスが管理団体と契約しているのはこのため。
自作の音源であっても、作詞者が別にいる場合は権利処理が要る。
本デモでの対応
| ID | 要件 |
|---|---|
| RGT-01 | 使用する音源は、ソウゾウが権利を持つもの、または商用利用・再配信が明示的に許諾されたもののみとする |
| RGT-02 | 音源ごとに、取得元・ライセンス種別・許諾範囲・確認日を docs/audio-license.md に記録すること。例外なく全曲 |
| RGT-03 | 「フリー素材」と書かれているだけの音源を使わない。ライセンス文書のURLと条項を確認し、記録すること |
| RGT-04 | 歌詞表示機能を実装する場合、歌詞データは自作の音源に対してのみ用意すること |
| RGT-05 | アーティスト名・アルバム名・ジャケットは、すべて架空のものを用意すること。実在のアーティストを想起させる名前を使わない |
| RGT-06 | ジャケット画像は、生成または商用利用可の素材のみ。既存アルバムのデザインを模倣しないこと |
| RGT-07 | デモ内に「本デモの音源は権利処理済みのものです」という注記を常設すること |
記事に載せる際の注意: 権利まわりの記述は、「一般的な構造の説明」に留め、個別案件の可否を断定しないこと。 実際の権利処理は個別の契約内容に依存する。記事には「弁護士・管理団体への確認を推奨する」旨を明記すること。
3. 記事への組み込み設計
音声固有の制約:勝手に音を出せない
ブラウザはユーザー操作なしの音声再生をブロックする(Autoplay Policy)。これは制約であると同時に、守るべきマナーでもある。
記事を読んでいる最中に、突然音が鳴るのは最悪の体験。 埋め込みは「押されるまで一切音を出さない」ことを絶対条件にする。
配置方針(2段構え)
| 段 | 配置 | 中身 | 音 |
|---|---|---|---|
| ① 記事内インライン | 記事の冒頭〜中盤、<iframe> |
**プレーヤー単体。**1曲、波形、再生コントロール | 押されるまで鳴らない |
| ② 全画面デモ | 「デモを開く」カード → 別タブ | アプリ全体(ライブラリ・キュー・オフライン・管理画面) | 同上 |
埋め込みモード(?embed=1)
| 通常モード | 埋め込みモード ?embed=1 |
|
|---|---|---|
| ヘッダー・サイドバー・ロール切替バー | 表示 | 非表示 |
| 表示内容 | アプリ全体 | 指定1曲のプレーヤー(アートワーク・波形・コントロール) |
| 自動再生 | しない | 絶対にしない |
| 音量の初期値 | 前回値 | 70%固定(大音量事故の防止) |
| Media Session API | 使う | 使わない(記事のタブがロック画面を奪わないように) |
| 遷移 | 通常 | アプリ内遷移を禁止。「全画面で開く」は target="_blank" |
<iframe
src="https://kanade.souzoh-demo.com/play/track-001?embed=1"
width="100%" style="aspect-ratio:16/7; border:0"
loading="lazy"
allow="autoplay"
title="KANADE 音楽プレーヤーのデモ"></iframe>
記事本体のパフォーマンスを壊さない
| ID | 要件 |
|---|---|
| EMB-01 | 記事内 iframe は loading="lazy" とし、ビューポート接近まで読み込まないこと |
| EMB-02 | 埋め込みの初期表示はアートワークと再生ボタンのみ。音声ファイルとオーディオ処理の初期化を再生ボタン押下まで遅延させること |
| EMB-03 | 埋め込みモードのJS初期バンドルは 60KB以下(gzip後)とすること |
| EMB-04 | 埋め込み iframe が記事の LCP 要素にならないこと |
| EMB-05 | 音声ありの自動再生を一切行わないこと。 AudioContext の生成も、ユーザー操作後に行うこと |
| EMB-06 | タブが非表示になったら、埋め込みプレーヤーの再生を一時停止すること(記事を離れたのに音が鳴り続けない) |
| EMB-07 | 波形描画は事前生成した静的データを使い、リアルタイム解析を埋め込みモードで行わないこと(CPU負荷対策) |
「動画つき資料」との一体化
記事タイトルの「動画つき資料」を、別途用意せずデモの体験から作る。
- 動画:再生 → 画面を消してロック画面から操作 → 通信を切っても再生継続 → 管理画面で分析、までを3分で収録。「Webでもここまでできる」が一目で伝わる
- 資料:本要件定義から抜粋した「音源ライセンスのチェックリスト」と「配信コスト計算シート」をPDF化
- デモ内の「資料をダウンロード」からも取得でき、そこを問い合わせ導線に接続する
導線と計測
| ID | 要件 |
|---|---|
| EMB-10 | 全画面デモの上部に、元の記事に戻るリンクを常設すること |
| EMB-11 | 問い合わせ導線を、資料ダウンロード後・ペイウォール内・管理画面に配置すること。再生中のプレーヤー画面には出さない |
| EMB-12 | GA4 で以下を計測すること:demo_open / track_play / track_complete / bg_play(バックグラウンド再生の発生)/offline_play / paywall_view / role_switch / doc_download / form_click / line_click |
| EMB-13 | form_click と line_click は既存メディアのキーイベント名と揃えること |
| EMB-14 | bg_play は特に重要。 「Webでバックグラウンド再生が実際に使われた」実績は、記事の主張を裏付けるデータになる |
SEO上の扱い
| ID | 要件 |
|---|---|
| EMB-20 | デモは記事とは別サブドメインで配信し、固有の title / description を持つこと |
| EMB-21 | ?embed=1 のURLは noindex とすること |
| EMB-22 | デモの各ページから記事へリンクし、記事側からも nofollow を付けずリンクすること |
| EMB-23 | 記事側に VideoObject(解説動画)と HowTo(開発手順)の構造化データを設置すること |
| EMB-24 | MusicRecording 等の音楽系構造化データは使わないこと。 架空の楽曲を実在の音源として検索エンジンに登録させない |
4. アーキテクチャ方針
基本方針
バックエンドとデータベースを持たない。ただし音源だけは実在させる。
| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL / Supabase | シードデータ(TypeScript)+ ブラウザ内ストア |
| 認証・会員管理 | デモ用ロール切替(ワンクリック) |
| 決済(Stripe) | プラン選択〜完了画面まで再現。課金処理なし |
| 音源のアップロード・変換 | UIと変換進捗を再現。実ファイルは事前に用意した素材 |
| DRM・署名付きURL | 実装しない。ペイウォールはUI上のゲートとして再現 |
| 音源の配信 | これだけは本物。 静的オブジェクトストレージ + CDN から配信する |
音源だけは実在させる理由
音が鳴らない音楽アプリのデモには、意味がない。再生ボタンを押して音が出るかどうかが、このデモの成否そのもの。
ただし実在させるのは配信であって、バックエンドではない。音声ファイルは静的ファイルなので、オブジェクトストレージに置くだけで配信できる。
音源素材の方針
| ID | 要件 |
|---|---|
| AUD-01 | 2章 RGT-01〜07 の権利要件を必ず満たすこと |
| AUD-02 | 曲数は 24〜36曲。4〜6アルバム/3〜5アーティスト(すべて架空)に分散させること |
| AUD-03 | 1曲あたり 1分30秒〜4分。合計 60〜90分に収める |
| AUD-04 | ジャンルとテンポに幅を持たせること。 全曲が似た曲調だと、レコメンドもプレイリストも意味を持って見えない |
| AUD-05 | 各曲を 2つのビットレート(高音質 256kbps / 標準 128kbps)で用意し、音質切替を実演できるようにすること |
| AUD-06 | ギャップレス再生を実演できるよう、切れ目なく繋がる2曲を1組以上用意すること |
| AUD-07 | 各曲の波形データを事前生成し、JSONとして保持すること(EMB-07・描画コスト対策) |
| AUD-08 | ジャケット画像は正方形 1000×1000。画像から抽出した代表色をシードに含めること(10章のダイナミック配色に使用) |
エンコード仕様
入力: WAV / 高品質音源
出力:
├ 高音質 AAC 256kbps (.m4a) ─┐
└ 標準 AAC 128kbps (.m4a) ─┘ どちらも単一ファイル(HLSにしない)
ラウドネス: -14 LUFS に正規化(★曲ごとの音量差をなくす)
波形: 512サンプルにダウンサンプリングしてJSON化
ジャケット: 1000px / 300px / 80px の3サイズ + 代表色5色を抽出
エンコードとメタデータ生成は scripts/prepare-audio.sh(ffmpeg + ffprobe)としてリポジトリに含める。これ自体が記事の「方法」パートの説明材料になる。
ラウドネス正規化を必ず行うこと。 曲ごとに音量が違うアプリは、それだけで素人臭くなる。
配信先
| 候補 | 判断 |
|---|---|
| Cloudflare R2 + Cloudflare CDN | 採用。 エグレス課金がないため、リピート再生が増えてもコストが跳ねない |
| Vercel から静的配信 | 不採用。転送量課金。音楽はリピート率が高く、動画より再生回数が伸びる |
| 音声専用サービス | 不採用(本デモでは)。13章で比較対象として扱う |
Range リクエスト(206 Partial Content)への対応が必須。 シークができなくなる。
単一オーディオ要素の原則(最重要の設計判断)
アプリ全体で <audio> 要素を1つだけ生成し、レイアウトに常駐させる。
app/layout.tsx
└ <PlayerProvider> ← ここに <audio> が1つだけ存在する
├ <AudioEngine /> ← Web Audio のノードグラフもここ
├ {children} ← 画面はここが差し替わるだけ
└ <MiniPlayer /> ← 常駐。画面遷移しても消えない
- 画面遷移で再生が途切れないことが音楽アプリの最低条件(FR-108)
- ページごとに
<audio>を作ると、遷移のたびに音が止まり、デモとして破綻する - Web Audio のノードグラフ(GainNode / AnalyserNode)も同様に一度だけ構築する
FIELDPIN の「地図インスタンスは1つ」、CASTA の「hls.js の破棄」と同じ位置づけの構造要件。
永続化の範囲
| 対象 | 挙動 |
|---|---|
| シードデータ(楽曲・アルバム・アーティスト) | 初期投入。管理画面から編集可能 |
| 再生履歴・再生回数・お気に入り・プレイリスト | localStorage に保存。リロードしても残る |
| 再生位置・キュー・再生モード・音量 | localStorage に保存。リロードで前回の続きから再開できる |
| ダウンロード済み楽曲 | Cache API に実ファイルを保存(オフライン再生を本物にする) |
| リセット | 「デモをリセット」で localStorage と Cache をクリア |
擬似ディレイ は 150〜400ms。ただし再生・一時停止・シーク・曲送りにはディレイを入れない(音楽アプリで操作に遅延があるのは致命的)。
5. スコープ定義
リスナー向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-L01 | 再生プレーヤー | 再生/停止/シーク/音量/曲送り戻し/リピート/シャッフル | Must |
| F-L02 | ミニプレーヤー | 画面下部に常駐。画面遷移しても再生が途切れない。タップで全画面へ | Must |
| F-L03 | 全画面プレーヤー | アートワーク大表示、波形シークバー、キュー、歌詞 | Must |
| F-L04 | キュー管理 | 次に再生される曲の一覧、ドラッグで並び替え、個別削除、「次に再生」追加 | Must |
| F-L05 | ギャップレス再生/クロスフェード | 曲間を途切れさせない。クロスフェード秒数を設定可 | Must |
| F-L06 | バックグラウンド再生 | 画面を消しても再生継続 | Must |
| F-L07 | ロック画面・通知領域での操作 | Media Session API。曲名・アートワーク表示、再生/停止/曲送り | Must |
| F-L08 | ライブラリ | アルバム/アーティスト/楽曲/プレイリストのタブ | Must |
| F-L09 | プレイリスト | 作成、曲の追加・削除・並び替え、カバー自動生成 | Must |
| F-L10 | 検索 | 楽曲・アルバム・アーティスト・プレイリストの横断検索 | Must |
| F-L11 | お気に入り | 楽曲・アルバムのお気に入り登録 | Must |
| F-L12 | 再生履歴 | 最近再生した曲、再生回数 | Should |
| F-L13 | オフライン再生(ダウンロード) | 楽曲・アルバムを端末に保存し、圏外でも再生できる | Must |
| F-L14 | 音質切替 | 高音質/標準/自動(通信状況に応じて) | Should |
| F-L15 | ビジュアライザー | 再生中の音を解析して波形/スペクトラムを描画 | Should |
| F-L16 | 歌詞表示 | 時間同期歌詞(LRC相当)。現在行のハイライトとタップでのジャンプ | Should |
| F-L17 | スリープタイマー | 指定時間後にフェードアウトして停止 | Should |
| F-L18 | イコライザー | プリセット(フラット/低音強調/ボーカル等)+ 手動調整 | Could |
| F-L19 | ペイウォール | 有料楽曲はプレビュー(既定30秒)後にゲート表示 | Must |
| F-L20 | プラン・契約 | プラン比較、選択、フォーム、完了(課金処理なし) | Must |
| F-L21 | 再生設定 | クロスフェード秒数、音質、自動再生、ラウドネス正規化のON/OFF | Should |
配信管理(運営)向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-A01 | 管理ダッシュボード | 本日の再生数、人気曲、新着、会員数推移 | Must |
| F-A02 | 音源アップロード | ドロップ、変換進捗の再現(解析→変換→波形生成→完了) | Must |
| F-A03 | 楽曲編集 | タイトル、アーティスト、アルバム、ジャンル、公開設定、無料/有料 | Must |
| F-A04 | ライセンス管理 | 楽曲ごとに、取得元・ライセンス種別・許諾範囲・確認日を記録。未記入の楽曲は公開できない | Must |
| F-A05 | アルバム管理 | アルバム作成、トラック順の並び替え、ジャケット設定 | Must |
| F-A06 | アーティスト管理 | プロフィール、画像、関連アルバム | Should |
| F-A07 | 歌詞編集 | 歌詞の入力、再生しながらのタイムスタンプ打ち込み | Should |
| F-A08 | プレイリスト(公式) | 運営が作る公式プレイリスト、公開設定 | Should |
| F-A09 | 会員・プラン管理 | 会員一覧、プランCRUD | Should |
| F-A10 | 再生分析 | 再生数推移、完聴率・スキップ率、曲別ランキング、時間帯別 | Must |
| F-A11 | 一括操作 | 複数楽曲の公開/非公開、ジャンル付与 | Should |
共通・基盤
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | ロール切替 | 未ログイン/無料会員/有料会員/運営 をワンクリック切替 | Must |
| F-S02 | 埋め込みモード | ?embed=1 でプレーヤー単体表示(3章) |
Must |
| F-S03 | 資料ダウンロード | ライセンスチェックリスト・費用計算シートのPDF | Must |
| F-S04 | 記事への導線 | 元記事へのリンク、問い合わせ導線 | Must |
| F-S05 | デモリセット | localStorage と Cache をクリア | Must |
| F-S06 | PWA | ホーム画面追加、オフライン起動 | Must |
| F-S07 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |
| F-S08 | キーボードショートカット | Space再生停止、←→シーク、↑↓音量、N/P曲送り | Should |
対象外(Won't have)
| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 4章の方針に基づく |
| 本物の認証・実際の課金処理 | ロール切替と完了画面で代替 |
| 既存の商用楽曲の配信 | 2章。権利上、実現不可能 |
| DRM・署名付きURL | デモでは実装しない。13章で必要性とコストに触れる |
| 楽曲の推薦アルゴリズム(機械学習) | ジャンル・テンポの一致によるルールベースで代替 |
| ソーシャル機能(フォロー・共有プレイリスト) | 対象外 |
| ポッドキャスト特有機能(章立て・購読) | 対象外 |
| 音楽の編集・DAW的機能 | 対象外 |
| 実際のアップロード音源の変換処理 | 進捗UIの再現に留める |
| ネイティブアプリ・車載連携(CarPlay等) | PWAで対応。必要なケースは13章で明示 |
スコープリスク: 音楽アプリは「レコメンドAIも」「歌詞も全曲」「SNS連携も」と広がりやすい。特に推薦アルゴリズムは精度の合意形成に時間がかかる。 ジャンル・テンポ・お気に入りの一致という説明可能なルールで実装し、「なぜこの曲が推薦されたか」を画面上に表示することで、体験としての納得感を作る。
6. ロールとデモ切替
ロール定義
| ロールID | 名称 | 聴ける範囲 | 特徴 |
|---|---|---|---|
guest |
未ログイン | 無料楽曲のみ。有料は30秒プレビュー | 会員登録の導線を体験 |
free |
無料会員 | 無料楽曲全編 + プレイリスト・お気に入り | 有料楽曲でペイウォール。ダウンロード不可 |
premium |
有料会員 | 全楽曲 + 高音質 + オフラインダウンロード | 制限なし |
admin |
運営 | 全楽曲 + 管理画面 | 配信管理・ライセンス管理・分析 |
ロールを跨ぐ体験(必ず動くようにする)
- 未ログインで有料曲を再生 → 30秒でペイウォール → 有料会員に切替 → 同じ位置から続きが再生される
- 再生したまま画面遷移(ライブラリ→検索→プレイリスト)→ 一度も音が途切れない
- 再生中に画面を消す → ロック画面に曲名とアートワークが出て、そこから曲送りできる
- 有料会員でアルバムをダウンロード → 機内モードにする → オフラインで再生できる
- 運営で楽曲を非公開に → リスナーに切替 → ライブラリから消えている
- 自分が再生した曲が、運営の再生分析にカウントされ、完聴率に反映される
デモ切替バー(F-S01)
画面上部に常時表示する固定バー。埋め込みモードでは非表示。
- 現在のロール表示と4ロールの切替
- 「デモをリセット」ボタン(確認ダイアログ付き)
- 元記事へ戻るリンク
- 「本デモの音源は権利処理済みのものです」の注記(RGT-07)
- 「これはデモです」の明示
デザイン上の扱い: アプリ本体がアートワークの色に染まるため、切替バーはその影響を受けない固定の濃色帯とする。プロダクトのダイナミック配色に巻き込まない。
7. 画面一覧とユーザーフロー
全28画面。画面IDはディレクトリ構成と 1:1 で対応させる。
リスナー画面
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | ホーム | / |
続きから聴く、最近再生、おすすめ、新着アルバム、公式プレイリスト |
| SC-002 | 全画面プレーヤー | /player(オーバーレイ) |
大アートワーク、波形シークバー、コントロール、キュー、歌詞タブ |
| SC-003 | 埋め込みプレーヤー | /play/[trackId]?embed=1 |
アートワーク・波形・コントロールのみ |
| SC-010 | ライブラリ | /library |
アルバム/アーティスト/楽曲/プレイリストのタブ |
| SC-011 | アルバム詳細 | /albums/[id] |
ジャケット、トラック一覧、全曲再生、ダウンロード |
| SC-012 | アーティスト詳細 | /artists/[id] |
プロフィール、人気曲、アルバム一覧 |
| SC-013 | プレイリスト詳細 | /playlists/[id] |
カバー、曲一覧、並び替え、共有 |
| SC-014 | プレイリスト作成・編集 | /playlists/new |
名称、説明、カバー自動生成、曲追加 |
| SC-015 | お気に入り | /library/favorites |
お気に入り楽曲・アルバム |
| SC-020 | 検索 | /search |
横断検索、タブ切替、最近の検索 |
| SC-021 | ジャンル一覧 | /genres/[slug] |
ジャンル別の楽曲・アルバム |
| SC-030 | 再生履歴 | /history |
時系列、再生回数 |
| SC-031 | ダウンロード管理 | /downloads |
ダウンロード済み一覧、容量、削除、一括削除 |
| SC-032 | 再生設定 | /settings/playback |
クロスフェード、音質、自動再生、ラウドネス正規化、イコライザー |
| SC-033 | スリープタイマー | モーダル | 15/30/45/60分、曲の終わりまで |
| SC-040 | プラン比較 | /plans |
3プランの比較表、FAQ |
| SC-041 | 申込フォーム | /plans/[planId]/checkout |
会員情報、支払い方法(入力UIのみ)、確認 |
| SC-042 | 申込完了 | /plans/complete |
完了、再生への導線 |
| SC-043 | 契約状況 | /settings/subscription |
現在のプラン、変更、解約 |
管理画面
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 管理ダッシュボード | /admin |
本日の再生数、人気曲、公開待ち、会員数推移 |
| SC-110 | 楽曲一覧 | /admin/tracks |
ステータス別タブ、ライセンス未記入の警告、一括操作 |
| SC-111 | 音源アップロード | /admin/tracks/upload |
ドロップ、変換進捗の再現、失敗ケース |
| SC-112 | 楽曲編集 | /admin/tracks/[id] |
メタデータ、公開設定、無料/有料、波形プレビュー |
| SC-113 | ライセンス管理 | /admin/tracks/[id]/license |
取得元、ライセンス種別、許諾範囲、確認日、証跡URL |
| SC-114 | 歌詞編集 | /admin/tracks/[id]/lyrics |
歌詞入力、再生しながらのタイムスタンプ打ち込み |
| SC-120 | アルバム管理 | /admin/albums |
アルバムCRUD、トラック順の並び替え、ジャケット |
| SC-121 | アーティスト管理 | /admin/artists |
プロフィール、画像 |
| SC-130 | 公式プレイリスト | /admin/playlists |
作成、曲追加、公開設定 |
| SC-140 | 会員・プラン管理 | /admin/members |
会員一覧、プランCRUD |
| SC-150 | 再生分析 | /admin/analytics |
再生数推移、完聴率・スキップ率、曲別ランキング、時間帯別、デバイス別 |
主要フロー
フローA:記事の読者が音に触れる(最重要)
SEO記事を読んでいる
↓
記事内の埋め込みプレーヤー(アートワーク + 再生ボタン。まだ何も読み込んでいない・音も出ない)
↓
再生ボタンを押す ── ここで初めて AudioContext を生成し、音源を取得
↓
再生開始(2秒以内)── 波形が動き、シークできる
↓
「全画面でデモを開く」→ 別タブでアプリ本体へ
↓
SC-001 ホーム ── ガイドツアーが3ステップで「試せること」を案内
↓
アルバムを開いて再生 → ★ 画面を遷移しても音が途切れない(ミニプレーヤー常駐)
↓
★ 画面を消す → ロック画面に曲名とアートワークが出る → そこから曲送りできる
↓
デモ切替バーで「運営」に切替 → SC-150 再生分析
↓ ★ たった今の再生が、完聴率のグラフに反映されている
資料ダウンロード or 元記事に戻る
フローB:ペイウォールと高音質・ダウンロード
SC-001 ホーム(未ログイン)── 有料バッジ付きの曲を選ぶ
↓
再生開始。プレーヤー上部に「無料プレビュー 残り0:23」
↓
30秒経過 ── フェードアウトして停止、ペイウォールがオーバーレイ表示
↓
SC-040 プラン比較 → SC-041 申込 → SC-042 完了
↓ ※ 決済処理は行わない。ロールが premium に変わる
★ 停止した0:30の位置から、そのまま続きが再生される
↓
SC-011 アルバム詳細 ──「ダウンロード」が有効になっている
↓
ダウンロード実行 ── ★ Cache API に実ファイルを保存(進捗表示)
↓
機内モードにする
↓
★ ダウンロード済みの曲が、オフラインで再生できる
未ダウンロードの曲は「オフラインでは再生できません」と明示
フローC:運営が音源を公開する(ライセンス管理が肝)
SC-111 音源アップロード ── ファイルをドロップ
↓
変換進捗の再現
解析中 → 変換中(256k / 128k)→ 波形生成 → ジャケット色抽出 → 完了
※ 一定確率で「変換失敗」を再現し、リトライ導線を見せる
↓
SC-112 楽曲編集 ── タイトル、アーティスト、アルバム、ジャンル、無料/有料
↓
公開しようとする
↓
★ ブロックされる:「ライセンス情報が未記入のため公開できません」
↓
SC-113 ライセンス管理
取得元/ライセンス種別/許諾範囲(配信可・改変不可 等)/確認日/証跡URL
↓
記入完了 → 公開可能になる
↓
SC-114 歌詞編集 ── 再生しながらタイムスタンプを打つ
↓
公開
↓
【ロール切替:無料会員】
SC-001 ホームの「新着」に出現
8. 機能要件
8.1 プレーヤー(このデモの心臓部)
| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | 再生/一時停止/シーク/音量/ミュート/次の曲/前の曲 を操作できること。前の曲は、再生3秒以降なら曲頭に戻ること | P1 |
| FR-102 | AudioContext はユーザー操作を起点にのみ生成すること。 ページ読み込み時に生成しないこと |
P1 |
| FR-103 | リピート(オフ/全曲/1曲)とシャッフルを切り替えられること。設定は保持されること | P1 |
| FR-104 | 波形シークバーを表示すること。事前生成した波形データを使い、再生済み部分を塗り分けること | P1 |
| FR-105 | ギャップレス再生:次の曲を事前にバッファし、曲間に無音を作らないこと | P1 |
| FR-106 | クロスフェード:秒数(0/2/4/6秒)を設定でき、GainNode で音量を交差させること。0秒時はギャップレス動作とすること | P1 |
| FR-107 | ラウドネス正規化により、曲ごとの音量差が感じられないこと。設定でOFFにできること | P1 |
| FR-108 | 画面遷移で再生が途切れないこと。 <audio> はアプリ全体で1つだけ、レイアウトに常駐させること(4章) |
P1 |
| FR-109 | ミニプレーヤーを画面下部に常駐させ、タップで全画面プレーヤーへ遷移できること。全画面から戻っても再生は継続すること | P1 |
| FR-110 | 再生位置・キュー・再生モード・音量を保存し、次回アクセス時に前回の続きから再開できること | P1 |
| FR-111 | 再生に失敗した場合、原因(ネットワーク/非対応形式/読み込み失敗)を区別したメッセージと再試行ボタンを表示すること。無言で止まらないこと | P1 |
| FR-112 | 音質を「高音質/標準/自動」から選択できること。切替時に再生位置と再生状態を維持すること | P2 |
| FR-113 | スリープタイマー(15/30/45/60分、曲の終わりまで)。終了時は即時停止せず、5秒かけてフェードアウトすること | P2 |
| FR-114 | イコライザーのプリセットと手動調整(BiquadFilterNode)を提供すること | P3 |
| FR-115 | キーボードショートカット:Space 再生停止、←→ 5秒シーク、↑↓ 音量、N/P 曲送り戻し、L お気に入り、? 一覧 |
P2 |
8.2 バックグラウンド再生とロック画面(Webの実力を示す部分)
| ID | 要件 | 優先 |
|---|---|---|
| FR-151 | 画面を消しても、タブが非表示になっても再生が継続すること | P1 |
| FR-152 | Media Session API でメタデータ(曲名・アーティスト・アルバム・アートワーク)を設定し、ロック画面と通知領域に表示させること | P1 |
| FR-153 | ロック画面/通知領域から、再生・一時停止・次の曲・前の曲 を操作できること | P1 |
| FR-154 | シーク操作(seekto / seekbackward / seekforward)に対応すること |
P2 |
| FR-155 | positionState を更新し、ロック画面の再生位置表示を正しく保つこと |
P2 |
| FR-156 | 曲が変わるたびにメタデータを更新すること。前の曲の情報が残らないこと | P1 |
| FR-157 | 他のアプリが音声を再生した場合(オーディオフォーカス喪失)、適切に一時停止すること | P2 |
| FR-158 | イヤホンの抜去を検知して一時停止すること(対応環境のみ) | P3 |
| FR-159 | 埋め込みモードでは Media Session API を使わないこと(記事のタブがロック画面を奪わない)(EMB) | P1 |
8.3 キューとプレイリスト
| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | 現在のキュー(次に再生される曲)を一覧表示し、ドラッグで並び替え、個別削除ができること | P1 |
| FR-202 | 曲を「次に再生」または「キューの最後に追加」できること | P1 |
| FR-203 | アルバム・プレイリストの再生時、キューがその内容で置き換わること。警告なく既存キューを消さない(確認するか、追加を選ばせる) | P1 |
| FR-204 | シャッフル時、現在再生中の曲を先頭に固定し、以降をランダム化すること。シャッフル解除で元の順序に戻ること | P1 |
| FR-205 | プレイリストを作成・編集・削除できること。曲の追加・削除・ドラッグ並び替えができること | P1 |
| FR-206 | プレイリストのカバーを、含まれる楽曲のジャケット4枚から自動生成すること | P2 |
| FR-207 | 楽曲・アルバムをお気に入り登録でき、ライブラリから確認できること | P1 |
| FR-208 | 再生履歴を記録し、時系列と再生回数で確認できること | P2 |
| FR-209 | ホームの「続きから聴く」に、途中で止めた曲・アルバムを表示すること | P1 |
8.4 ライブラリ・検索・レコメンド
| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | ライブラリをアルバム/アーティスト/楽曲/プレイリストのタブで表示すること | P1 |
| FR-302 | 検索は楽曲名・アーティスト名・アルバム名・プレイリスト名を対象とし、表記ゆれ(ひらがな/カタカナ/半角全角)を吸収すること | P1 |
| FR-303 | 検索結果を種別ごとにグループ化し、タブで絞り込めること | P1 |
| FR-304 | 絞り込み条件をURLクエリに反映し、リロード・共有で再現できること | P1 |
| FR-305 | おすすめは、ジャンル・テンポ・お気に入りの一致というルールベースで算出すること | P2 |
| FR-306 | 「なぜこの曲が推薦されたか」を1行で表示すること(「よく聴くジャンルから」「お気に入りのアーティスト」等) | P2 |
| FR-307 | 楽曲一覧の行から、右クリック/長押しでコンテキストメニュー(次に再生/プレイリストに追加/お気に入り/アルバムへ移動)を開けること | P2 |
8.5 オフライン再生
| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 有料会員のみ、楽曲・アルバムをダウンロードできること | P1 |
| FR-402 | Cache API に実際の音声ファイルを保存すること。 フラグだけの偽装をしないこと | P1 |
| FR-403 | ダウンロードの進捗を曲単位で表示し、中断・再開ができること | P2 |
| FR-404 | オフライン時、ダウンロード済みの曲が実際に再生できること | P1 |
| FR-405 | オフライン時、未ダウンロードの曲は再生不可であることを明示し、ダウンロード済みへの絞り込み導線を出すこと | P1 |
| FR-406 | SC-031 でダウンロード済み一覧・使用容量・個別削除・一括削除ができること | P1 |
| FR-407 | ストレージ容量の上限接近時に警告すること | P2 |
| FR-408 | オフライン状態を画面上部のバナーで常時明示すること | P1 |
| FR-409 | PWA としてホーム画面に追加でき、オフラインでも起動できること | P1 |
8.6 ビジュアライザー・歌詞
| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | AnalyserNode を使い、再生中の音を解析してビジュアライザーを描画すること(スペクトラム/波形の2種) | P2 |
| FR-502 | ビジュアライザーはCanvasで描画し、**60fpsを維持すること。**維持できない環境では自動的に簡易表示へ落とすこと | P2 |
| FR-503 | ビジュアライザーをOFFにできること。prefers-reduced-motion 指定時は既定でOFFとすること |
P1 |
| FR-504 | 埋め込みモードではリアルタイム解析を行わず、事前生成した波形の再生位置ハイライトのみとすること(EMB-07) | P1 |
| FR-505 | 時間同期歌詞を表示し、現在行をハイライトして自動スクロールすること | P2 |
| FR-506 | 歌詞の行をタップすると、その時間へジャンプすること | P2 |
| FR-507 | 歌詞がない楽曲では、歌詞タブを表示しないこと(空タブを出さない) | P1 |
8.7 会員・ペイウォール
| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | 楽曲は「無料/有料」の区分を持ち、一覧でバッジ表示すること | P1 |
| FR-602 | 未ログイン・無料会員が有料楽曲を再生した場合、既定30秒のプレビュー後にフェードアウトして停止し、ペイウォールを表示すること | P1 |
| FR-603 | プレビュー中、残り時間をプレーヤー上部に常時表示すること | P1 |
| FR-604 | ペイウォールには、プラン概要・解放される内容・CTA を含めること。閉じて他の曲を探す導線も置くこと | P1 |
| FR-605 | プラン加入後、プレビューが停止した位置から続きを再生できること | P1 |
| FR-606 | プラン比較で3プラン(無料/スタンダード/プレミアム)の違い(楽曲範囲・音質・ダウンロード可否)を表で示すこと | P1 |
| FR-607 | 申込フォームの入力検証は実装し、送信時に課金処理を行わないこと。デモである旨を明示すること | P1 |
| FR-608 | 申込完了でロールが premium に変わり、切替バーの表示も更新されること |
P1 |
| FR-609 | 解約すると視聴範囲が制限され、ダウンロード済み楽曲も再生できなくなること(実サービスの挙動に合わせる) | P2 |
8.8 配信管理
| ID | 要件 | 優先 |
|---|---|---|
| FR-701 | 音源ファイルをドロップでアップロードできること。対応形式・最大サイズを事前に明示すること | P1 |
| FR-702 | 変換進捗を段階表示で再現すること:解析中 → 変換中(256k/128k)→ 波形生成 → 色抽出 → 完了 | P1 |
| FR-703 | 変換完了後の再生は、事前に用意した素材で代替すること。デモである旨を管理画面上に明示すること | P1 |
| FR-704 | 一定確率で「変換失敗」を再現し、原因表示とリトライ導線を見せること | P2 |
| FR-705 | 楽曲編集で、タイトル・アーティスト・アルバム・ジャンル・トラック番号・無料/有料 を設定できること | P1 |
| FR-706 | ライセンス情報(取得元・種別・許諾範囲・確認日・証跡URL)が未記入の楽曲は、公開できないこと | P1 |
| FR-707 | 楽曲一覧で、ライセンス未記入の楽曲を警告表示し、絞り込めること | P1 |
| FR-708 | 公開設定として「下書き/公開/非公開」を選べること。非公開でリスナー側から即座に消えること | P1 |
| FR-709 | 歌詞を入力でき、再生しながら「現在位置でタイムスタンプを打つ」ことができること | P2 |
| FR-710 | アルバムを作成し、トラック順をドラッグで並び替えられること。キーボードでも並び替えられること | P1 |
| FR-711 | アーティストのプロフィール・画像を管理できること | P2 |
| FR-712 | 運営が公式プレイリストを作成・公開できること | P2 |
| FR-713 | 複数楽曲を選択して、公開/非公開・ジャンル付与を一括実行できること | P2 |
8.9 再生分析
| ID | 要件 | 優先 |
|---|---|---|
| FR-801 | 再生数・総再生時間・ユニークリスナー数を、期間指定(今日/7日/30日/任意)で表示すること | P1 |
| FR-802 | 完聴率とスキップ率を曲別に表示すること。スキップが発生した平均秒数も示すこと | P1 |
| FR-803 | 曲別・アルバム別・アーティスト別のランキングを表示すること | P1 |
| FR-804 | 時間帯別の再生数を表示すること(音楽アプリでは「いつ聴かれるか」が重要) | P2 |
| FR-805 | デバイス別・音質別の再生比率を表示すること | P2 |
| FR-806 | デモ中の再生が、これらの分析に反映されること | P1 |
| FR-807 | 分析データをCSVでエクスポートできること | P3 |
8.10 デモ基盤
| ID | 要件 | 優先 |
|---|---|---|
| FR-901 | 4ロールをワンクリックで切り替えられること | P1 |
| FR-902 | 「デモをリセット」で localStorage と Cache API の内容をクリアすること | P1 |
| FR-903 | 再生位置・キュー・お気に入り・プレイリスト・契約状態がリロード後も保持されること | P1 |
| FR-904 | 元記事へ戻るリンクと資料ダウンロードを常設すること | P1 |
| FR-905 | 「本デモの音源は権利処理済みのものです」の注記を常設すること(RGT-07) | P1 |
| FR-906 | ロール権限外の画面にアクセスした場合、案内画面から切り替えられること。素の404を出さないこと | P1 |
| FR-907 | 権限で不可の操作(無料会員のダウンロード等)はボタンを非活性にし、理由をツールチップで示すこと | P1 |
| FR-908 | データスコープと再生可否の判定は lib/repo/_scope.ts に集約すること |
P1 |
| FR-909 | 初回訪問時に3ステップのガイドツアーを表示し、「今後表示しない」を選べること | P2 |
9. データ設計
型定義(lib/types/)
// ---- コンテンツ ----
type Artist = {
id: string
name: string // ★ すべて架空(RGT-05)
nameKana: string
imageUrl: string
bio: string
albumIds: string[]
}
type Album = {
id: string
title: string
artistId: string
releaseDate: string
coverUrl: string // 1000px
coverUrlSm: string // 300px
coverUrlXs: string // 80px
palette: string[] // ★ ジャケットから抽出した代表色5色(10章のダイナミック配色)
trackIds: string[] // 並び順を保持
genreIds: string[]
isPublished: boolean
}
type Track = {
id: string
title: string
albumId: string
artistId: string
trackNumber: number
durationSec: number
bpm: number // レコメンドのテンポ一致に使用
genreIds: string[]
// --- 配信情報(実在するファイルを指す) ---
audioUrlHigh: string // AAC 256kbps
audioUrlStandard: string // AAC 128kbps
waveform: number[] // ★ 512点。事前生成(AUD-07)
loudnessLufs: number // 正規化用
// --- ギャップレス ---
gaplessNextTrackId?: string // ★ 切れ目なく繋がる次曲(AUD-06)
// --- 公開・課金 ---
access: 'free' | 'paid'
previewSec: number // 有料時のプレビュー秒数(既定30)
status: 'draft' | 'published' | 'private'
// --- 権利(★ 未記入では公開できない。FR-706) ---
license: TrackLicense | null
lyrics?: LyricLine[]
playCount: number
createdAt: string
}
type TrackLicense = {
source: string // 取得元(自社制作 / 提供元名)
licenseType: string // CC0 / CC BY / 商用ライセンス / 自社原盤 等
scope: string // 許諾範囲(配信可・改変不可 等)
evidenceUrl: string // ライセンス文書のURL(RGT-03)
confirmedAt: string // 確認日
confirmedBy: string
note: string
}
type LyricLine = { atSec: number; text: string }
type Genre = { id: string; name: string; slug: string }
// ---- 会員・プラン ----
type Plan = {
id: string
name: string // 無料 / スタンダード / プレミアム
priceMonthly: number
accessScope: 'free_only' | 'all'
maxQuality: 'standard' | 'high'
canDownload: boolean // ★ プランごとの機能差
features: string[]
sortOrder: number
}
type Member = {
id: string
name: string
email: string
avatarUrl: string
role: 'guest' | 'free' | 'premium' | 'admin'
planId: string
status: 'active' | 'cancelled'
createdAt: string
}
// ---- 再生状態(★ 永続化される中核) ----
type PlaybackState = {
currentTrackId?: string
positionSec: number
isPlaying: boolean // 復元時は false にする
queue: QueueItem[]
queueIndex: number
repeat: 'off' | 'all' | 'one'
shuffle: boolean
shuffleOrder?: number[] // 元順序に戻すため保持(FR-204)
volume: number // 0-1
quality: 'high' | 'standard' | 'auto'
crossfadeSec: 0 | 2 | 4 | 6
loudnessNormalization: boolean
sourceContext?: { type: 'album' | 'playlist' | 'artist' | 'search'; id: string }
}
type QueueItem = { id: string; trackId: string; addedBy: 'context' | 'manual' }
// ---- ユーザーデータ ----
type Playlist = {
id: string
ownerId: string
name: string
description: string
coverUrl?: string // 未設定ならジャケット4枚から自動生成(FR-206)
trackIds: string[]
isOfficial: boolean // 運営作成
isPublished: boolean
createdAt: string
}
type Favorite = { id: string; memberId: string; targetType: 'track' | 'album'; targetId: string; addedAt: string }
type PlayHistory = { id: string; memberId: string; trackId: string; playedAt: string; completedRatio: number }
// ---- オフライン ----
type DownloadItem = {
id: string
trackId: string
memberId: string
quality: 'high' | 'standard'
sizeBytes: number
state: 'queued' | 'downloading' | 'done' | 'failed'
progress: number // 0-100
cacheKey: string // ★ Cache API のキー(FR-402)
downloadedAt?: string
}
// ---- 再生イベント(分析の元データ) ----
type PlayEvent = {
id: string
trackId: string
memberId: string
type: 'start' | 'progress' | 'complete' | 'skip' | 'pause' | 'seek'
positionSec: number
quality: 'high' | 'standard'
device: 'desktop' | 'mobile' | 'tablet'
isBackground: boolean // ★ バックグラウンド再生だったか(EMB-14)
isOffline: boolean
occurredAt: string
}
// ---- 分析(集計値。ストアに保存せず lib/calc で算出) ----
type TrackStats = {
trackId: string
plays: number
uniqueListeners: number
completionRate: number // 完聴率
skipRate: number // スキップ率
avgSkipAtSec: number // ★ 平均スキップ秒数(FR-802)
byHour: number[] // 24要素。時間帯別
byDevice: Record<'desktop'|'mobile'|'tablet', number>
byQuality: Record<'high'|'standard', number>
}
// ---- アップロード(擬似) ----
type UploadJob = {
id: string
fileName: string
sizeBytes: number
stage: 'uploading' | 'analyzing' | 'transcoding' | 'waveform' | 'palette' | 'done' | 'failed'
progress: number
errorReason?: string
linkedTrackId?: string // 完了時に既存素材へ紐付け(FR-703)
createdAt: string
}
シードデータ(lib/seed/)
| ファイル | 内容 | 件数 |
|---|---|---|
genres.ts |
ジャンル | 8件 |
artists.ts |
アーティスト(すべて架空) | 3〜5組 |
albums.ts |
アルバム(ジャケット + 抽出済みパレット) | 4〜6枚 |
tracks.ts |
楽曲(実在する音声URLを指す。波形・ライセンス情報を含む) | 24〜36曲 |
lyrics.ts |
時間同期歌詞(自作音源のみ、RGT-04) | 4曲分 |
plans.ts |
プラン | 3件 |
members.ts |
会員(無料/有料を分散、過去90日で登録日を散らす) | 180名 |
playlists.ts |
公式プレイリスト + ペルソナの個人プレイリスト | 8件 |
playEvents.ts |
再生イベント(過去90日分。完聴率・スキップ率が意味を持つ分布) | 20,000件 |
playbackState.ts |
ペルソナの初期再生状態(途中まで聴いた曲を1つ) | — |
シード作成のルール(品質を左右する)
- アーティスト名・アルバム名・曲名は架空で、かつ実在を想起させないこと(RGT-05)
- 曲名を「Track 01」のようなダミーにしない。 世界観のある名前を付ける。ここが手抜きだとアプリ全体が安っぽくなる
- ジャンルとBPMに幅を持たせる。 全曲が似ていると、レコメンドもプレイリストも意味を持って見えない
- 再生イベントは自然な分布で生成する。 冒頭10秒でのスキップが一定割合あり、完聴する曲としない曲がある。曲ごとにパターンを変える
- スキップされやすい曲を意図的に作る。 全曲が完聴されると、分析画面が無意味になる
- 時間帯別の再生に山を作る(通勤時間帯・夜間)。フラットだと「いつ聴かれるか」の分析が死ぬ
- 日付は現在日時からの相対で生成する。固定日付を埋め込まない
- ペルソナに「途中まで聴いた曲」を持たせる(起動直後の「続きから聴く」が空にならないように)
- ライセンス未記入の楽曲を1〜2曲、下書き状態で用意する(フローCの実演用)
ストアとリポジトリ
lib/
├── types/ seed/
├── audio/ # ★ オーディオの中核。ここだけが Web Audio を触る
│ ├── engine.ts # AudioContext / GainNode / AnalyserNode のグラフ構築
│ ├── player.ts # 再生・停止・シーク・曲送り
│ ├── crossfade.ts # クロスフェードとギャップレス
│ ├── loudness.ts # ラウドネス正規化のゲイン計算
│ ├── media-session.ts # Media Session API
│ └── visualizer.ts # AnalyserNode からの描画データ生成
├── offline/
│ ├── cache.ts # Cache API のラッパー(FR-402)
│ └── download-queue.ts
├── calc/ # 完聴率・スキップ率・レコメンド・パレット適用
├── query/ # 検索・絞り込み
├── store/ { session, content, playback, library, download, ui }
└── repo/ # ★ コンポーネントが触るのはここだけ
├── _delay.ts _scope.ts
├── tracks.ts albums.ts artists.ts playlists.ts
├── members.ts plans.ts downloads.ts uploads.ts analytics.ts
リポジトリ層とオーディオ層の規約(厳守)
- リポジトリの全メソッドを
asyncで定義。戻り値は{ ok: true; data: T } | { ok: false; error: string }に統一 - コンポーネントから Zustand ストアを直接参照しない
AudioContextと<audio>をlib/audio/以外で触らない。 コンポーネントはusePlayer()フック経由でのみ操作する- 再生可否の判定(
resolveAccess(track, role))はlib/repo/_scope.tsに集約する。 コンポーネント側でrole === 'premium'のような分岐を書かない(FR-908) - 派生値(完聴率・スキップ率・パレット適用色)はストアに保存せず
lib/calc/で算出する - 再生・一時停止・シーク・曲送りに擬似ディレイを入れない
// lib/repo/_scope.ts — 実案件では署名付きURL発行・DRM に置き換わる箇所
export function resolveAccess(track: Track, member: Member, plan: Plan): AccessResult {
if (track.access === 'free') return { kind: 'full', quality: plan.maxQuality }
if (member.role === 'premium' || member.role === 'admin')
return { kind: 'full', quality: plan.maxQuality }
return { kind: 'preview', limitSec: track.previewSec }
}
export function canDownload(plan: Plan): boolean { return plan.canDownload }
10. デザイン要件
アートディレクション
「アートワークの色に、画面を染める」
音楽アプリは、聴いている間ほとんど見られない。だから見られる瞬間に強い印象を残す必要がある。KANADE は、**再生中の曲のジャケットから抽出した色が、画面全体に静かに広がる。**曲が変わると色が変わる。
- ベースは暗いが、無彩色ではない。 現在の曲のパレットで背景が染まる
- モーションは音に同期する。 ビジュアライザー、波形、再生中のアイコンの脈動
- アートワークが最大の要素。 全画面プレーヤーでは画面の半分以上をジャケットが占める
- ミニプレーヤーは常に画面下に。 音楽が鳴っている間、それが見えていることが安心につながる
CASTA との差別化(特に注意) どちらもダーク基調のメディアアプリだが、方向は真逆にする。 CASTA:色を持たない(無彩色)/モーションを抑える/UIが消える KANADE:色が曲ごとに変わる/モーションが音に同期する/UIが色に染まる 並べたときに「同じダークテーマ」と見えたら失敗。
カラートークン
:root {
/* 静的な基盤 */
--bg-deep: #08090C; /* 最下層 */
--bg-base: #101218; /* ページ背景(この上にパレットが乗る) */
--bg-raised: #191C24; /* カード */
--bg-overlay: #23262F; /* モーダル・シート */
--fg-high: #F4F5F7;
--fg-mid: #A6ACB8;
--fg-low: #6E7480;
--border: #262A33;
/* ★ ダイナミック:現在の曲のジャケットから抽出して JS で更新する */
--art-1: #4A5A8C; /* 主要色 */
--art-2: #7A5A6E; /* 副次色 */
--art-3: #2A3350; /* 暗色 */
--art-accent: #C8A45E; /* アクセント(再生・進捗・現在地) */
/* 派生グラデーション */
--wash: radial-gradient(120% 80% at 50% 0%,
color-mix(in srgb, var(--art-1) 28%, transparent) 0%,
transparent 70%);
}
ダイナミック配色のルール(重要)
| ID | 要件 |
|---|---|
| CLR-01 | 曲が変わったら、その曲のアルバムパレットで --art-* を更新すること |
| CLR-02 | 色の切り替えは 600ms かけて補間すること。 瞬間的に色が変わると目に刺さる |
| CLR-03 | 抽出した色をそのまま背景にしない。 明度と彩度を抑え、--bg-base に 15〜30% 混ぜる程度に留める |
| CLR-04 | テキストのコントラスト比を、色が変わっても常に 4.5:1 以上に保つこと。 抽出色が明るすぎる場合は自動で暗く補正する。これを実装しないと、曲によって文字が読めなくなる |
| CLR-05 | 管理画面ではダイナミック配色を適用しないこと(作業画面が曲で色が変わると邪魔になる) |
| CLR-06 | デモ切替バーはダイナミック配色の影響を受けないこと |
タイポグラフィ
| 役割 | 書体 | 用途 |
|---|---|---|
| ディスプレイ | Noto Sans JP 900(letter-spacing: -.02em) |
曲名・アルバム名。詰めた極太で、ジャケットと拮抗させる |
| 本文 | Noto Sans JP 400 / 500 | 説明、リスト |
| 時間 | Roboto Mono 500(tabular-nums) |
再生時間、残り時間 |
| トークン | デスクトップ | モバイル | 用途 |
|---|---|---|---|
| display | 40px / 900 | 28px | 全画面プレーヤーの曲名 |
| title | 24px / 900 | 20px | アルバム名、画面タイトル |
| item | 15px / 500 | 15px | リストの曲名 |
| body | 14px / 400 | 14px | 説明、アーティスト名 |
| meta | 12px / 400 | 12px | 再生時間、トラック番号 |
| time | 13px / 500 Mono | 12px | プレーヤーの時間表示 |
CASTA は 700 まで、KANADE は 900 を使う。 極太で詰めたタイポグラフィが、音楽アプリらしい強さを作る。
レイアウトとスペーシング
| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース:4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 |
| ミニプレーヤー | 画面下部に常駐。高さ 64px(モバイル)/80px(デスクトップ)。 コンテンツ領域は必ずこの分の余白を確保すること |
| 全画面プレーヤー | ボトムシート的に下から迫り上がる。ジャケットは画面幅の80%(最大 480px) |
| セーフエリア | env(safe-area-inset-bottom) を必ず考慮(ミニプレーヤーがホームインジケータに被らないように) |
| 高さ | 100dvh を使う。100vh を使わない |
| グリッド | アルバムグリッドは モバイル2列/タブレット3列/デスクトップ 4〜6列 |
| 角丸 | 8px(ジャケット・カード)/4px(バッジ)/999px(ボタン・チップ) |
| 影 | ジャケットのみに落とす(0 8px 24px rgba(0,0,0,.4))。UIカードには使わず、明度差で分ける |
主要コンポーネント仕様
| コンポーネント | 仕様 |
|---|---|
| ミニプレーヤー | ジャケット48px/曲名(1行クランプ)/アーティスト/再生停止/次へ。上端に極細の進捗バー(2px、--art-accent)。タップで全画面へ |
| 全画面プレーヤー | 大ジャケット/曲名(display)/アーティスト/波形シークバー/コントロール/お気に入り・キュー・歌詞タブ。背景に --wash |
| 波形シークバー | 事前生成の512点を縦バーで描画。再生済みは --art-accent、未再生は --fg-low の40%。ドラッグ中は拡大表示 |
| 再生ボタン | 全画面では64px、ミニでは40px。再生中はアイコンが微かに脈動(2秒周期) |
| トラック行 | トラック番号(Mono)/曲名/アーティスト/再生時間(Mono)/お気に入り/メニュー。再生中の行はアクセント色 + 小さな音波アニメーション |
| アルバムカード | ジャケット(正方形)/アルバム名(2行クランプ)/アーティスト。ホバーで再生ボタンがジャケット右下に浮かぶ |
| キューシート | ドラッグハンドル付きリスト。「次に再生」と「このあと」でセクション分け |
| ビジュアライザー | Canvas。スペクトラム(バー)/波形(線)の2種。prefers-reduced-motion で既定OFF |
| 歌詞ビュー | 現在行を --fg-high + 拡大、前後は --fg-low。自動スクロールは中央固定 |
| ペイウォール | ジャケットの上にオーバーレイ。ブラー + 暗転。プラン概要・CTA・閉じる導線 |
| ダウンロードバッジ | ダウンロード済みは下向き矢印アイコン。進行中は円形プログレス |
| オフラインバナー | 上端固定。「オフラインです。ダウンロード済みの曲を再生できます」+ 絞り込み導線 |
| 空状態 | 全一覧に専用の空状態。ダーク背景に合う線画 |
| デモ切替バー | ダイナミック配色の影響を受けない固定の濃色帯(CLR-06) |
モーション
| 対象 | duration | 内容 |
|---|---|---|
| パレットの切替 | 600ms | 色の補間(CLR-02) |
| 全画面プレーヤーの開閉 | 320ms | 下から迫り上がる。cubic-bezier(.32,.72,0,1) |
| ジャケットの遷移 | 320ms | ミニ↔全画面で共有要素として拡大縮小(position の連続性を保つ) |
| 再生ボタンの脈動 | 2000ms | ループ。再生中のみ |
| 再生中トラック行の音波 | 900ms | 3本のバーが上下する。ループ |
| ビジュアライザー | 60fps | AnalyserNode に同期 |
| クロスフェード | 設定秒数 | GainNode で音量交差(視覚的にも波形が重なる表現) |
| トースト | 200ms | 下から(ミニプレーヤーの上に出す) |
prefers-reduced-motion: reduce 時は、パレット切替を即時にし、脈動・音波・ビジュアライザーを停止すること。
アクセシビリティ(WCAG 2.1 AA)
| ID | 要件 |
|---|---|
| A11Y-01 | ダイナミック配色でも、テキストのコントラスト比を常に 4.5:1 以上に保つこと(CLR-04)。全アルバムのパレットで実測して検証すること |
| A11Y-02 | プレーヤーの全操作をキーボードで完遂できること(FR-115) |
| A11Y-03 | 音声ありの自動再生を行わないこと |
| A11Y-04 | 波形シークバーは role="slider" とし、aria-valuenow / aria-valuetext(「1分24秒 / 3分40秒」)を更新すること |
| A11Y-05 | 再生・一時停止・曲の変更を aria-live="polite" で通知すること |
| A11Y-06 | 再生中のトラックを、色とアニメーションだけでなくテキスト(「再生中」)でも示すこと |
| A11Y-07 | フォーカスリングは2px・オフセット2pxで常時可視。ジャケットの上でも視認できる色にすること |
| A11Y-08 | ビジュアライザーは装飾であり、aria-hidden="true" とすること。OFFにできること |
| A11Y-09 | ペイウォール表示時にフォーカスをトラップし、Escで閉じられること |
| A11Y-10 | タップ領域は最小44×44px。ミニプレーヤーの再生ボタンは特に、誤タップしやすいので48px確保すること |
| A11Y-11 | フォントサイズ200%指定でも、ミニプレーヤーとトラックリストが操作できること |
ライティング規約
| 原則 | 例 |
|---|---|
| 再生状態は簡潔に | ○「無料プレビュー 残り0:23」/×「あなたは現在無料プレビューを再生しています」 |
| ペイウォールは責めない | ○「続きはスタンダードプラン以上でお聴きいただけます」/×「再生権限がありません」 |
| オフラインは不安にさせない | ○「オフラインです。ダウンロード済みの曲を再生できます」/×「通信エラー」 |
| ダウンロード不可は条件を書く | ○「ダウンロードはプレミアムプランでご利用いただけます」/×「ダウンロードできません」 |
| エラーは原因と対処 | ○「通信が不安定なため再生できません。再試行するか、音質を標準に下げてお試しください」/×「エラーが発生しました」 |
| 空状態は誘導する | ○「まだプレイリストがありません。好きな曲を集めてみましょう」+ 作成ボタン |
| システム語を使わない | ○「この曲を非公開にする」/×「statusをprivateに更新」 |
11. 技術要件・ディレクトリ構成
技術スタック(固定・勝手に変更しない)
| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15(App Router)/ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | ダイナミック配色は CSS 変数を JS で更新 |
| UIコンポーネント | shadcn/ui(Radix UI基盤) | ダークテーマに合わせて上書き |
| 状態管理 | Zustand + persist | localStorage に永続化 |
| 音声再生 | HTMLAudioElement + Web Audio API | MediaElementAudioSourceNode を起点にノードグラフを構築 |
| プレーヤーUI | 自前実装 | howler.js / wavesurfer.js を使わない。既製ライブラリを貼るとデモの意味がなくなる |
| バックグラウンド操作 | Media Session API | FR-152 |
| オフライン | Cache API + Service Worker | FR-402。実ファイルを保存する |
| PWA | next-pwa(または自前 Service Worker) | |
| D&D | dnd-kit | キュー・プレイリスト・トラック順。キーボード対応必須 |
| グラフ | Recharts | 分析画面(動的import) |
| フォーム | React Hook Form + Zod | |
| 日付 | date-fns(ja locale) | |
| アイコン | lucide-react | |
| ホスティング | Vercel | アプリ本体 |
| 音源配信 | Cloudflare R2 + CDN | 4章参照。Range リクエスト対応が必須 |
| 音源準備 | ffmpeg / ffprobe(scripts/prepare-audio.sh) |
ローカル実行 |
| テスト | Vitest / Playwright / axe-core |
上記以外のライブラリを入れる前に必ず提案し、承認を得ること。特に音声再生ライブラリ(howler.js / Tone.js / wavesurfer.js)の導入は禁止。
ディレクトリ構成
/
├── CLAUDE.md
├── app/
│ ├── layout.tsx # ★ PlayerProvider / MiniPlayer をここに常駐させる
│ ├── page.tsx # SC-001 ホーム
│ ├── play/[trackId]/page.tsx # SC-003(?embed=1)
│ ├── albums/[id]/ artists/[id]/ playlists/
│ ├── library/ search/ history/ downloads/
│ ├── settings/ plans/
│ ├── admin/ # SC-100〜150
│ └── dev/components/
├── components/
│ ├── player/ # ★ 最重要
│ │ ├── PlayerProvider.tsx # <audio> は アプリ全体でここに1つだけ
│ │ ├── MiniPlayer.tsx # 常駐
│ │ ├── FullPlayer.tsx # オーバーレイ
│ │ ├── WaveformSeekBar.tsx
│ │ ├── QueueSheet.tsx
│ │ ├── LyricsView.tsx
│ │ ├── Visualizer.tsx
│ │ ├── Paywall.tsx
│ │ └── usePlayer.ts # ★ コンポーネントはこのフック経由でのみ操作する
│ ├── catalog/ # AlbumCard, TrackRow, ArtistCard, Grid
│ ├── domain/ # DownloadBadge, OfflineBanner, LicenseForm, UploadStepper, StatsChart
│ ├── demo/ # RoleSwitcher, ResetButton, GuideTour, BackToArticle
│ └── layout/
├── lib/
│ ├── types/ seed/ store/ repo/
│ ├── audio/ # ★ Web Audio を触る唯一の場所
│ ├── offline/ # Cache API
│ ├── calc/ query/ validation/ utils/
├── scripts/
│ └── prepare-audio.sh # ffmpeg エンコード・波形生成・パレット抽出
├── docs/
│ └── audio-license.md # ★ 全曲のライセンス記録(RGT-02)
├── e2e/
└── public/
コーディング規約
- TypeScript strict。
any禁止 AudioContextと<audio>をlib/audio/とPlayerProvider以外で触らない- コンポーネントは
usePlayer()フック経由でのみプレーヤーを操作する - コンポーネントから Zustand ストアを直接参照しない。必ず
lib/repo/経由 - 再生可否を
role === 'premium'のようにコンポーネントに書かない。resolveAccess()を使う AudioContextはユーザー操作を起点にのみ生成する(FR-102)MediaElementAudioSourceNodeは1つの<audio>に対して1回しか作れない。 再生成しようとするとエラーになるため、必ず1度だけ構築して使い回すこと- ビジュアライザーの
requestAnimationFrameは、再生停止時・アンマウント時に必ずキャンセルすること - 高さは
100dvhを使う。100vhを使わない - コミットは1タスクごと。メッセージに要件IDを含める
例:
feat(player): クロスフェードをGainNodeで実装 (FR-106)
12. 非機能要件
| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | 再生ボタン押下から音が出るまで | 1.5秒以下 |
| NFR-02 | 曲送り操作から次の曲が鳴るまで | 500ms以下 |
| NFR-03 | 曲間の無音(ギャップレス時) | 知覚できないこと(50ms以下) |
| NFR-04 | LCP(モバイル4G相当) | 2.5秒以下 |
| NFR-05 | INP | 200ms以下 |
| NFR-06 | CLS | 0.1以下。ミニプレーヤー分の余白を最初から確保すること |
| NFR-07 | 埋め込みモードの初期JSバンドル(gzip後) | 60KB以下 |
| NFR-08 | アプリ本体の初期JSバンドル(gzip後) | 200KB以下(Recharts は動的import) |
| NFR-09 | ビジュアライザー描画時のフレームレート | 60fps維持。維持できない環境では自動的に簡易表示へ |
| NFR-10 | 30曲を連続再生してもメモリが単調増加しないこと | — |
| NFR-11 | Lighthouse | Performance 90 / Accessibility 95 以上 |
| NFR-12 | 対応環境 | iOS Safari 16以降、Android Chrome 最新、PC Chrome / Edge / Safari / Firefox 最新2バージョン |
| NFR-13 | 音源配信で Range リクエスト(206)に対応し、シークが正しく動作すること | — |
| NFR-14 | Cache API の使用量が上限に達した場合、警告を表示し、古いダウンロードから削除すること。データを破損させないこと | — |
セキュリティ・権利
| ID | 要件 |
|---|---|
| SEC-01 | 全楽曲のライセンスを docs/audio-license.md に記録すること(RGT-02) |
| SEC-02 | ジャケット・アーティスト画像も同様にライセンスを記録すること |
| SEC-03 | ユーザー入力(プレイリスト名・管理画面の説明文)をサニタイズすること |
| SEC-04 | 申込フォームの入力をどこにも送信しない。 ブラウザ内で完結し、リセットで消えることを明示すること |
実案件への転用時の注意(デモ本体の要件ではない。商談で聞かれた際に説明できるよう記録)
- 本デモの音源URLは公開されており、URLを知る第三者が直接ダウンロードできる。 実サービスでは署名付きURL(有効期限付き)や暗号化配信が必要
- 有料コンテンツを扱う場合、特定商取引法に基づく表記、解約条件の明示が必要
- 権利者への使用料の分配が必要な場合、再生回数の正確な記録と集計の仕組みが要る。 これは開発スコープに大きく影響する
- 会員情報は個人情報保護法の対象
- 本デモは会員情報をサーバーに送信しないため、これらの義務は発生しない。「デモである」旨は画面上に常時明示する
13. 費用設計
記事の主題の半分は「費用」。この章は実装要件であると同時に、記事本文の原稿素材でもある。
音楽アプリの費用は、他のアプリと構造が違う
| 層 | 内容 | 特徴 |
|---|---|---|
| ⓪ 音源のライセンス費 | 楽曲の使用許諾、原盤権、権利者への分配 | 音楽アプリ固有。しかも最初に検討すべき最大の分岐(2章) |
| ① 初期開発費 | 設計・実装・テスト | 機能スコープで決まる |
| ② 固定運用費 | ホスティング、ドメイン、監視 | 小さい |
| ③ 変動運用費 | 転送量・ストレージ | 視聴時間に比例。音楽はリピート率が高い |
⓪を無視して①だけ見積もると、事業が成立しない。 記事ではこの順序で説明する。
⓪ 音源のライセンス費(最大の分岐)
| 調達方法 | 費用構造 | 注意点 |
|---|---|---|
| 自社で制作 | 制作費(作編曲・演奏・録音・ミックス)の買い切り | **最も確実。**権利をすべて自社に集約できる契約にすること |
| ロイヤリティフリー素材 | 買い切り or 月額 | 「フリー」の意味が提供元ごとに違う。 再配信可か、必ず規約を確認する |
| サブスク型音源ライブラリ | 月額 | 解約後も配信を続けられるかを必ず確認する |
| アーティストとの契約 | 前払い + 再生数に応じた分配 | 分配のための再生数集計の実装が必要(開発費に跳ねる) |
| 既存の商用楽曲 | — | 2章。実質不可能 |
「再生数に応じた分配」を採用する場合、集計の正確性が事業リスクになる。 何を1再生と数えるか(30秒以上か、完聴か)を最初に決める必要があり、これは仕様であり費用でもある。
③ 変動運用費の計算式
月間転送量(GB) = ビットレート(kbps) ÷ 8 ÷ 1024 × 平均再生秒数 × 月間再生数 ÷ 1024
例:標準音質(128kbps)/ 平均再生 3分(180秒)/ 月間 50,000再生
= 128 ÷ 8 ÷ 1024 × 180 × 50,000 ÷ 1024
≈ 134 GB/月
| 規模 | 月間再生数 | 概算転送量(128kbps) |
|---|---|---|
| 小(教室・社内) | 5,000 | 約 13 GB |
| 中(会員制配信) | 50,000 | 約 134 GB |
| 大(成長したサブスク) | 500,000 | 約 1.3 TB |
動画より1再生あたりは軽い。ただし音楽はリピート率が圧倒的に高い。 同じ曲を何十回も聴かれるため、再生数の伸び方が動画とは違う。
そしてオフラインダウンロードは、転送量を減らす機能でもある。 1回ダウンロードすれば、以降の再生で転送が発生しない。FR-401〜409 は機能要件であると同時にコスト最適化施策であり、記事ではこの視点を示す。
開発費の目安(スコープ別)
| スコープ | 含まれるもの | 目安 |
|---|---|---|
| 最小構成 | 曲一覧・再生(既製プレーヤー)・会員なし | 小 |
| 標準構成(本デモ相当) | 自前プレーヤー(ギャップレス・クロスフェード・キュー)/バックグラウンド再生/オフライン/会員制ペイウォール/配信管理/ライセンス管理/再生分析 | 中 |
| 拡張構成 | 上記 + ネイティブアプリ + DRM + 権利者への分配計算 + 車載連携 | 大 |
Webで足りるか、ネイティブが必要か
位置情報アプリ(FIELDPIN)と違い、音楽アプリはWebでかなりのことができる。 これは記事の重要な発見になる。
| やりたいこと | Web(PWA) | ネイティブ |
|---|---|---|
| バックグラウンド再生 | できる | できる |
| ロック画面・通知領域での操作 | できる(Media Session API) | できる |
| オフライン再生 | できる(Cache API) | できる |
| ホーム画面から起動 | できる | できる |
| ギャップレス・クロスフェード | できる(Web Audio API) | できる |
| CarPlay / Android Auto 連携 | できない | できる |
| ネイティブのオーディオフォーカス制御 | 限定的 | できる |
| 大容量のオフライン保存 | ブラウザの容量制限あり | できる |
判断の目安: 車載連携と大量のオフライン保存が必須でなければ、Webで十分。開発費は大きく下がる。
デモ自体の運用コストを実測して記事に載せる
| ID | 要件 |
|---|---|
| COST-01 | デモ公開後、月次で「再生数・総再生時間・転送量・ダウンロード数・実際にかかった費用」を記録すること |
| COST-02 | 記録した実測値を記事に掲載し、確認日を明記すること |
| COST-03 | 転送量の上限アラートを設定すること |
| COST-04 | ダウンロード機能による転送量削減効果を測定し、記事で示すこと。「機能追加がコストを下げた実例」として説明できる |
記事に載せる際の注意: 音源ライブラリや配信サービスの単価は変動するため、この文書に金額を書き込まない。 記事執筆時点で公式の料金ページ・規約を確認し、確認日を明記して掲載すること。 特にライセンス条件は変更されることがあるため、「必ず最新の規約を確認すること」を記事に明記する。
14. 実装タスク
この順に進める。フェーズを飛ばさない。 完了時は [x] に更新する。
Phase 0 — 基盤と音源(3日)
0-1. 初期化
- Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- ESLint / Prettier / husky(pre-commit で lint + typecheck)
- 11章のディレクトリ構成を作成
0-2. 音源の準備(先にこれを終わらせる。素材がないとプレーヤーが作れない)
- 使用する音源を確定し、2章 RGT-01〜07 の権利要件を満たすことを確認する
-
docs/audio-license.mdに全曲のライセンスを記録(取得元・種別・許諾範囲・証跡URL・確認日)(RGT-02) -
scripts/prepare-audio.shを作成(AAC 256k/128k 変換、-14 LUFS 正規化、波形512点のJSON化、ジャケット3サイズ + パレット5色抽出) - 24〜36曲をエンコード。ジャンルとBPMに幅を持たせる(AUD-04)
- ギャップレス実演用に、切れ目なく繋がる2曲を1組以上用意(AUD-06)
- Cloudflare R2 にアップロードし、CDN経由で配信できることを確認
- Range リクエスト(206)が返ることを確認(NFR-13)
- CORS と Cache-Control を設定
- 架空のアーティスト・アルバム・曲名を作成(ダミー名にしない、RGT-05)
0-3. 型とシード
-
lib/types/に9章の型定義をすべて実装 -
lib/seed/の全ファイル -
playEvents.tsを自然な分布で生成。 冒頭10秒スキップが一定割合、曲ごとに完聴率のパターンを変える - スキップされやすい曲を意図的に作る。 時間帯別に山を作る
- ペルソナに「途中まで聴いた曲」を持たせる
- ライセンス未記入の下書き楽曲を1〜2曲用意(フローCの実演用)
0-4. ストアとリポジトリ
-
lib/store/{session,content,playback,library,download,ui}.tsを Zustand + persist で実装 -
lib/repo/_scope.tsにresolveAccess()とcanDownload()を実装(FR-908) -
lib/repo/の全モジュール。再生系にディレイを入れない -
lib/calc/に 完聴率・スキップ率・レコメンド・パレット補正 を実装 -
resolveAccess()とcalcの単体テストをこの時点で書く
0-5. デザインシステム
-
app/globals.cssにカラートークンを CSS 変数で定義(静的 + ダイナミック) -
lib/calc/palette.ts:抽出色から--art-*を算出し、コントラスト比4.5:1を保証する補正を実装(CLR-04) - 全アルバムのパレットでコントラスト比を実測して検証する(A11Y-01)
-
tailwind.config.tsから CSS 変数を参照するよう theme を拡張 - フォント(Noto Sans JP 400/500/900 / Roboto Mono)。時間に
tabular-nums -
100dvhとセーフエリアの対応 - shadcn/ui を導入し、ダークトークンに合わせて上書き
- 共通コンポーネント:
AlbumCard/TrackRow/EmptyState/Skeleton/Badge/Grid -
prefers-reduced-motionの対応 -
/dev/componentsにコンポーネントカタログを作成
0-6. デモ基盤
-
RoleSwitcher(4ロール)(FR-901) - デモ切替バー。ダイナミック配色の影響を受けない固定の濃色帯(CLR-06)
- 「デモをリセット」(localStorage + Cache API をクリア)(FR-902)
- 元記事リンク・資料ダウンロード・権利注記(FR-904, FR-905)
Phase 0 完了チェック
- R2 の音声URLをブラウザで直接開いて再生でき、シークできる
-
docs/audio-license.mdに全曲が記録されている - どのアルバムのパレットでも、テキストのコントラスト比が4.5:1以上
-
/dev/componentsで全コンポーネントがトークン通りに表示される
Phase 1 — プレーヤー(5日・最重要フェーズ)
1-1. オーディオエンジン
-
PlayerProvider.tsx:アプリ全体で<audio>を1つだけ生成し、レイアウトに常駐させる(FR-108) -
lib/audio/engine.ts:AudioContext→MediaElementAudioSourceNode→GainNode→AnalyserNode→ destination のグラフ構築 -
AudioContextをユーザー操作起点でのみ生成(FR-102) -
MediaElementAudioSourceNodeは1度だけ作り、使い回す(再生成でエラーになるため) -
usePlayer.tsフック:コンポーネントからの唯一の操作口 - 再生/停止/シーク/音量/曲送り戻し(3秒以降は曲頭へ)(FR-101)
- リピート・シャッフル(現在曲を先頭固定、解除で元順序復帰)(FR-103, FR-204)
- ラウドネス正規化(GainNode で LUFS 差を補正)(FR-107)
- エラー表示(原因を区別 + 再試行)(FR-111)
1-2. ギャップレスとクロスフェード
- 次曲の事前バッファ(FR-105)
-
lib/audio/crossfade.ts:2つ目の<audio>を裏で用意し、GainNode で音量を交差(FR-106) - クロスフェード秒数の設定(0/2/4/6秒)。0秒でギャップレス動作
- 曲間の無音が50ms以下であることを実測(NFR-03)
1-3. UI
-
MiniPlayer.tsx:画面下部常駐。上端に極細進捗バー。コンテンツ側に余白を確保(NFR-06) -
FullPlayer.tsx:下から迫り上がる。ジャケットをミニ↔全画面で共有要素として拡大縮小 -
WaveformSeekBar.tsx:事前生成512点の描画、再生済みの塗り分け、ドラッグ中の拡大 -
role="slider"とaria-valuetext(A11Y-04) -
QueueSheet.tsx:ドラッグ並び替え、個別削除、「次に再生」(FR-201, FR-202) - ダイナミック配色の適用:曲変更で
--art-*を600msかけて補間(CLR-01, CLR-02) - 再生位置・キュー・モード・音量の永続化と復元(FR-110)
- キーボードショートカット(FR-115)
1-4. バックグラウンド再生とロック画面
-
lib/audio/media-session.ts:メタデータ設定(曲名・アーティスト・アルバム・アートワーク)(FR-152) - アクションハンドラ(play / pause / nexttrack / previoustrack / seekto)(FR-153, FR-154)
-
positionStateの更新(FR-155) - 曲変更時のメタデータ更新(FR-156)
- タブ非表示・画面消灯でも再生継続を確認(FR-151)
- オーディオフォーカス喪失時の一時停止(FR-157)
- 実機(iOS / Android)でロック画面の表示と操作を検証する
1-5. 埋め込みモード
- SC-003 埋め込みモード(FR-159, EMB-02〜07)
- アートワーク + 再生ボタンのみの初期表示。押下で音源取得
- Media Session API を使わない(FR-159)
- リアルタイム解析を行わず、事前波形のハイライトのみ(FR-504, EMB-07)
- タブ非表示で一時停止(EMB-06)
- 初期バンドル 60KB以下を実測
Phase 1 完了チェック
- 再生ボタン押下から1.5秒以内に音が出る(NFR-01)
- 曲間に無音が知覚できない(ギャップレス、NFR-03)
- 画面を遷移しても一度も音が途切れない(FR-108)
- 実機で画面を消しても再生が続き、ロック画面から操作できる(FR-151〜153)
- 30曲を連続再生してもメモリが単調増加しない(NFR-10)
- 埋め込みモードで自動再生が起きず、初期バンドルが60KB以下
Phase 2 — ライブラリとペイウォール(4日)
2-1. カタログ
- SC-001 ホーム(続きから聴く、最近再生、おすすめ、新着、公式プレイリスト)(FR-209)
- SC-010 ライブラリ(4タブ)(FR-301)
- SC-011 アルバム詳細 / SC-012 アーティスト詳細
- SC-020 検索(表記ゆれ吸収、種別グループ化、URL同期)(FR-302〜304)
- SC-021 ジャンル一覧
- レコメンドのルールベース実装と「なぜ推薦されたか」の表示(FR-305, FR-306)
- トラック行のコンテキストメニュー(FR-307)
- 再生中トラック行の音波アニメーション + 「再生中」テキスト(A11Y-06)
2-2. プレイリスト・お気に入り
- SC-013 / SC-014 プレイリスト詳細・作成編集(FR-205)
- カバーの自動生成(ジャケット4枚)(FR-206)
- SC-015 お気に入り(FR-207)
- SC-030 再生履歴(FR-208)
- アルバム・プレイリスト再生時のキュー置換(警告なく消さない)(FR-203)
2-3. ペイウォール
- 有料バッジ表示(FR-601)
- プレビュー制限:30秒でフェードアウトして停止(FR-602)
- 残り時間の常時表示(FR-603)
-
Paywall.tsx:オーバーレイ、プラン概要、CTA、閉じる導線、フォーカストラップ(FR-604, A11Y-09) - SC-040〜042 プラン比較・申込・完了(FR-606〜608)
- 加入後、停止位置から続きを再生(FR-605)
- SC-043 契約状況、解約(FR-609)
2-4. 設定
- SC-032 再生設定(クロスフェード・音質・自動再生・ラウドネス正規化)(FR-112, FR-121)
- SC-033 スリープタイマー(5秒フェードアウト)(FR-113)
Phase 2 完了チェック
- フローBの前半が完走する(プレビュー → ペイウォール → 加入 → 続きから再生)
- 6章「ロールを跨ぐ体験」の1と2が動作する
- 検索・ライブラリを行き来しても再生が途切れない
Phase 3 — オフラインとPWA(3日)
- Service Worker / PWA マニフェスト(FR-409)
-
lib/offline/cache.ts:Cache API に実際の音声ファイルを保存(FR-402) - ダウンロード実行(有料会員のみ)と進捗表示、中断・再開(FR-401, FR-403)
- オフライン時にダウンロード済みの曲が実際に再生できること(FR-404)
- オフライン時の未ダウンロード曲の扱いと絞り込み導線(FR-405)
- SC-031 ダウンロード管理(一覧・容量・削除)(FR-406)
- 容量上限の警告(FR-407, NFR-14)
- オフラインバナー(FR-408)
- 解約時のダウンロード再生不可(FR-609)
- 「デモをリセット」で Cache もクリアされることを確認(FR-902)
Phase 3 完了チェック
- フローBの後半が完走する(ダウンロード → 機内モード → オフライン再生)
- 6章「ロールを跨ぐ体験」の4が動作する
- PWA としてホーム画面から起動でき、オフラインでも動く
Phase 4 — ビジュアライザー・歌詞・配信管理(4日)
4-1. ビジュアライザーと歌詞
-
Visualizer.tsx:AnalyserNode からスペクトラム/波形を Canvas 描画(FR-501) - 60fps維持、不足時は簡易表示へ自動フォールバック(FR-502, NFR-09)
-
prefers-reduced-motionで既定OFF、手動OFFも可(FR-503) -
requestAnimationFrameの確実なキャンセル -
LyricsView.tsx:現在行ハイライト、自動スクロール、タップでジャンプ(FR-505, FR-506) - 歌詞がない曲ではタブを出さない(FR-507)
4-2. 配信管理
- SC-100 管理ダッシュボード
- SC-111 音源アップロード:変換進捗の段階再現(FR-701, FR-702)
- 完了後は既存素材に紐付け。デモである旨を明示(FR-703)
- 変換失敗の再現とリトライ(FR-704)
- SC-112 楽曲編集(FR-705)
- SC-113 ライセンス管理。未記入では公開できないバリデーション(FR-706)
- SC-110 楽曲一覧でライセンス未記入を警告表示・絞り込み(FR-707)
- 公開設定と、非公開でリスナー側から即座に消えること(FR-708)
- SC-114 歌詞編集(再生しながらタイムスタンプ打ち込み)(FR-709)
- SC-120 アルバム管理(dnd-kit で並び替え、キーボード対応)(FR-710)
- SC-121 アーティスト管理 / SC-130 公式プレイリスト(FR-711, FR-712)
- SC-140 会員・プラン管理
- 一括操作(FR-713)
Phase 4 完了チェック
- フローCが完走する(アップロード → ライセンス記入で公開解禁 → 歌詞 → 公開 → リスナー側に出現)
- 6章「ロールを跨ぐ体験」の5が動作する
- ライセンス未記入の楽曲が絶対に公開できない
Phase 5 — 分析・記事統合・仕上げ(3日)
5-1. 再生分析
- SC-150 再生数・総再生時間・ユニークリスナー(期間指定)(FR-801)
- 完聴率・スキップ率・平均スキップ秒数(FR-802)
- 曲別・アルバム別・アーティスト別ランキング(FR-803)
- 時間帯別の再生数(FR-804)
- デバイス別・音質別(FR-805)
- デモ中の再生が分析に反映されること(FR-806)
- CSVエクスポート(FR-807)
5-2. 記事統合
- 記事側の埋め込みコードを作成し、実際の記事ページで動作確認(EMB-01〜07)
- 記事を読んでいるだけで音が鳴らないことを、複数ブラウザで確認
- 記事の Core Web Vitals を埋め込み前後で比較検証
- 「デモを開く」カードの設置
- 資料PDF(ライセンスチェックリスト・費用計算シート)の作成とダウンロード導線(F-S03)
- 元記事リンク・問い合わせ導線(EMB-10, EMB-11)
- GA4 イベント設定。
bg_playとoffline_playを必ず含める(EMB-12, EMB-14) -
?embed=1をnoindexに(EMB-21) - デモ本体の title / description / OGP(EMB-20)
- 記事側に
VideoObjectとHowToの構造化データ。MusicRecordingは使わない(EMB-23, EMB-24)
5-3. 体験の総点検
- 6章「ロールを跨ぐ体験」の6パターンをすべて手動で確認
- 全28画面を開き、空の画面が1つもないことを確認
- 実機(iOS Safari / Android Chrome)でのバックグラウンド再生とロック画面操作を再検証
- ガイドツアーを実装(FR-909)
5-4. アクセシビリティ監査
- axe-core を全画面に実行し、Critical / Serious を0件に
- 全アルバムのパレットでコントラスト比を再実測(A11Y-01, CLR-04)
- キーボードのみで全画面・全プレーヤー操作を完遂
- スクリーンリーダーで再生状態の変化が読み上げられることを確認(A11Y-05)
- フォントサイズ200%での操作確認(A11Y-11)
5-5. パフォーマンス
- Recharts を動的import に切り出し、初期バンドルを200KB以下に(NFR-08)
- 再生開始1.5秒以下、曲送り500ms以下、曲間無音50ms以下を実測(NFR-01〜03)
- ビジュアライザーの60fpsを実測(NFR-09)
- 30曲連続再生でのメモリ推移を計測(NFR-10)
- Lighthouse で Performance 90 / Accessibility 95(NFR-11)
5-6. 公開と記録
- Playwright で フローA・B・C の E2E
- Vitest:
lib/audiolib/calc_scopeのカバレッジ80%以上 -
/dev/componentsを本番で非公開に - Vercel へデプロイ、R2 の転送量アラートを設定(COST-03)
- 転送量・費用・ダウンロードによる削減効果の記録を開始(COST-01, COST-04)
- 解説動画(3分)の収録:再生 → 画面消灯でロック画面操作 → 通信を切ってオフライン再生 → 管理画面で分析
- 各フェーズのキャプチャを整理し、記事の「方法」パートの素材にまとめる
合計 22営業日(約4.5週間)/1名専任 + レビュー体制。
工数配分の意図: Phase 1 に5日を割いているのは、ギャップレス再生・クロスフェード・バックグラウンド再生・ロック画面操作が、この記事の主張(「Webでここまでできる」)そのものだから。既製ライブラリを貼れば1日で音は出るが、それでは何も証明できない。 Phase 0 の音源準備を最優先にすること。権利確認が終わらないと、1行も書けない。
15. 受入基準
- 8章の優先度 P1 の要件がすべて実装されていること
- 7章の主要フローA・B・Cが、エンドツーエンドで完走すること
- 6章「ロールを跨ぐ体験」の6パターンがすべて動作すること
- 再生ボタン押下から1.5秒以内に音が出ること
- 画面を遷移しても再生が一度も途切れないこと(
<audio>がアプリ全体で1つだけであること) - 曲間の無音が知覚できないこと(ギャップレス)。クロスフェードが設定秒数どおりに動作すること
- 実機で、画面を消しても再生が続き、ロック画面から曲名・アートワークの確認と操作ができること
- 有料会員でダウンロードした曲が、オフラインで実際に再生できること(Cache API に実ファイルが保存されていること)
- ペイウォールが動作し、加入後に停止位置から続きが再生されること
- ライセンス情報が未記入の楽曲を、公開できないこと
docs/audio-license.mdに全楽曲・全画像のライセンスが記録されていること- どのアルバムのパレットでも、テキストのコントラスト比が 4.5:1 以上であること
- 30曲を連続再生してもメモリが単調増加しないこと
- 埋め込みモードで自動再生が一切起きず、初期バンドルが60KB以下であること
- デモ中の再生が再生分析に反映され、完聴率・スキップ率が意味のある値で表示されること
- 全28画面のいずれにも空の状態がなく、リアルなコンテンツが表示されていること
- axe-core で Critical / Serious の指摘が0件であること
- データアクセスがすべて
lib/repo/を経由し、AudioContextがlib/audio/以外で触られていないこと - HIREBASE / RELATE / CASTA / FIELDPIN と並べたときに、明確に別のプロダクトとして見えること。特に CASTA と混同されないこと
判断に迷ったときのルール
- 仕様がこの文書にない場合は、実装せずに質問する。 推測で作らない
- 権利確認を後回しにしない。 ライセンスが確認できない音源は、1曲たりとも使わない
- 既製の音声ライブラリ(howler.js / Tone.js / wavesurfer.js)を導入しない。 自前実装であることがこのデモの価値
<audio>はアプリ全体で1つだけ。 画面遷移で音が途切れたら設計が間違っているAudioContextはユーザー操作起点でのみ生成する。 ページ読み込み時に作らないMediaElementAudioSourceNodeは1度だけ作る。 再生成はエラーになる- ダイナミック配色でコントラスト比を割らせない。 見た目の美しさより可読性が優先
- ビジュアライザーの
requestAnimationFrameを必ずキャンセルする。このアプリで最も起きやすい不具合はメモリリーク - スコープ外(5章 Won't have)は実装しない
- 「デモだから」を理由に品質を落とす判断はしない
- CASTA のトークン・コンポーネントをコピーしない。 同じダークテーマでも方向は真逆