# 機能設計書 40-音楽ファイル再生

## 概要

本ドキュメントは、K-Shoot MANIAのゲームプレイにおける音楽ファイル再生機能の設計を記述する。OGG/MP3/WAV形式の音楽ファイルをBASS Audio Libraryで再生する機能（ストリーミングURL対応）である。

### 本機能の処理概要

**業務上の目的・背景**：リズムゲームにおいて音楽の再生はゲームプレイの中核をなす。譜面データに指定された音楽ファイルを正確なタイミングで再生し、ノーツ判定と同期させる必要がある。

**機能の利用シーン**：ゲームプレイ開始時に譜面ファイル（.ksh）の`m=`パラメータから音楽ファイルパスを読み取り、BASS Audio Libraryでストリームを作成する。オフセット時刻に到達した時点で再生を開始し、プレイ中はリアルタイムにタイミング同期を行う。

**主要な処理内容**：
1. 譜面ファイルの`m=`パラメータから音楽ファイルパスを解析（セミコロン区切りで複数トラック対応）
2. ローカルファイルの場合はメモリにロードし`BASS_StreamCreateFile`でストリーム作成
3. HTTP/HTTPS URLの場合は`BASS_StreamCreateURL`でネットワークストリーム作成
4. 複数トラック（通常/エフェクト付き）のリンク再生
5. オフセット時刻到達時の再生開始（`playSongIfTimeReachesOffset`）
6. 再生タイミングの同期・補正（`processPlayTiming`）
7. 複数トラック間の遅延補正（`fixPlaybackDelay`）

**関連システム・外部連携**：BASS Audio Library（bass.dll）、BASS VST プラグイン、BASS_ChannelSetLink によるマルチトラック同期。

**権限による制御**：auto変数（AUTO PLAYモード）によるトラック数制御。output変数（連番出力モード）による再生制御。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 5 | ゲームプレイ画面 | 主画面 | 音楽ファイルの再生・停止・タイミング同期 |
| 3 | 曲選択画面 | 補助画面 | 選曲BGMのフェードアウト（selenterid） |

## 機能種別

メディア再生

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| csongfile | str[] | Yes | 音楽ファイルパス配列（セミコロン区切り） | 1-4要素、各要素はファイルパスまたはHTTP(S) URL |
| offset | int | Yes | 楽曲再生オフセット（ms）。譜面の`o=`パラメータから取得 | 整数値 |
| auto | int | Yes | AUTO PLAYフラグ（0=通常, 1=AUTO） | 0または1 |
| output | int | Yes | 連番出力フラグ（0=通常, 1=出力モード） | 0または1 |
| mastervol | int | Yes | マスターボリューム（0-100） | 0-100 |
| dirvol | int | Yes | ディレクトリボリューム（0-100） | 0-100 |
| csongvol | int | Yes | 楽曲ボリューム（mvol値 * dirvol / 100） | 整数値 |

### 入力データソース

譜面ファイル（.ksh）の`m=`パラメータから音楽ファイルパスを取得。`o=`パラメータからオフセット値を取得。`mvol=`パラメータから楽曲ボリュームを取得。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| mid | int[] | BASS Audio Libraryのチャンネルハンドル配列（1-4要素） |
| mlength | int | 楽曲の長さ（ms） |
| mfl | int | 再生開始フラグ（0=未再生, 1=再生中） |
| t | int | 現在の再生時刻（ms）。判定処理・描画処理で使用 |
| rt | int | sync補正前の再生時刻（ms）。再生開始判定に使用 |
| at | int | 内部タイマーによる時刻（ms）。タイミング同期用 |
| t_mo | int | 音楽オフセット時刻（ms）。再生時刻の基準点 |
| cpos | int | 再生カーソル位置（0-200） |

### 出力先

- メモリ: 判定処理（processButtonNoteJudgment等）でのタイミング参照
- メモリ: 描画処理（drawFrame）での再生進捗表示
- オーディオデバイス: BASS Audio Libraryを通じた音声出力

## 処理フロー

### 処理シーケンス

