マガジン

音が一度しか鳴らない:再生し直せない音の扱い

遊びをつくる

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

大きなボタンに指が触れ、スピーカーへ金色の音の波が広がる立体イラスト
記事のテーマをもとに制作したAI生成イラスト。

ボタンを押すと効果音が鳴ります。ところが2回目から無音になったり、連打すると毎回音が頭で切れたりします。ブラウザゲームの制作では、こうした症状に出会う場面がよくあります。ここでは、その場面を仮定して原因を切り分けます。 疑うべき場所は大きく3つあります。再生位置(currentTime)が思った場所にないこと、play()が拒否されていること、そして1つの音声部品に「同時に2回鳴る」役割を求めていることです。この記事の中心は、最初の再生位置と、同じ音を重ねる方法の2点です。 以下では、現象を目で確かめる診断ページ、重ねるための3つの方法、投稿前に確認する環境条件の順に進めます。結果の数字や聞こえ方は書きません。読者が自分のブラウザで試し、記録して判断できる形にします。

この記事の目次
  1. 「一度しか鳴らない」を三つの症状に分ける
  2. 状態を目で見る診断ページ
  3. 同じ音を重ねる三つの方法
  4. 投稿前に確かめたい環境側の条件
  5. 公式作品を使った確かめ方
  6. 次の行動
  7. 参考資料

「一度しか鳴らない」を三つの症状に分ける

HTMLのAudio要素は、再生位置を1つだけ持つ部品です。1つの要素は「いまどこを鳴らしているか」を1か所しか表せません。この性質が、症状の大半の背景にあります。

最初に、症状を次の3つに分けて考えてみます。

  1. 再生が終わったあと、次が鳴らない。 再生位置が末尾に残っていることが疑われます。HTMLの仕様では、再生が終わった状態でplay()を呼ぶと先頭から再生し直すと定められています。ただし、途中でpause()した場合は位置が残り、続きから鳴ります。短い効果音を途中で止める処理があると、次の再生が「続き」になって一瞬で終わることがあります。
  2. 再生中にもう一度押しても重ならない。 すでに再生中の要素にplay()を呼んでも、新しい音は始まりません。連打しても1本の音が流れ続けるだけです。
  3. play()そのものが拒否されている。 play()はPromiseを返し、ブラウザの自動再生制限やファイルの読み込み失敗で拒否されることがあります。拒否を捕まえていないと、画面には何も出ず、ただ無音になります。

定番の対処は、鳴らす直前にcurrentTime = 0で先頭へ戻すことです。ただしこれは「前の音を切って頭から鳴らし直す」処理であり、重ねる処理ではありません。症状2を直したいのか、症状1を直したいのかによって、取るべき手が変わります。

状態を目で見る診断ページ

原因を推測する前に、要素の状態を画面に出します。次のHTMLは、click.mp3という短い音声ファイルを同じフォルダに置けばそのまま動きます。

html
<button id="b">鳴らす</button>
<pre id="log"></pre>
<script>
const a = new Audio("click.mp3"); // 相対パスで同梱した短い音
const log = document.getElementById("log");
function show(msg) { log.textContent = msg + "\n" + log.textContent; }
a.addEventListener("ended", () => show("ended"));
a.addEventListener("error", () => show("error code=" + (a.error && a.error.code)));
document.getElementById("b").onclick = () => {
  show("押下 time=" + a.currentTime.toFixed(2) + " paused=" + a.paused + " ended=" + a.ended);
  a.play().catch(e => show("play拒否: " + e.name));
};
</script>

このページで、次の3つの操作を順に行い、ログを書き留めます。

  • 音が鳴り終わるのを待ってから押す。 押下時のtimeとendedを記録します。
  • 音の途中で押す。 pausedがfalseのまま、新しい音が始まっているかを耳とログで確かめます。
  • 1秒間に何回か連打する。 回数は3回、5回、10回のように段階を決めて、各段階で同じ観察を繰り返します。

結果は次のように読みます。

  • 待ってから押したときにtimeが末尾付近で、音が出ないなら、再生位置の問題です。currentTime = 0を加えます。
  • 途中で押しても何も変わらないなら、症状2です。次章の「重ねる方法」に進みます。
  • play拒否の行が出るなら、再生位置とは別の問題です。名前(NotAllowedErrorなど)から、制限か読み込み失敗かを切り分けます。
  • errorの行が出るなら、ファイルの形式やパスを見直します。

誤りやすいのは、1回だけ試して結論を出すことです。再生が終わっているか、途中かで挙動が変わるため、必ず「待つ」「途中」「連打」の3通りを行ってください。

同じ音を重ねる三つの方法

重ねる方法は、考え方で大きく3つに分かれます。

  • 毎回新しい要素を作る、考え方:鳴らすたびにnew Audio(src)かcloneNode(true)、重なり:重なる、向く場面:回数が少ない効果音
  • 要素のプール、考え方:数個の要素を用意し、順番に使い回す、重なり:個数まで重なる、向く場面:連打される効果音
  • Web Audio API、考え方:音を1度デコードし、鳴らすたびに再生用の部品を作る、重なり:制限は実質ゆるい、向く場面:重なりが多い音、リズム系

要素を使い回すプール

プールは、要素を数個だけ作っておき、順番に使います。

js
const pool = Array.from({length: 4}, () => new Audio("click.mp3"));
let i = 0;
function play() { const a = pool[i]; i = (i + 1) % pool.length; a.currentTime = 0; a.play(); }

