マガジン

音の再生を忘れた状態を作る:ブラウザの自動再生制限を体験する

遊びをつくる

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

成功、失敗、選択に応じた三種類の音を、異なる形の響きで表したイラスト。
記事のテーマをもとに制作したAI生成イラスト。

タイトル画面を開いた瞬間にBGMが流れるブラウザゲームを作った場面を考えてみます。自分の環境では鳴っているのに、リンクを送った相手の画面では無音のまま始まります。コードに間違いは見当たらず、エラーも出ません。原因は、多くのブラウザが「利用者が何も操作していないページの音」を止める仕組みを持っていることにあります。 この仕組みは、実際に無音の状態を自分の手で作ってみると理解が早く進みます。この記事は結果を示す記事ではなく、実験の設計書です。最初の操作前に音が出ない状況を再現し、どの操作で解除されるのかを切り分け、ゲームの開始ボタンをどこに置くべきかを自分の環境で決められるようにします。ブラウザの種類や版、サイトごとの許可設定で挙動が変わりうるため、答えはお使いの環境で確かめてください。

この記事の目次
  1. 準備するものと、観察する問い
  2. 実験1:操作前の状態を作る
  3. 実験2:どの操作で音が解放されるかを分ける
  4. 記録の取り方と結果の読み方
  5. 起こりやすい誤りと、設計への落とし込み
  6. 次の行動
  7. 参考資料

準備するものと、観察する問い

準備するのは次のものです。

  • テキストエディタと、HTMLファイルを保存できる場所
  • ブラウザ。可能なら2種類以上、スマートフォンのブラウザもあれば理想的です
  • 音量を上げたスピーカーかヘッドホン。端末の消音スイッチやBluetooth接続の有無も控えておきます
  • 記録用のメモ。表計算ソフトでも紙でも構いません

この実験で確かめる問いは三つです。

  1. ページを開いただけの状態で、音を扱う仕組み(AudioContext)はどんな状態になっているか
  2. どの種類の操作が、その状態を「鳴らせる」状態に変えるか
  3. 操作前に鳴らそうとした音は、捨てられるのか、遅れて鳴るのか

AudioContextは、ブラウザで音を合成・再生するWeb Audio APIの中心になる部品です。stateという値を持ち、suspended(停止中)、running(動作中)、closed(終了)などを取ります。自動再生の制限がかかっているとき、作った直後のAudioContextはsuspendedになりうる、というのが一般的な説明です。ただし、必ずそうなるとは限りません。そのため、観察で確かめます。

実験1:操作前の状態を作る

次のHTMLをautoplay-test.htmlという名前で保存し、ブラウザで開きます。外部ライブラリは使いません。ページを開いた直後にbeep()を呼び、その時点の状態をログに残します。ボタンを押したときにはresume()を呼び、もう一度鳴らします。

html
<!doctype html>
<meta charset="utf-8">
<title>自動再生の実験</title>
<button id="go">音を鳴らす</button>
<pre id="log"></pre>
<script>
const out = document.getElementById("log");
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const log = (m) => {
  out.textContent += "[" + ctx.state + " / t=" +
    ctx.currentTime.toFixed(2) + "] " + m + "\n";
};
ctx.onstatechange = () => log("statechange");
log("作成直後");

function beep() {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  const t = ctx.currentTime;
  osc.frequency.value = 440;
  gain.gain.setValueAtTime(0.1, t);
  gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.4);
  osc.connect(gain).connect(ctx.destination);
  osc.start(t);
  osc.stop(t + 0.4);
}

beep();
log("操作前に beep() を呼んだ");

document.getElementById("go").addEventListener("click", async () => {
  log("クリック直後(resume前)");
  try { await ctx.resume(); } catch (e) { log("resume失敗 " + e.name); }
  log("resume完了後");
  beep();
  log("操作後に beep() を呼んだ");
});
</script>

手順は次のとおりです。

  1. 新しいタブでファイルを開き、何も触らずに10秒待ちます。マウスもキーボードも動かさず、画面もスクロールしません
  2. その間に音が聞こえたかどうかと、画面のログの内容を記録します
  3. ボタンを1回だけ押します。音が聞こえたか、ログがどう変わったかを記録します
  4. タブを閉じ、新しいタブで開き直して、手順1〜3をもう2回繰り返します。合計3回です

ログの行頭にあるt=は、AudioContextの内部時計の秒数です。この値が動いているかどうかが、音の出る状態を見分ける手がかりになります。操作前に呼んだbeep()の音が、ボタンを押した後に遅れて聞こえるかどうかも、観察項目に入れてください。

実験2:どの操作で音が解放されるかを分ける

実験1では「ボタンのクリック」という一つの操作しか試していません。次に、クリック、キー入力、タッチ、スクロール、マウスの移動を別々に試します。

実験1のファイルをコピーして、document.getElementById("go")...から始まるボタンのイベント登録部分を、次のコードに差し替えます。

js
for (const type of ["pointerdown", "keydown", "touchend", "wheel", "mousemove"]) {
  addEventListener(type, (e) => {
    log("イベント " + type + (e.key ? " " + e.key : ""));
    ctx.resume().catch(() => {});
  });
}