```
1. 譜面パラメータ解析（parseKSHChartParamLines）
   ├─ m= パラメータからcsongfile配列を生成
   ├─ セミコロン区切りで最大4トラック分のファイルパスを解析
   ├─ auto=1時: csongfile.0=csongfile.1, csongfile.1=csongfile.3（エフェクト無しトラック使用）
   └─ mid配列のサイズをトラック数に応じて決定

2. ストリーム作成（parseKSHChartParamLines内）
   ├─ HTTP/HTTPS URLの場合: BASS_StreamCreateURL(ccsongfile,0,0x20000,0,0,0)
   ├─ ローカルファイルの場合:
   │   ├─ bloadでメモリにファイルデータを読み込み（mdata0-mdata3）
   │   └─ BASS_StreamCreateFile(1,mdataptr,0,0,strsize,0,0x20000)
   ├─ 1トラック目: mlength = BASS_ChannelGetLength_ms(mid.0)
   └─ 2トラック目以降: BASS_ChannelSetLink(mid.0, mid.cnt)で連動再生設定

3. 再生開始判定（playSongIfTimeReachesOffset）
   ├─ t_moex <= rt かつ mfl=0 かつ output=0 かつ mendt=-1 かつ mplayfl=1
   ├─ rt > t_moex の場合: 途中再生位置をBASS_ChannelSetPosition_msで設定
   ├─ ボリューム0.0に設定後、BASS_ChannelPlayで再生開始
   ├─ 複数トラック時: BASS_ChannelSetLink失敗時は個別にBASS_ChannelPlayで再生
   └─ mfl=1 に設定（再生中フラグ）

4. タイミング同期（processPlayTiming）
   ├─ stat1 = BASS_ChannelGetPosition_ms(mid.0) で実際の再生位置取得
   ├─ t = t_mo + stat1 で基本時刻を計算
   ├─ hggettime/hgsettimeで内部タイマー(at)との同期
   ├─ timingdif = (timingdif*7 + (t-at)) / 8 で移動平均による補正
   ├─ t = at + round(timingdif) で最終時刻を決定
   └─ t += sync でAUTO SYNC補正を適用（cnt!=0時）

5. 複数トラック遅延補正（fixPlaybackDelay）
   ├─ mid.0の再生位置を取得
   ├─ 他トラックとの差が100msを超えた場合: tadj=2（再調整フラグ）
   └─ 再調整時に全トラックの位置を同期

6. 再生終了検出
   ├─ mlength <= stat1 で楽曲終了を検出
   ├─ mendt = tp（終了時刻を記録）
   └─ endfl = 1（終了フラグ）
```

### フローチャート

```mermaid
flowchart TD
    A[parseKSHChartParamLines: m=解析] --> B{URL?}
    B -->|http/https| C[BASS_StreamCreateURL]
    B -->|ローカル| D[bload → BASS_StreamCreateFile]
    C --> E[mlength取得]
    D --> E
    E --> F{2トラック以上?}
    F -->|Yes| G[BASS_ChannelSetLink mid.0,mid.cnt]
    F -->|No| H[ストリーム準備完了]
    G --> H

    I[playSongIfTimeReachesOffset] --> J{t_moex<=rt?}
    J -->|Yes| K{mfl=0?}
    K -->|Yes| L{途中再生?}
    L -->|Yes| M[BASS_ChannelSetPosition_ms rt-t_moex]
    L -->|No| N[BASS_ChannelSetPosition_ms 0]
    M --> O[BASS_ChannelPlay]
    N --> O
    O --> P[mfl=1]
    J -->|No| Q[待機]
    K -->|No| Q

    R[processPlayTiming] --> S[stat1=BASS_ChannelGetPosition_ms]
    S --> T[t=t_mo+stat1]
    T --> U{出力モード?}
    U -->|No| V[hggettime at]
    V --> W[timingdif移動平均計算]
    W --> X[t=at+round timingdif]
    U -->|Yes| Y[t=at]
    X --> Z[t+=sync]
    Y --> Z
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-01 | マルチトラック | セミコロン区切りで最大4トラックの音楽ファイルを指定可能 | m=パラメータ |
| BR-40-02 | AUTO時トラック選択 | auto=1時は4トラック構成の場合、csongfile.1（エフェクト無し）とcsongfile.3を使用 | auto=1 かつ length(csongfile)>=4 |
| BR-40-03 | ストリーミング対応 | http/httpsで始まるURLの場合はBASS_StreamCreateURLを使用 | URLがhttp:またはhttps:で始まる |
| BR-40-04 | メモリロード | ローカルファイルはbloadでメモリに読み込んでからBASS_StreamCreateFile（mem=1）で作成 | ローカルファイル |
| BR-40-05 | トラック同期 | 2トラック以降はBASS_ChannelSetLinkで1トラック目と連動 | length(mid)>=2 |
| BR-40-06 | 遅延補正閾値 | 複数トラック間の再生位置差が100msを超えた場合に再同期 | fixPlaybackDelay |
| BR-40-07 | タイミング補正 | timingdifの移動平均（7:1比率）で内部タイマーとBASSの再生位置を同期 | disableadjtimer=0 かつ mstop=0 |
| BR-40-08 | 巻き戻し検知 | BASSの再生位置が前フレームより500ms以上後退した場合、全トラックをミュート（mrepeat=1） | stat1 < mtp-500 |
| BR-40-09 | 楽曲終了検出 | mlength <= 再生位置 で楽曲終了を検出しmendt/mendc/mendatに記録 | endtype!=0 または adjmode!=0 |

### 計算ロジック

```
// 再生時刻の計算
stat1 = BASS_ChannelGetPosition_ms(mid.0)  // BASS再生位置(ms)
t = t_mo + stat1                            // 基本時刻

