ソースコード解説・目次

CHAPTER 18 · 全23項目

効果音と BGM の合成

発振器・音量・ノイズから音を作り、BGMを少し先の時刻へ予約します。

主な参照先:ac / beep / boom / throttled / musicTick / scheduleStep

音を作り、音量を通して出力する

  1. 発振器OscillatorNode が波形を作る
  2. 音量GainNode が減衰を付ける
  3. 出力destination へ接続

ac() は初めて必要になったときに AudioContext を作り、中断中なら再開します。クリックやキー入力の入口で呼ぶ構成です。既存の音源ファイルを再生するのではなく、波形と音量の変化から効果音を作ります。

game.html · L340–353ソースを見る ↗
function beep({ f0 = 440, f1 = f0, dur = 0.1, type = "square", vol = 0.12, delay = 0 }) {
  if (muted) return;
  try {
    const a = ac(), t = a.currentTime + delay;
    const o = a.createOscillator(), g = a.createGain();
    o.type = type;
    o.frequency.setValueAtTime(f0, t);
    o.frequency.exponentialRampToValueAtTime(Math.max(f1, 1), t + dur);
    g.gain.setValueAtTime(vol, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g).connect(a.destination);
    o.start(t); o.stop(t + dur + 0.02);
  } catch (e) {}
}

f0 から f1 へ周波数を変えると、上昇音や下降音になります。音量を短時間で小さくすることで、鳴りっぱなしの波形を射撃音などに変えます。指数的な変化では0を使わず、音量の終点を小さな正数にしています。

爆発はノイズとフィルター

ensureNoise() は1秒分のホワイトノイズを一度作って共有します。boom() はそのバッファを再生し、ローパスフィルターで高い音を削り、音量を減衰させます。発射するたびにノイズ全体を作り直さないことが負荷の削減につながります。

同じ音を重ねすぎない

効果音は鳴らすたびに発振器やフィルターなどの音声ノードを新しく作ります。ボムやレーザーで同じ瞬間に何十体も倒すと、撃破音も何十個も同時に作られ、重なった音が割れてしまいます。そこで撃破音だけは throttled() で包み、前回から40ミリ秒たっていなければ鳴らさないようにしています。

game.html · L397–407ソースを見る ↗
// 前回から ms ミリ秒たっていなければ鳴らさない効果音にする。ボムやレーザーで同じ瞬間に何体も倒すと、
// 同じ音が何十も重なって音が割れ、音声ノードも一度に大量に作られるため。
function throttled(ms, play) {
  let at = -Infinity;
  return (...args) => {
    const now = performance.now();
    if (now - at < ms) return;
    at = now;
    play(...args);
  };
}
game.html · L412–412ソースを見る ↗
  alien: throttled(40, () => { boom(0.14, 0.15, 1300); beep({ f0: 320, f1: 60, dur: 0.1, type: "sawtooth", vol: 0.08 }); }),

throttled() は、最後に鳴らした時刻 at を覚えた関数を作って返します(クロージャ)。sfx.alien() を呼ぶ側は何も変える必要がありません。撃破音は1回0.14秒ほど続くので、40ミリ秒に1回でも途切れずに聞こえます。

BGMはオーディオの時計で予約する

game.html · L487–495ソースを見る ↗
function musicTick() {
  if (!actx) return;
  while (music.next < actx.currentTime + 0.16) {
    if (music.next < actx.currentTime) music.next = actx.currentTime + 0.05;
    scheduleStep(music.step, music.next);
    music.step = (music.step + 1) % 32;
    music.next += MSTEP;
  }
}

BGMは112 BPMの8分音符を1ステップとし、32ステップで循環します。50ミリ秒ごとのタイマーは音を直接鳴らすためではなく、オーディオ時刻の0.16秒先まで予約を補充するために使います。scheduleStep() がステップ番号からベース、キック、ハイハット、旋律を選びます。

停止とミュート

ポーズやミュートでは musicStop() が予約を補充するタイマーを止めます。すでに予約済みの短い音まで一括で取り消す構造ではないため、停止直後に短い余韻が残る場合があります。ミュート設定はブラウザへ保存されます。