Linear ArchiveArchived issues viewer
← Back to list
INS-894

iOS で BGM(HtmlBgmTrack)の音量制御が効かない(crossfade/fade/duck が無効・楽曲が重なる)

StatusDone
TeamInstansys
Assigneeasuki.uehata@instansys.co.jp
PriorityMedium
Created2026/06/26 06:56
Completed2026/07/01 07:08
Archived2026/07/09 00:50
Bug

Description

症状

iOS 実機で BGM 切替時に crossfade(fadeOut/fadeIn)が機能せず、旧曲と新曲が

約1秒間フル音量で重なって聞こえる。fade も段階的にならず、被りが目立つ。

1. 背景・コンテキスト

  • 前提 PR: INS-887(BGM を HtmlBgmTrack/HTMLAudio ストリーミングに一本化し PCM 常駐メモリを解消)。本作業はこの上に乗る。INS-887 マージ後に develop からブランチを切ること。
  • ブランチ名案: fix/INS-XXX_ios_bgm_gain_volume
  • 関連: メモリ削減(INS-887)は維持必須=AudioBuffer 化(全尺PCM デコード)に戻してはいけない。MediaElementSource は streaming なので維持できる。

2. 確定した根本原因

iOS WKWebView / Safari は HTMLAudioElement.volume への代入を無視する(iOS では volume は読み取り専用=ハードウェア制御)。

  • 実機検証済み: __sm._bgmTrack._el.volume = 0.1音量に一切変化なし(返り値は 0.1 だが実音量不変)。
  • 影響: <audio>.volume ベースの制御がすべて iOS で no-op:
    • crossfade(fadeOut/fadeIn, _crossfadeDuration=1000ms)→ 旧/新トラックが全音量で約1秒重なる
    • setBGMVolume(設定スライダー)
    • duckBGMFor(クリアSE 中の減衰)
    • 起動/復帰時 fadeIn
  • 旧実装(@pixi/sound = WebAudio)は GainNode で制御していたので iOS でも効いた。HtmlBgmTrack 移行で <audio>.volume 制御になり退行。

3. ゴール(案A)

HtmlBgmTrack<audio> を WebAudio グラフに通す:

MediaElementAudioSourceNode(el) → GainNode → audioCtx.destination

音量は el.volume ではなく gainNode.gain で制御する(iOS でも効く)。streaming のままなのでメモリ削減は維持

4. なぜ低リスクか(変更の局所性)

  • crossfade/fade/duck/スライダーのステートマシンは SoundManager 側にあり、すべて track.volume = x(HtmlBgmTrack の volume setter)経由で操作している。
  • したがって HtmlBgmTrack の volume getter/setter を gainNode.gain に張り替えるだけで、SoundManager 側の _ensureMainPlaying の token/crossfade ロジックは無変更で iOS でも効くようになる。これが案A の肝。
  • 参照: sound.ts:_ensureMainPlaying(fadeOut _bgmFadeOutTimer / fadeIn _bgmFadeInTimer)、duckBGMForsetBGMVolume_bgmFinalVolume()。これらは触らない想定。

