SOUZOHSPECS

一般向けアプリ ・ KANADE

音楽配信

画面を消しても止まらない、音楽配信アプリのデモ

v1.0・作成 2026-08-17

  1. 要件定義書を保存する

    パソコンに kanade-app のようなフォルダを作り、その中に保存します。

    要件定義書.md として保存

  2. ChatGPT で画面イメージを作る デザインにこだわりたい人

    ChatGPT に要件定義書を添付して、下の文を送ります。雰囲気のところは、好きな言葉に書きかえてください。

    ChatGPT に送る
    添付の要件定義書は、これから作るアプリ「音楽配信」のものです。
    このアプリのいちばん大事な画面を、デザイン画像にしてください。
    実際のアプリのように、文字やボタンまで入れてください。
    雰囲気:(例:やさしい/高級感/ポップ/シンプル など、好きな言葉で)

    気に入るまで「もっと明るく」「ボタンを大きく」のように直してもらい、できた画像を保存します。ほかの画面も欲しければ、同じように作ってもらえます。

  3. Claude Code に渡して「作って」と頼む

    Claude のデスクトップアプリで「Code」タブを開き、「Select folder」から kanade-app を選びます。画面イメージがある人は、入力欄に画像をドラッグして貼りつけてから、下の文を送ります。詳しく

    Claude Code に送る(画像あり)
    @要件定義書.md をもとに、このアプリを作ってください。
    デザインは添付の画像に合わせてください。
    Claude Code に送る(画像なし)
    @要件定義書.md をもとに、このアプリを作ってください。

    作り終わるまで、しばらくかかります。途中で「許可しますか」と聞かれたら、内容を見て許可してください。

  4. 動かして確かめる・直す

    できたら、実際に画面を開いて触ります。直してほしいところは、スクリーンショットを貼りつけて、言葉で伝えれば大丈夫です。

    Claude Code に送る
    アプリを起動して、ブラウザで開けるようにしてください。
    Claude Code に送る
    (画面名)で(操作)をすると(今の動き)になります。
    (こうなってほしい動き)にしてください。
    Claude Code に送る
    このエラーを直してください。
    
    (ここにエラーの文をそのまま貼る)
  5. 途中で止まったら 必要なときだけ

    要件定義書が長いので、一度で作りきれないことがあります。そのときは新しい会話で、こう送ります。

    Claude Code に送る
    @要件定義書.md の続きを作ってください。
    まず、どこまでできているかを確かめてから進めてください。

    それでも進まないときは、30分無料相談でお手伝いします。公式LINEからのご相談もどうぞ。

    1段階ずつ頼みたいとき(全6段階)

    要件定義書には、作る順番が Phase 0〜5 で書かれています。止まりやすいときは、1つずつ頼むと確実です。

    Phase 0基盤と音源
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 0 を作ってください。
    Phase 1プレーヤー
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 1 を作ってください。
    Phase 2ライブラリとペイウォール
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 2 を作ってください。
    Phase 3オフラインとPWA
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 3 を作ってください。
    Phase 4ビジュアライザー・歌詞・配信管理
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 4 を作ってください。
    Phase 5分析・記事統合・仕上げ
    Claude Code に送る
    @要件定義書.md の「実装タスク」にある Phase 5 を作ってください。
  6. 自分用に変える・公開する 必要な人だけ

    Claude Code に送る
    この要件定義書は、記事に埋め込むデモ用に書かれています。
    自分の会社で実際に使えるように変えたいです。外したほうがいいところと、残したほうがいいところを教えてください。
    Claude Code に送る
    このアプリをインターネットに公開したいです。
    無料でできる方法を、1手順ずつ案内してください。

