← Back to list5. 実装ガイド(主に
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
- crossfade(fadeOut/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)、duckBGMFor、setBGMVolume、_bgmFinalVolume()。これらは触らない想定。
5. 実装ガイド(主に bgm-track.ts の HtmlBgmTrack)
- コンストラクタ/初回 play 時に、現在の
sound.context.audioContextを読んで source/gain を生成・接続:const ctx = sound.context.audioContextthis._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. 重大な注意点(実装前に必ず潰す)
- 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 可否を確認。
- AudioContext 再生成(
_recreateAudioContextImpl): 画面ロック復帰等でclose + initし context が作り替わる。旧 context に張った source/gain は無効化されるが、recreate は_bgmTrack = nullにして reconcile が新トラックを作り直すため、新トラックが新 context で張り直せば整合する。- → HtmlBgmTrack はノード生成時に必ず「その時点の」
sound.context.audioContextを読むこと(キャッシュした古い ctx 参照を使わない)。これさえ守れば recreate は自然に回復する。 - interrupted/suspended 中は gain 操作しても無音なので、既存の
_recoverAudio/_recreateAudioContext復帰フローとの整合を実機で確認。
- → HtmlBgmTrack はノード生成時に必ず「その時点の」
createMediaElementSourceは要素ごとに1回のみ。HtmlBgmTrack は track ごとにnew Audio()→destroy()なので1要素1ソースで問題ないが、同一要素を使い回さないこと。- destination への接続を忘れると無音。
- メモリ: ルーティング後も
__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.ts—HtmlBgmTrack(本作業の主対象。volume→gain)apps/client/src/lib/sound.ts—_ensureMainPlaying(crossfade。無変更想定)/_recreateAudioContextImpl/_recoverAudio/duckBGMFor/setBGMVolume/_bgmFinalVolumeapps/client/src/lib/sound-volume.ts—resolveBgmInitialVolume- 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)によって解消されている