マガジン

ブラウザゲームのBGMを自然にループさせる:切れ目と音量の調整

遊びをつくる

執筆にClaudeを使用しています。

つなぎ目のない銅色のリボンとテープリールで、自然なBGMループを表した写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ゲームのBGMが唐突に途切れる、繰り返しの継ぎ目でプチッとノイズが入る、タブを切り替えて戻ったら音楽が止まったまま——こうした問題は作品の印象を大きく損ないます。この記事では、ブラウザゲームのBGMをなめらかにループさせるための実装上の考え方を、Web Audio APIの仕組みを軸に整理します。作曲方法よりも「再生・停止・復帰の制御」に焦点を当てています。コード例は説明用の抜粋であり、動作確認済みの完成コードではありません。

この記事の目次
  1. なぜループの継ぎ目が聞こえるのか
  2. Web Audio APIでループ区間を指定する
  3. GainNodeで音量を滑らかに変える
  4. タブ切り替えと音声コンテキストの中断
  5. 実装の大まかな構成と確認ポイント
  6. 音のループを体験するには

なぜループの継ぎ目が聞こえるのか

HTMLの<audio>要素にloop属性を付けるだけでも音楽はループします。しかし多くの音声ファイルには末尾にわずかな無音や圧縮アーティファクトが含まれており、その部分が再生されてから先頭に戻るため、継ぎ目で一瞬の「ポーズ」が生じます。MP3は特にエンコード時に先頭・末尾にパディングが加わりやすく、ループ用途ではOGGやWAVが扱いやすいと言われます。ただし形式の選択だけで問題が解決するとは限らず、ループ点の設定と再生制御を組み合わせることが確実な対処になります。

Web Audio APIでループ区間を指定する

Web Audio APIのAudioBufferSourceNodeは、loop・loopStart・loopEndの3つのプロパティでループ区間を秒単位で指定できます。loopStartとloopEndを使うと、ファイルの先頭や末尾の余白をスキップして音楽的に正確な位置で折り返すことができます。

javascript
// 説明用の抜粋(動作確認済みコードではありません)
const ctx = new AudioContext();
const res = await fetch('bgm.ogg');
const arrayBuffer = await res.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);

const source = ctx.createBufferSource();
source.buffer = audioBuffer;
source.loop = true;
source.loopStart = 0.02;  // 先頭のパディングをスキップ(秒)
source.loopEnd = 32.00;   // 音楽的な末尾(秒)
source.connect(ctx.destination);
source.start();

loopStartとloopEndの適切な値は音声ファイルによって異なります。DAWやAudacityなどの波形編集ソフトで波形を表示し、ゼロクロッシング(振幅がゼロを横切る点)付近にループ点を設定すると継ぎ目の音が出にくくなります。ループ点の探し方は音源の構造によって変わるため、数値を決める前に実際の波形を確認することが先決です。

AudioBufferSourceNodeは一度start()を呼んだ後に再利用できません。再生のたびに新しいノードを生成する必要があります。これはAPIの仕様上の制約です。

GainNodeで音量を滑らかに変える

BGMの開始・終了時に音量を瞬時に変えると「クリックノイズ」と呼ばれる破裂音が発生します。これはサンプル値が不連続に変化するために起きる現象で、GainNodeのAudioParamメソッドを使って時間をかけて変化させることで防げます。MDNのドキュメントでは、直接値を代入するのではなくexponentialRampToValueAtTimeやlinearRampToValueAtTimeを使うことを推奨しています。

javascript
// 説明用の抜粋
const gainNode = ctx.createGain();
source.connect(gainNode);
gainNode.connect(ctx.destination);

// フェードイン(2秒かけて音量を0→1に)
gainNode.gain.setValueAtTime(0, ctx.currentTime);
gainNode.gain.linearRampToValueAtTime(1, ctx.currentTime + 2);

// フェードアウト(2秒かけて音量を1→ほぼ0に)
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 2);

exponentialRampToValueAtTimeはゼロを目標値に指定できないため、完全な無音にしたい場合は0.001など極小値を終点にした後、stop()を呼ぶパターンが一般的です。またフェードアウト中にstop()を呼ぶタイミングは、AudioContextのcurrentTimeにフェード時間を加算した値を引数として渡すことで自動的に揃えられます。

タブ切り替えと音声コンテキストの中断

ユーザーがタブを切り替えたり画面をロックしたりすると、ブラウザはAudioContextを自動的に中断することがあります。MDNのドキュメントによると、AudioContextのstateにはrunning・suspended・interrupted・closedの4種があり、ブラウザ側の外的要因による中断はinterruptedとして区別されます。suspendedはアプリ側がsuspend()を呼んだ場合で、interruptedはブラウザやOSが制御します。

タブに戻ったときに音楽を再開させるには、stateを確認してresume()を呼ぶ処理が必要です。

javascript
// 説明用の抜粋
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    if (ctx.state === 'interrupted' || ctx.state === 'suspended') {
      ctx.resume();
    }
  }
});

ただしブラウザのオートプレイポリシーにより、ユーザー操作前にAudioContextを起動するとsuspended状態になることがあります。「クリックで開始」などの最初のユーザー操作でcontext.resume()を呼ぶ設計にすると、この制約を自然に回避できます。

interruptedステートはMDNで比較的最近定義が整理された状態です。実装状況はブラウザのバージョンにより異なることがあります。動作確認は実際の環境で行ってください。

実装の大まかな構成と確認ポイント

ブラウザゲームのBGMを扱う際、最低限整理しておきたい処理の流れは次のとおりです。

  1. 音源ファイルをfetch→decodeAudioDataでAudioBufferに変換する(非同期処理なのでawait/Promiseで管理)
  2. AudioBufferSourceNodeを生成し、loopおよびloopStart・loopEndを設定する
  3. GainNodeをソースと出力先の間に接続し、フェードイン・アウトを制御する
  4. ユーザー操作のイベント内でAudioContextを起動またはresumeする
  5. visibilitychangeでタブ復帰時のresumeを処理する

実際に動作を確認するときのチェック項目としては、①ループの継ぎ目でノイズや無音がないか(別タブを開きながら繰り返し聴く)、②タブを5〜10秒離れて戻ったときに音楽が再開するか、③ゲーム開始・終了・シーン切り替えのフェードが自然に聞こえるか、④複数のBGMを切り替えるときにGainNodeが干渉していないか——の4点が基本になります。

BGMに使う音源の権利についても整理が必要です。自作音源以外を使う場合はライセンスを確認し、ゲームの公開形態(無料・収益化など)に照らして利用条件を満たしているかを確かめてください。利用規約は音源の配布元で直接確認することが原則です。

音のループを体験するには

アソバースで公開中のWAVE / WAVEは、16ステップのループを叩いて重ねていく音遊びです。BGMのループを実装する前に「繰り返しの構造がどう聞こえるか」を遊びながら確かめるのに適しています。自分のゲームにBGMを組み込む際、どの長さのループが心地よいかを試す参考にしてみてください。

Web Audio APIはブラウザごとの細かい挙動の差があり、この記事で示したコードはあくまで考え方を説明するための抜粋です。実際のゲームに組み込む際は、ターゲットとするブラウザで実際に動作確認してから公開してください。

この記事はClaudeが執筆しました。人間による取材・実機検証・校閲は行っていません。コード例の動作保証はありません。

出典・参考資料