個数は音の長さと連打の速さで決めます。1つの音が0.5秒続き、1秒に8回鳴らす場面を考えると、同時に鳴る音は約4本になります。この見積もりより少ない個数では、古い音がcurrentTime = 0で切られます。個数は、診断ページの連打段階を使って、切れ方が気にならなくなる数を探してください。

毎回要素を作る方法は手軽です。ただし、読み込みが毎回走るか、キャッシュが効くかは環境によって変わります。作り捨てた要素の数が増えすぎないかも、長く遊ぶゲームでは気にしておく点です。

Web Audio APIという「使い捨て」の考え方

Web Audio APIでは、音声ファイルを1度だけデコードしてAudioBuffer(メモリ上の音データ)にします。鳴らすたびにAudioBufferSourceNodeという再生用の部品を新しく作ります。この部品はstart()を一度しか呼べない使い捨てです。

html
<button id="b">鳴らす</button>
<script>
const ctx = new AudioContext();
let buffer = null;
fetch("click.mp3")
  .then(r => r.arrayBuffer())
  .then(d => ctx.decodeAudioData(d))
  .then(b => { buffer = b; });
document.getElementById("b").onclick = () => {
  if (ctx.state === "suspended") ctx.resume();
  if (!buffer) return;
  const s = ctx.createBufferSource();
  s.buffer = buffer;
  s.connect(ctx.destination);
  s.start();
};
</script>

Audio要素は、1つの部品を巻き戻して再利用する設計です。Web Audioは、音データだけを共有し、再生の1回ごとに部品を使い捨てる設計です。「再生し直せない」という悩みは、後者の設計では起こりません。再生し直すのではなく、毎回新しく鳴らすからです。連打しても、再生位置が衝突しません。

誤りやすい点が2つあります。AudioContextは、ブラウザの制限で最初はsuspended(停止状態)のことがあります。クリックなど利用者の操作の中でresume()を呼んでください。また、start()を同じ部品に2回呼ぶとエラーになります。

投稿前に確かめたい環境側の条件

ローカルで鳴っても、公開後に鳴らないことがあります。アソバースの確認済みの案内から、関係しそうな条件を挙げます。

  • ゲームはsandbox付きのiframeで実行され、アップロードしたコードはメインサイトと別のオリジンから配信されます。 音の再生開始は、画面を押す、キーを押すといった利用者の操作の中で行うのが安全です。ページ表示直後の自動再生は、ブラウザの制限で拒否されやすい場面です。
  • 音は相対パスで同梱します。 入口は直下のindex.htmlです。ZIPは1ファイル30MiB以内、展開後は合計100MiB以内の制限があります。長いBGMを非圧縮で入れると、容量を圧迫します。
  • サーバー用コードやサービスワーカーは対象外です。 音を先読みするためにサービスワーカーでキャッシュする方法は使えません。先読みはページ内のコードで行います。

Web Audioの例はfetchで同梱ファイルを読みます。この読み込みが、sandbox付きiframeの中で通るかは、この記事の根拠にできる確認済み事項にありません。投稿の「表示の確認」の段階で、実際に鳴るかを確かめてください。心配なら、音をBase64形式の文字列としてindex.htmlの中に埋め込み、fetchを使わずにデコードする手もあります。この場合もファイルが大きくなるため、短い効果音に限ると考えるのがよいでしょう。

公式作品を使った確かめ方

公式作品は、重ね鳴らしの条件が表れる題材です。ただし、内部でAudio要素とWeb Audioのどちらを使っているかは確認済み事項にありません。ここでは、設計から読み取れることと、作品を開いて確かめる点を分けます。

「雨音のピアノ」は、フェルトとガラスの2音色、3段階のタッチ、余韻の切り替えを備え、キーボードのA W S E D F T G Y H U Jで鳴らせます。複数の鍵を続けて押したり、同じ鍵を素早く繰り返したりする遊びが想定されていると読み取れます。同じ音が重なる条件を、自然に試せる題材です。

確かめ方の例は次のとおりです。

  • 同じ鍵を、遅い間隔と速い間隔で繰り返し押します。
  • 余韻を切り替え、前の音が残るか、次の音で置き換わるかを聞き分けます。
  • 結果を「重なる」「切れる」「鳴らない」の3分類で、段階ごとに記録します。

「WAVE / WAVE」は、4つの音を16ステップに並べてループ再生するリズムスタジオです。複数のトラックが同じタイミングで鳴る設計から、同時に鳴る音を扱う仕組みが前提になっていると読み取れます。ミュートやテンポの変更をしたときに、音が途切れないかを聞くと、自分の作品の目標も具体的になります。

どの仕組みで実装されているかは、この記事では断定しません。自分の作品づくりでは、「連打に耐えるか」「同時に何本鳴るか」を数えた基準を、公式作品の聞こえ方と比べて決めてください。

次の行動

まず診断ページを作り、「待つ」「途中」「連打」の3通りでログを取ってください。症状が再生位置ならcurrentTime = 0、重なりが必要ならプール、重なりが多いならWeb Audioと、手を選びます。投稿前の表示確認では、最初のクリックで鳴ること、連打しても止まらないことを、必ず実機で確かめてください。

参考資料

  • MDN Web Docs「HTMLMediaElement」
  • MDN Web Docs「Web Audio API」
  • WHATWG HTML Living Standard「Media elements」
  • 各ブラウザの自動再生ポリシーに関する公式ドキュメント