準備や、うまくいかないときの対処は 作り方ガイド にまとめています。分からないときは 30分無料相談 へどうぞ。

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. プロジェクト概要
  2. 権利の壁 — この記事の核心
  3. 記事への組み込み設計
  4. アーキテクチャ方針
  5. スコープ定義
  6. ロールとデモ切替
  7. 画面一覧とユーザーフロー
  8. 機能要件
  9. データ設計
  10. デザイン要件
  11. 技術要件・ディレクトリ構成
  12. 非機能要件
  13. 費用設計
  14. 実装タスク
  15. 受入基準

1. プロジェクト概要

背景と目的

「音楽アプリを作りたい」という相談の8割は、頭の中に Spotify / Apple Music がある。しかしそれは個人や中小企業には作れない。技術の問題ではなく、権利の問題として作れない(2章)。

一方で、実際に作れて、かつ需要があるものははっきりしている。

  • 音楽教室・ボイストレーニングのレッスン音源配信
  • ヨガ・瞑想・睡眠・作業用のBGM配信
  • インディーレーベル・アーティストの自主配信プラットフォーム
  • 社内向けの研修音声・ポッドキャスト
  • 効果音・BGM素材の販売プラットフォーム

共通するのは、**「自分たちが権利を持つ音源を、会員に配信する」**という構造。KANADE はこの構造の実装デモとして作る。

目的 内容
実装力の証明 ギャップレス再生・クロスフェード・キュー管理・オフライン再生・ビジュアライザーまで実装した本物のプレーヤーを見せる
期待値の正常化 「Spotifyは作れない。作れるのはこれ」を、実物で示す。 これが最大の価値
Webの実力の証明 ロック画面での操作・バックグラウンド再生がWebでもできることを実演する
費用説明の裏付け 音源のライセンス費用という、最も見落とされるコストを説明する
リード獲得 デモ内から記事・問い合わせへの導線を常設し、GA4で計測する

プロダクト定義

項目 内容
プロダクト名 KANADE(奏)※仮称
一言定義 自社の音源を会員に配信する、サブスクリプション型の音楽配信アプリ
想定業態 音楽教室、レーベル、ヨガ・瞑想スタジオ、作業用BGM、音声メディア、素材販売
提供形態 レスポンシブWebアプリ(PWA)。フロントエンド完結(サーバー側の永続化なし)+ 静的音源配信
主戦場 スマホ。画面を消したまま聴かれる。 バックグラウンド再生とロック画面操作が成立しないと使い物にならない
想定利用者 記事の読者。登録なしで即座に再生でき、会員機能もロール切替で体験できる

プロダクトコンセプト

「聴いている間、アプリは邪魔をしない。」

音楽アプリの大半の時間、ユーザーは画面を見ていない。だからKANADEは、画面を見ていないときに強い——ロック画面で操作でき、通信が切れても止まらず、次の曲が途切れずに始まる。そして画面を見たときには、その曲の色に染まっている。

デモとしての成功条件

  1. 記事の読者が、1タップで音を出せる
  2. 画面を消しても再生が続き、ロック画面で操作できる — Webでここまでできることを体験させる
  3. 曲間が途切れない — ギャップレス/クロスフェードが効いている
  4. 画面遷移しても再生が止まらない — ミニプレーヤーが常駐している
  5. 管理側も見られる — 音源のアップロード・配信管理・再生分析まで確認できる
  6. 記事に戻れる/問い合わせできる — デモが行き止まりにならない

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_clickline_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 運営 全楽曲 + 管理画面 配信管理・ライセンス管理・分析

ロールを跨ぐ体験(必ず動くようにする)

  1. 未ログインで有料曲を再生 → 30秒でペイウォール → 有料会員に切替 → 同じ位置から続きが再生される
  2. 再生したまま画面遷移(ライブラリ→検索→プレイリスト)→ 一度も音が途切れない
  3. 再生中に画面を消すロック画面に曲名とアートワークが出て、そこから曲送りできる
  4. 有料会員でアルバムをダウンロード → 機内モードにする → オフラインで再生できる
  5. 運営で楽曲を非公開に → リスナーに切替 → ライブラリから消えている
  6. 自分が再生した曲が、運営の再生分析にカウントされ、完聴率に反映される

