ブラウザゲームで音が出ない:ユーザー操作とAudioContextの再開
遊びをつくる執筆にClaudeを使用しています。

ブラウザゲームに効果音やBGMを実装したのに、実際にプレイすると無音のまま——そんな報告は開発者のフォーラムで繰り返し登場します。コードに誤りはなく、ローカルでは鳴っていたのに、公開後に「音が出ない」と言われる。原因のほとんどはオートプレイポリシー(Autoplay Policy)です。 現代のブラウザは、ページ読み込み直後に音を自動再生することを制限しています。ユーザーがそのページ上で何らかの操作(クリック・タップ・キー入力など)を行う前に AudioContext を起動しようとすると、ブラウザはその要求を黙って保留します。エラーは出ないまま音だけが出ない、という状況が生まれます。この記事では、なぜそうなるのかを整理し、正しく音を鳴らすための実装パターンを具体的に説明します。
なぜ「ユーザー操作」が必要なのか
ブラウザのオートプレイ制限は、訪問者の意図しない音の再生を防ぐために設けられています。Chrome・Firefox・Safari・Edgeのいずれも、この方針に沿った挙動をとります。細かいルールはブラウザごとに異なりますが、共通する原則は「ユーザーのジェスチャー(gesture)が発生したあとでなければ、音声系のAPIを起動してはならない」というものです。
Web Audio APIの中心にある AudioContext は、生成した直後は suspended(停止)状態です。ユーザー操作を経てはじめて running 状態に遷移できます。AudioContext を new AudioContext() で生成しただけで音を鳴らそうとしても、コンテキストが停止したままなので音は出ません。<audio> タグや HTMLAudioElement の .play() も同様で、操作前に呼ぶと Promise が rejected になるか、再生が無視されます。
ゲーム開発でよくある失敗パターンは次の二つです。
- window.onload や DOMContentLoaded のタイミングで AudioContext を生成・起動しようとする
- ゲームエンジンの初期化処理の中で音声コンテキストを即時起動するコードが含まれており、それがページロード直後に走っている
どちらも「ユーザーがまだ何もしていない」段階で音声を起動しようとするため、ブラウザに止められます。
AudioContextの状態を確認する
問題を診断するには、まず AudioContext の現在の状態を確認します。
const ctx = new AudioContext();
console.log(ctx.state); // "suspended" または "running"公開済みのゲームで音が出ないと報告されたとき、ブラウザの開発者ツール(コンソール)でこの値を確認するか、ゲーム内に状態表示を仕込んでおくと原因の切り分けが速くなります。suspended のままであれば、resume() が呼ばれていないか、呼ばれるべきタイミングで呼ばれていないことが分かります。
Safariは挙動がやや異なり、AudioContext を生成した瞬間から suspended になります。ChromeはEngagement(サイトへの関与度)が高いと判定したページでは自動起動を許可することがありますが、新規訪問のゲームページでは期待しないほうが安全です。「ローカルでは鳴っていた」というのも、開発中に何度もリロードしてサイトへの関与度が上がっていたためにChromiumが例外扱いしていた、というケースが多くあります。
正しい再開パターンと実装手順
基本的な考え方は「最初のユーザー操作イベントのハンドラ内で AudioContext.resume() を呼ぶ」です。以下にシンプルな実装を示します。
パターン1:スタートボタンで一度だけ再開する
ゲームにスタート画面やタイトル画面がある場合、そのボタンのクリックイベントで音声を初期化するのが最も確実な方法です。
const ctx = new AudioContext();
document.getElementById('start-button').addEventListener('click', () => {
if (ctx.state === 'suspended') {
ctx.resume().then(() => {
// ここから音声を使う処理を始める
startGame();
});
} else {
startGame();
}
});resume() は Promise を返すので、.then() の中に音声を使う処理を入れることで、コンテキストが確実に running になってから再生が始まります。
パターン2:最初の任意操作で再開する(スタート画面がない場合)
スタート画面を設けず、ゲームが即座に始まる構成の場合は、click・keydown・touchstart のどれかを一度だけ捕捉して再開します。
const ctx = new AudioContext();
function resumeAudioOnce() {
if (ctx.state === 'suspended') {
ctx.resume();
}
document.removeEventListener('click', resumeAudioOnce);
document.removeEventListener('keydown', resumeAudioOnce);
document.removeEventListener('touchstart', resumeAudioOnce);
}
document.addEventListener('click', resumeAudioOnce);
document.addEventListener('keydown', resumeAudioOnce);
document.addEventListener('touchstart', resumeAudioOnce);removeEventListener でリスナーを一度だけ動作させることが重要です。毎回のイベントで resume() が呼ばれても動作上は問題ありませんが、意図が明確になり処理の無駄を減らせます。
パターン3:`<audio>` タグ・HTMLAudioElementを使う場合
Web Audio APIを使わず HTMLAudioElement でBGMを流す構成でも原則は同じです。
const bgm = new Audio('bgm.mp3');
document.getElementById('start-button').addEventListener('click', () => {
bgm.play().catch(err => {
// ここに来た場合は操作前に呼んでいる可能性がある
console.warn('再生できませんでした:', err);
});
});.play() が返す Promise に .catch() を付けることで、失敗したときの原因がコンソールで確認できます。.catch() を付けていないと、エラーが出ても気づかないまま「音が出ない」状態になります。
よくある落とし穴と確認ポイント
BGMのループ再生が途中で止まるケース:タブをバックグラウンドに切り替えたとき、ブラウザが AudioContext を自動的に suspended に戻すことがあります。タブに戻ったとき(visibilitychange イベント)に resume() を呼ぶ処理を追加すると対応できます。
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && ctx.state === 'suspended') {
ctx.resume();
}
});ゲームエンジンやライブラリを使っている場合:Phaser・PixiJS・Three.jsなど多くのライブラリは独自の音声管理を持っています。これらのライブラリが内部で生成している AudioContext を直接 resume() するのではなく、ライブラリが提供する「ユーザー操作後に呼ぶ初期化API」を使うほうが安全です。公式ドキュメントの「Autoplay」や「Audio unlock」に関するセクションを確認してください。
iOSのSafariは追加の制約がある:iOS Safariは、ユーザーのタップイベントのコールスタック上(同期的な処理の中)で .play() や .resume() を呼ぶ必要があります。非同期処理(setTimeout や fetch の後など)でこれらを呼ぶと、制約に引っかかる場合があります。スタートボタンのクリックイベントハンドラの中で直接呼ぶ、という構造を守ることが重要です。
音声ファイルの読み込みタイミング:AudioContext が running になっていても、音声ファイルのデコードが終わっていなければ再生できません。decodeAudioData を使う場合はデコード完了を待ってから再生処理を実行します。ユーザー操作が来る前にデコードだけ済ませておき、操作後に再生だけ行う、という分業が有効です。
音が出ないときの診断チェックリスト
問題を順序よく切り分けるために、以下の順で確認します。
- ブラウザの開発者ツールのコンソールに AudioContext に関する警告("was not allowed to start"など)が出ていないか確認する
- ctx.state を出力し、suspended のまま変わっていないか確認する
- resume() を呼んでいる箇所が、ユーザー操作イベントのハンドラ内にあるかを確認する(非同期処理の後になっていないか)
- iOSでテストしている場合、タップイベントの同期コールスタック上で呼んでいるかを確認する
- ライブラリを使っている場合、そのライブラリ固有のアンロック処理を使っているかを確認する
次の行動
手元のゲームで音が出ない場合は、まずブラウザの開発者ツールのコンソールで AudioContext の状態警告を確認し、スタートボタンのクリックイベント内に resume() を移動してみてください。それでも解決しない場合は、使用しているライブラリの公式ドキュメントで「Autoplay Policy」または「Audio unlock」の項目を参照してください。修正したゲームはHTMLファイルまたはZIPファイルの形式でアソバースに投稿し、実機のスマートフォンを含む複数の環境で動作確認することをお勧めします。
参考資料
- Web Audio API(MDN Web Docs)
- Autoplay Policy Changes(Google Developers)
- HTMLMediaElement.play()(MDN Web Docs)
- WebKit Autoplay Policy(WebKit Blog)