// タイミング同期（移動平均）
timingdif = (timingdif * 7 + (t - at)) / 8
t = at + round(timingdif)

// sync補正（AUTO SYNC）
t += sync

// オフセット時刻
t_moex = -offset - T_MOEX_DELAY - globaloffset

// 再生カーソル位置
cpos = 200 * t / mlength

// ボリューム設定
volume = csongvol / 100 * mastervol / 100
```

## データベース操作仕様

本機能はデータベースを使用しない。

## エラー処理

### エラーケース一覧

| エラー | 発生条件 | 対応 |
|--------|---------|------|
| ファイル未存在 | exist結果がstrsize<=0 | csongfile_exist=0に設定。再生をスキップ |
| ChannelSetLink失敗 | BASS_ErrorGetCode()!=0 | mlinkfailed=1に設定。個別にBASS_ChannelPlayで再生 |
| 空ファイル名 | csongfile.0=""または".mp3" | csongfile_empty=1に設定。タイマーベースの時刻のみ使用 |

### リトライ仕様

リトライは行わない。ChannelSetLink失敗時は個別再生にフォールバック。

## トランザクション仕様

トランザクション管理は不要。

## パフォーマンス要件

- ストリーム作成は譜面読み込み時に1回実行
- processPlayTimingは毎フレーム実行（16ms間隔目安）
- ローカルファイルはメモリにプリロードしてストリーム作成（低レイテンシ）
- タイミング同期は移動平均による滑らかな補正

## セキュリティ考慮事項

特になし。

## 備考

- BASS_StreamCreateFileのフラグ0x20000はBASS_STREAM_DECODE（デコードストリーム）ではなく、BASS_SAMPLE_FLOAT（浮動小数点出力）を意味する
- mid配列のサイズはauto変数とcsongfileの要素数によって動的に変化する（1-4要素）
- mdata0/mdata1/mdata2/mdata3はメモリにロードした音楽ファイルデータを保持し、ストリーム再生中はGCされないようにスコープを維持する
- t_moex = -offset - T_MOEX_DELAY - globaloffset で、再生開始のタイミングがオフセットとグローバルオフセットで制御される
- mfl（music flag）は再生開始後1になり、以降の再再生を防止する
- output=1（連番出力モード）ではBASSによる実再生を行わず、タイマーベースの仮想時刻で処理する

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: 音楽ファイルパス解析とストリーム作成を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | play_chart_load.hsp | `src/scene/play/play_chart_load.hsp` | parseKSHChartParamLines関数内のm=パラメータ処理（393-609行目） |

**主要処理フロー**:
- **393-398行目**: `m=`パラメータの検出、セミコロンでsplitしてcsongfile配列を生成
- **399行目**: `dim mid,length(csongfile)*(auto=0)+auto` - auto=1時はmid配列サイズを1に
- **401-402行目**: 2トラック構成でcsongfile.0が存在しない場合、csongfile.1で代替
- **403-426行目**: トラック数に応じたmid/hPeakFX/hCompFX等の配列初期化とpeffect/feffect設定
- **428-433行目**: auto=1かつ4トラック以上の場合、csongfile.0=csongfile.1, csongfile.1=csongfile.3（エフェクト無しトラック使用）
- **514-517行目**: mid配列を-1で初期化
- **518-607行目**: foreachでトラック毎にストリーム作成
  - **538-556行目**: HTTP/HTTPS URLの場合、BASS_StreamCreateURLでネットワークストリーム作成
  - **562-583行目**: ローカルファイルの場合、bloadでメモリにロード後BASS_StreamCreateFile（mem=1）でストリーム作成
  - **584-585行目**: 1トラック目でmlength = BASS_ChannelGetLength_ms(mid.0)
  - **587-591行目**: 2トラック目以降でBASS_ChannelSetLinkによる連動設定

**読解のコツ**: csongfile配列の要素数が1/2/4で処理が分岐する。1トラック=モノラル、2トラック=通常+エフェクト付き、4トラック=通常+エフェクト付き+ピーク抑制版の構成。auto=1時はエフェクト無しのトラックのみ使用する。

#### Step 2: 再生開始処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | play_audio.hsp | `src/scene/play/play_audio.hsp` | playSongIfTimeReachesOffset関数（96-138行目） |

**主要処理フロー**:
- **97行目**: `t_moex<=rt`かつ`mfl=0`かつ`output=0`かつ`mendt=-1`かつ`mplayfl=1`の条件判定
- **98行目**: `csongfile_empty=0`かつ`csongfile_exist=1`のチェック
- **99-105行目**: rt > t_moex の場合、途中再生位置を設定（オフセット分を引いた位置）
- **106-112行目**: rt <= t_moex の場合、先頭から再生
- **114-119行目**: ボリューム0.0に設定（後でprocessAudioEffectsでボリュームが設定される）
- **120-128行目**: BASS_ChannelPlayで再生開始。mlinkfailed時はmid_swも個別に再生
- **129-133行目**: 2トラック構成時はBASS_ChannelUpdateで同期
- **134-135行目**: t_mo=t_moex, mfl=1 で再生状態を記録

#### Step 3: タイミング同期処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | play_audio.hsp | `src/scene/play/play_audio.hsp` | processPlayTiming関数（1-94行目） |

**主要処理フロー**:
- **3行目**: `stat1=BASS_ChannelGetPosition_ms(mid.0)` - BASSの再生位置を取得
- **4-12行目**: 再生位置が前フレームより500ms以上後退した場合の巻き戻し検出（全トラックミュート）
- **14-29行目**: mshift!=0時のシーク処理（全トラックの再生位置を同期変更）
- **30行目**: `t=t_mo+stat1` - 基本時刻の計算
- **33-43行目**: 内部タイマー(at)の取得。出力モード時は計算ベースの時刻を使用
- **44-50行目**: タイミング同期。disableadjtimer=0時は移動平均でt-at間の差分を補正
- **48行目**: `timingdif=(timingdif*7+double(t-at))/8f` - 7:1比率の移動平均
- **49行目**: `t=at+int(round(timingdif))` - 補正後の最終時刻
- **51行目**: STOP中は`t=t_mo+stoppos`で時刻を固定
- **82行目**: `t+=sync` - AUTO SYNC補正の適用

#### Step 4: 複数トラック遅延補正を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | play_audio.hsp | `src/scene/play/play_audio.hsp` | fixPlaybackDelay関数（140-148行目） |

**主要処理フロー**:
- **141行目**: `stat1=BASS_ChannelGetPosition_ms(mid.0)` - 基準トラックの再生位置取得
- **142-147行目**: 他トラックとの差が100msを超えた場合、`tadj=2`を設定して再調整を要求

#### Step 5: 呼び出し元と全体の流れを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | play_init.hsp | `src/scene/play/play_init.hsp` | start_play関数（67行目: loadKSHChartFile、71行目: parseKSHChartParamLines） |
| 5-2 | play_update.hsp | `src/scene/play/play_update.hsp` | on_update_play関数（3行目: processPlayTiming、37行目: playSongIfTimeReachesOffset） |

### プログラム呼び出し階層図

```
start_play (play_init.hsp)
    |
    +-- loadKSHChartFile (play_chart_load.hsp:254)
    |       +-- 譜面ファイル読み込み
    |
    +-- parseKSHChartParamLines (play_chart_load.hsp:307)
    |       +-- m= パラメータ解析 (393-609行目)
    |       |       +-- csongfile配列生成（split by ";"）
    |       |       +-- mid配列サイズ決定
    |       |       +-- BASS_StreamCreateURL (538-556行目) [URL時]
    |       |       +-- bload + BASS_StreamCreateFile (562-583行目) [ローカル時]
    |       |       +-- BASS_ChannelGetLength_ms → mlength (584-585行目)
    |       |       +-- BASS_ChannelSetLink (587行目) [2トラック以降]
    |       +-- o= パラメータ解析 → offset (631-633行目)
    |       +-- mvol= パラメータ解析 → csongvol (616-618行目)
    |
    +-- on_update_play (play_update.hsp:1)
            |
            +-- processPlayTiming (play_audio.hsp:1-94)
            |       +-- BASS_ChannelGetPosition_ms(mid.0) → stat1
            |       +-- t = t_mo + stat1
            |       +-- hggettime → at（内部タイマー）
            |       +-- timingdif移動平均によるタイミング同期
            |       +-- t += sync（AUTO SYNC補正）
            |
            +-- playSongIfTimeReachesOffset (play_audio.hsp:96-138)
            |       +-- t_moex <= rt で再生開始判定
            |       +-- BASS_ChannelPlay(mid.cnt)
            |       +-- mfl = 1
            |
            +-- fixPlaybackDelay (play_audio.hsp:140-148)
            |       +-- 複数トラック間の100ms超遅延を検出
            |
            +-- processLaserSlamSound (play_audio.hsp:150-193)
            |       +-- レーザースラムサウンド再生
            |
            +-- processKeySound (play_audio.hsp:195-216)
                    +-- キーサウンド再生