デモ切替バー(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.tsresolveAccess()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.tsAudioContextMediaElementAudioSourceNodeGainNodeAnalyserNode → 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.tsCache 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_playoffline_play を必ず含める(EMB-12, EMB-14)
  • ?embed=1noindex に(EMB-21)
  • デモ本体の title / description / OGP(EMB-20)
  • 記事側に VideoObjectHowTo の構造化データ。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/audio lib/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. 受入基準

  1. 8章の優先度 P1 の要件がすべて実装されていること
  2. 7章の主要フローA・B・Cが、エンドツーエンドで完走すること
  3. 6章「ロールを跨ぐ体験」の6パターンがすべて動作すること
  4. 再生ボタン押下から1.5秒以内に音が出ること
  5. 画面を遷移しても再生が一度も途切れないこと<audio> がアプリ全体で1つだけであること)
  6. 曲間の無音が知覚できないこと(ギャップレス)。クロスフェードが設定秒数どおりに動作すること
  7. 実機で、画面を消しても再生が続き、ロック画面から曲名・アートワークの確認と操作ができること
  8. 有料会員でダウンロードした曲が、オフラインで実際に再生できること(Cache API に実ファイルが保存されていること)
  9. ペイウォールが動作し、加入後に停止位置から続きが再生されること
  10. ライセンス情報が未記入の楽曲を、公開できないこと
  11. docs/audio-license.md に全楽曲・全画像のライセンスが記録されていること
  12. どのアルバムのパレットでも、テキストのコントラスト比が 4.5:1 以上であること
  13. 30曲を連続再生してもメモリが単調増加しないこと
  14. 埋め込みモードで自動再生が一切起きず、初期バンドルが60KB以下であること
  15. デモ中の再生が再生分析に反映され、完聴率・スキップ率が意味のある値で表示されること
  16. 全28画面のいずれにも空の状態がなく、リアルなコンテンツが表示されていること
  17. axe-core で Critical / Serious の指摘が0件であること
  18. データアクセスがすべて lib/repo/ を経由し、AudioContextlib/audio/ 以外で触られていないこと
  19. HIREBASE / RELATE / CASTA / FIELDPIN と並べたときに、明確に別のプロダクトとして見えること。特に CASTA と混同されないこと

判断に迷ったときのルール

  1. 仕様がこの文書にない場合は、実装せずに質問する。 推測で作らない
  2. 権利確認を後回しにしない。 ライセンスが確認できない音源は、1曲たりとも使わない
  3. 既製の音声ライブラリ(howler.js / Tone.js / wavesurfer.js)を導入しない。 自前実装であることがこのデモの価値
  4. <audio> はアプリ全体で1つだけ。 画面遷移で音が途切れたら設計が間違っている
  5. AudioContext はユーザー操作起点でのみ生成する。 ページ読み込み時に作らない
  6. MediaElementAudioSourceNode は1度だけ作る。 再生成はエラーになる
  7. ダイナミック配色でコントラスト比を割らせない。 見た目の美しさより可読性が優先
  8. ビジュアライザーの requestAnimationFrame を必ずキャンセルする。このアプリで最も起きやすい不具合はメモリリーク
  9. スコープ外(5章 Won't have)は実装しない
  10. 「デモだから」を理由に品質を落とす判断はしない
  11. CASTA のトークン・コンポーネントをコピーしない。 同じダークテーマでも方向は真逆

FREE CONSULTATION

分からないときは、お気軽に30分無料相談へ

途中で止まった、エラーが消えない、自社向けに作り変えたい。どんなことでも大丈夫です。

日程を決めて話す

代表の西澤と直接お話しできます。空いている日時を選ぶだけで予約できます。

代表 西澤と話す日程を選ぶ

まずは問い合わせる

要件定義書がほしい方・ご相談は公式LINEから。メールでのお問い合わせはフォームから。