5. 実装ガイド(主に bgm-track.tsHtmlBgmTrack

  • コンストラクタ/初回 play 時に、現在の sound.context.audioContext を読んで source/gain を生成・接続:
    • const ctx = sound.context.audioContext
    • this._source = ctx.createMediaElementSource(this._el)
    • this._gain = ctx.createGain()
    • this._source.connect(this._gain).connect(ctx.destination)
  • get volume()/set volume()this._gain.gain.value(0..1 クランプ)に張り替える。el.volume はもう使わない(iOS no-op のため)。
  • destroy(): el の停止/解放に加え、source.disconnect() / gain.disconnect() を必ず行う(ノードリーク防止)。
  • pause()/play()/isPlaying は従来どおり el ベースで可。

6. 重大な注意点(実装前に必ず潰す)

  1. CORS(最重要): createMediaElementSource を通すと、グラフが要素の音を読むためメディアが CORS クリーンでないと出力が無音(tainted)になる
    • 資産パス BGM(home/gacha/caravan/result/章/title)= objectURL Blob = same-origin → OK
    • 直 URL BGM(Luna / シナリオの CDN URL)が問題。現在 HtmlBgmTrack は意図的に crossOrigin を設定していない(「CDN が CORS ヘッダを返さない場合に再生が壊れるのを避ける」)。GainNode 経由にすると、直 URL は crossOrigin="anonymous" + CDN 側 CORS ヘッダが必須になる。
    • 方針判断が要る: (a) 直 URL BGM の CDN に CORS を付与して全部 GainNode 経由、(b) 直 URL は従来どおり <audio> 直結(GainNode 通さない)にして iOS では fade 無しにする、のどちらか。実装前に CDN の CORS 可否を確認。
  2. AudioContext 再生成(_recreateAudioContextImpl): 画面ロック復帰等で close + init し context が作り替わる。旧 context に張った source/gain は無効化されるが、recreate は _bgmTrack = null にして reconcile が新トラックを作り直すため、新トラックが新 context で張り直せば整合する。
    • HtmlBgmTrack はノード生成時に必ず「その時点の」sound.context.audioContext を読むこと(キャッシュした古い ctx 参照を使わない)。これさえ守れば recreate は自然に回復する。
    • interrupted/suspended 中は gain 操作しても無音なので、既存の _recoverAudio / _recreateAudioContext 復帰フローとの整合を実機で確認。
  3. createMediaElementSource は要素ごとに1回のみ。HtmlBgmTrack は track ごとに new Audio()destroy() なので1要素1ソースで問題ないが、同一要素を使い回さないこと。
  4. destination への接続を忘れると無音
  5. メモリ: ルーティング後も __audioMem() に BGM の AudioBuffer が出ないこと(MediaElementSource は full PCM デコードしない)を確認=INS-887 の削減維持。

7. フォールバック(案B)

案A が直 URL の CORS で詰む / recreate 統合が重すぎる経路があれば、その経路だけ iOS ではハードカット(crossfade 無し) に退避。被りは消えるが fade 無し。

8. 検証計画(iOS 実機必須)

  • 未完了__sm._bgmTrack._gain.gain.value = 0.1(実装後)で実音量が下がること。
  • 未完了BGM 切替で crossfade が聞こえる(被り解消、fadeOut/fadeIn が効く)。
  • 未完了設定スライダー / duck が iOS で反映される。
  • 未完了直 URL BGM(Luna ホーム BGM / シナリオ BGM)が鳴ること(CORS で無音化していない)。
  • 未完了画面ロック→復帰、バックグラウンド→復帰で BGM が正しい音量で再開(context 再生成後の張り直し)。
  • 未完了__audioMem() に BGM AudioBuffer が出ない(メモリ削減維持)。__bgmSizes() の Blob は従来どおり。
  • 未完了Android / Web で回帰なし(createMediaElementSource 二重呼び出しエラーが出ない等)。

9. 関連ファイル / デバッグ手段

  • apps/client/src/lib/bgm-track.tsHtmlBgmTrack(本作業の主対象。volume→gain)
  • apps/client/src/lib/sound.ts_ensureMainPlaying(crossfade。無変更想定)/ _recreateAudioContextImpl / _recoverAudio / duckBGMFor / setBGMVolume / _bgmFinalVolume
  • apps/client/src/lib/sound-volume.tsresolveBgmInitialVolume
  • AudioContext: sound.context.audioContext(@pixi/sound 共有。新規生成しない)
  • デバッグ(dev/staging 公開): window.__sm / window.__bgmCache / コンソール関数 __bgmTracks() __bgmSizes() __audioMem()

10. 実装者への申し送り

  • まず CDN CORS の可否を確認 → 直 URL BGM を GainNode 経由にできるか(案A 完全形)/フォールバックが要るか(案B 併用)を最初に決める。これがアーキの分岐点。
  • SoundManager の crossfade/token ロジックは触らないのが原則(局所変更で済むのが案A の利点)。volume の「操作面」だけ gain に差し替える。

Comments (1)

asuki.uehata@instansys.co.jp2026/07/01 07:08

fix/INS-892_BGM_volume にて、論理的に副次的に改善した。 ** バトル→ホームの経路では crossFade(同時再生)は起きない が、それは意図どおりで、BGM の重なりもありません

  • あなたの修正(HtmlBgmTrack + token 管理)は、crossFade が必要なシーン切り替え(例:ホーム BGM 切り替え)でも正しく動作する設計になっています
  • develop で改善されていた「重なる問題」は、この修正ブランチ(_bgmOldTrack の確実な破棄 + token preempt)によって解消されている