どのイベントでもresume()を呼ぶので、最初にrunningへ変わったイベントがどれかをログで特定できます。ここで大事なのは、1回の読み込みで確かめられる操作は一つだけという点です。一度runningになると、その後の操作では差が見えなくなります。次のように、操作ごとにページを読み込み直してください。

  • マウスの移動のみ、やること:ページ上でカーソルを動かすだけ。クリックはしない
  • スクロール(wheel)、やること:ホイールまたは2本指スクロールのみ
  • 文字キー、やること:英字キーを1回押す
  • Tab・Escなどの特殊キー、やること:1種類ずつ押す
  • クリック/タップ、やること:何もない場所を1回押す

仕様上は、操作の種類によって「利用者が能動的に操作した」とみなされるものと、みなされないものがあります。どれが該当するかは、一覧を暗記するのではなく、お使いのブラウザで確かめた記録を残すほうが確実です。スマートフォンではtouchendとpointerdownのどちらで変わるかも、別の欄に記録してください。

記録の取り方と結果の読み方

記録は1回の試行につき1行、次の項目で取ります。

  • ブラウザと版、内容:名前と、わかれば版
  • 条件、内容:何もしない/クリック/キーなど
  • 作成直後のstate、内容:suspendedかrunningか、その他か
  • 待機中のt、内容:10秒後も0.00か、進んでいるか
  • 操作後のstate、内容:変化の有無
  • 音が聞こえたか、内容:あり/なし/遅れて
  • 備考、内容:消音スイッチ、他のタブの状況など

各条件を最低3回、できれば2種類以上のブラウザで取ります。読み方の目安は次のとおりです。

  • 作成直後がsuspendedで、操作後にrunningへ変わった場合:制限が働いている典型的な状態です。ゲームの開始操作の中でresume()を呼ぶ設計にすれば解決します
  • 作成直後からrunningだった場合:その環境ではすでに許可されています。サイトごとの許可設定や、そのサイトでの過去の操作が影響している可能性があります。これだけを見て「問題なし」と判断せず、設定の異なる環境でも試してください
  • 操作してもsuspendedのままの場合:resume()が失敗していないかをログで確認します。ブラウザの設定、端末の状態、埋め込み先の制約などが考えられます。原因を一つに決めつけず、条件を一つずつ変えて切り分けます
  • 操作前に呼んだbeep()が、操作後に遅れて聞こえた場合:停止中に予約した音が、再開後に再生された可能性があります。ゲームでは意図しないタイミングの音になるので、開始前に音を予約しない設計にします
  • 操作の種類によって結果が分かれた場合:開始ボタンに使える操作と使えない操作の線引きが見えたことになります。スクロールやマウスの移動を「開始」の合図にしないほうが安全です

起こりやすい誤りと、設計への落とし込み

実験そのものを歪めやすい点を挙げます。

  • 同じタブで繰り返す:前の操作が残り、条件が揃いません。毎回、タブを閉じて開き直してください
  • 一度でも触ってから「何もしない」条件を測る:最初の10秒を待つ条件では、開く前後にもマウスやキーに触れないようにします
  • ブラウザの音声設定を確認しない:サイトごとに自動再生を許可していると、制限を再現できません。結果がおかしいときは、設定とシークレットウィンドウの両方で試してください
  • 端末側の無音を制限と取り違える:音量、消音スイッチ、出力先のデバイスが原因のこともあります。stateがrunningでtが進んでいるのに無音なら、端末側を疑います
  • file://で開いた結果だけで判断する:公開時はHTTPSで配信されます。結果が違う可能性があるため、公開に近い形でも確認してください

実験の結果を設計に戻すと、次の方針が基本になります。

  1. 音の準備(AudioContextの作成やresume())は、タイトル画面の「はじめる」ボタンなど、利用者の明確な操作の中で行う
  2. 操作より前に音を予約しない
  3. ctx.stateがrunningでなければ、画面に「音をオンにする」ボタンを出す
  4. 音がなくても遊べるようにしておく。アソバースの公式作品「こだまの森」は、音なしでも遊べる設計です。聴覚に頼る遊びでも、画面側の手がかりを用意しておけば、無音の環境でも進行が止まりません
  5. タブを切り替えて戻ったときにも、stateを確認する習慣をつける

アソバースに投稿する場合、ゲームはsandbox付きのiframeの中で実行されます。埋め込まれた枠の中でも同じ挙動になるかは、実験で確かめる価値があります。投稿は「入力→表示の確認→公開」の3段階で、HTMLを直接アップロードする方法があります。実験用のHTMLをindex.htmlとして試せるか、画面の案内で確認してみてください。確認だけなら、公開まで進める必要はありません。

次の行動

まず実験1のファイルを保存し、何もしない状態でログを見てください。次に、実験2で操作の種類を一つずつ切り分け、お使いのブラウザでの境目を表にまとめます。そのうえで、自作ゲームのタイトル画面に「はじめる」ボタンを置き、音の開始をそこへ移してみてください。

参考資料

  • MDN Web Docs「Autoplay guide for media and Web Audio APIs」
  • MDN Web Docs「AudioContext」および「BaseAudioContext.state」
  • W3C「Web Audio API」仕様
  • WHATWG「HTML Living Standard」(ユーザー活性化に関する節)