```

### データフロー図

```
[入力]                           [処理]                         [出力]

m=パラメータ ────────> parseKSHChartParamLines         mid[]    ──> BASSチャンネルハンドル
(csongfile)              ストリーム作成                mlength  ──> 楽曲長さ(ms)

o=パラメータ ────────> t_moex計算                      t_moex   ──> 再生開始時刻
offset                   -offset-T_MOEX_DELAY-globaloffset

BASS再生位置 ───────> processPlayTiming               t        ──> 判定・描画用時刻
内部タイマー(at) ──>   timingdif移動平均              rt       ──> 再生開始判定用時刻
sync値 ────────────>   sync補正                       cpos     ──> 再生カーソル位置

t_moex, rt ────────> playSongIfTimeReachesOffset      mfl      ──> 再生中フラグ

mid[0]再生位置 ────> fixPlaybackDelay                 tadj     ──> 再調整フラグ
mid[n]再生位置 ──>   100ms超遅延検出
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| play_audio.hsp | `src/scene/play/play_audio.hsp` | ソース | processPlayTiming/playSongIfTimeReachesOffset/fixPlaybackDelay/processLaserSlamSound/processKeySoundの実装 |
| play_chart_load.hsp | `src/scene/play/play_chart_load.hsp` | ソース | m=パラメータ解析、BASS_StreamCreateFile/BASS_StreamCreateURLによるストリーム作成（393-609行目） |
| play_init.hsp | `src/scene/play/play_init.hsp` | ソース | start_play関数でのloadKSHChartFile/parseKSHChartParamLines呼び出し |
| play_update.hsp | `src/scene/play/play_update.hsp` | ソース | on_update_playでのprocessPlayTiming/playSongIfTimeReachesOffset呼び出し（3行目、37行目） |
| play_key_input.hsp | `src/scene/play/play_key_input.hsp` | ソース | processPlaybackControlInputでの一時停止・再開時のBASS_ChannelStop/Play |
| play_audio_effects.hsp | `src/scene/play/play_audio_effects.hsp` | ソース | エフェクト適用時のボリューム制御・トラック切り替え |
| play_draw_frame.hsp | `src/scene/play/play_draw_frame.hsp` | ソース | 楽曲終了時のフェードアウト処理（BASS_ChannelSlideAttribute） |
| play_splash_screen.hsp | `src/scene/play/play_splash_screen.hsp` | ソース | プレイ前のスプラッシュ画面でのBGMフェードアウト |
