[JavaScript] 効果音はBEEPで十分!Web Audioで作るゲームサウンド

1. はじめに

効果音を鳴らすとき、多くの人は MP3やWAVといった音源ファイル を用意します。 しかし実は、ブラウザだけで完結する方法があります。Web Audio API を使えば、数行のJavaScriptコードで「ピッ」「チャイム」「コイン音」などの効果音を生成できるのです。

これは、かつてのパソコン文化にあった BEEP音 に通じるものがあります。 音源ファイルがなくても、プログラムが直接「音を描く」――そんなBEEP文化を、ブラウザ上で現代風に再現できるのです。

この記事では、最小の「beep関数」から始めて、波形やエンベロープ、ビブラート、トレモロなどを組み合わせ、ゲームや通知に使える BEEPハッカー的効果音生成術 を紹介します。

2. 最小サンプル

まずは最小の beep() 関数から始めましょう。
Web Audio API では AudioContext を作り、そこにオシレーター(発振器)を接続することで音を出せます。以下は「サイン波 880Hz を0.2秒だけ鳴らす」最小サンプルです。

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>beep sample</title></head>
<body>
  <button id="beepBtn">beep!</button>

  <script>
    let audioCtx;
    let unlocked = false;

    // ユーザー操作でAudioContextを解錠
    window.addEventListener("click", () => {
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      if (audioCtx.state === "suspended") audioCtx.resume();
      unlocked = true;
    }, { once: true });

    function beep(freq = 880, dur = 0.2) {
      if (!unlocked || !audioCtx) return;

      const osc = audioCtx.createOscillator(); // 発振器
      const g = audioCtx.createGain();         // 音量コントロール

      osc.type = "sine";           // 波形: サイン波
      osc.frequency.value = freq;  // 周波数: 880Hz

      osc.connect(g);
      g.connect(audioCtx.destination);

      const t = audioCtx.currentTime;
      g.gain.setValueAtTime(0.1, t);   // 音量
      osc.start(t);
      osc.stop(t + dur);               // dur 秒後に停止
    }

    document.getElementById("beepBtn").onclick = () => beep();
  </script>
</body>
</html>

ブラウザでこのコードを開き、ボタンをクリックすると「ピッ」と鳴ります。
ここでは オーディオファイルを一切使っていない のがポイントです。

3. 応用テク

最小サンプルに、波形・ADSR・グライド・ビブラート/トレモロ・倍音を足していくと、MP3なしでも“それっぽい”効果音が作れます。 まずは使い回しできる 高機能 beep() を用意しておくと便利です。

3.1 汎用シンセ版 beep()(コピペOK)

前セクションの audioCtx/「クリックで解錠」処理は流用してください。

<script>
function beep({
  freq = 880,         // 基本周波数(Hz)
  dur = 0.25,         // 全体長さ(秒)
  vol = 0.12,         // 音量(0〜1)
  wave = "triangle",  // "sine" | "square" | "sawtooth" | "triangle"
  attack = 0.005,     // ADSR(秒)
  decay = 0.05,
  sustain = 0.5,      // 0〜1
  release = 0.1,
  glideTo = null,     // 最後に向けてピッチを滑らせる(Hz)
  vibratoHz = 0,      // ビブラート周波数(Hz)
  vibratoDepth = 0,   // ビブラート深さ(Hz)
  tremoloHz = 0,      // トレモロ周波数(Hz)
  tremoloDepth = 0,   // トレモロ深さ(0〜1)
  harmonics = [],     // 例: [{ratio:1.5, gain:0.35}, {ratio:2, gain:0.2}]
} = {}) {
  if (!audioCtx) return;

  const t0 = audioCtx.currentTime;
  const t1 = t0 + dur;

  // マスター出力
  const master = audioCtx.createGain();
  master.gain.value = 1;
  master.connect(audioCtx.destination);

  // トレモロ(音量揺らぎ)
  if (tremoloHz > 0 && tremoloDepth > 0) {
    const tremOsc = audioCtx.createOscillator();
    tremOsc.type = "sine";
    tremOsc.frequency.value = tremoloHz;
    const tremGain = audioCtx.createGain();
    tremGain.gain.value = tremoloDepth;
    tremOsc.connect(tremGain).connect(master.gain);
    // バイアス(最小音量を確保)
    master.gain.value = 1 - tremoloDepth;
    tremOsc.start(t0);
    tremOsc.stop(t1 + release + 0.05);
  }

  // 1音を生成する小関数(基本波/倍音で使い回す)
  const makeVoice = (baseFreq, gainMul = 1) => {
    const osc = audioCtx.createOscillator();
    osc.type = wave;

    const amp = audioCtx.createGain();
    amp.gain.value = 0;

    osc.connect(amp).connect(master);

    // ピッチ
    osc.frequency.setValueAtTime(baseFreq, t0);
    if (glideTo && glideTo > 0) {
      // 自然な感じに指数ランプ
      osc.frequency.exponentialRampToValueAtTime(glideTo, t1);
    }

    // ビブラート(周波数揺らぎ)
    if (vibratoHz > 0 && vibratoDepth > 0) {
      const vOsc = audioCtx.createOscillator();
      vOsc.type = "sine";
      vOsc.frequency.value = vibratoHz;
      const vGain = audioCtx.createGain();
      vGain.gain.value = vibratoDepth;
      vOsc.connect(vGain).connect(osc.frequency);
      vOsc.start(t0);
      vOsc.stop(t1 + release + 0.05);
    }

    // ADSRエンベロープ
    const peak = Math.max(0.0001, vol * gainMul);
    amp.gain.setValueAtTime(0, t0);
    amp.gain.linearRampToValueAtTime(peak, t0 + attack);
    amp.gain.linearRampToValueAtTime(peak * sustain, t0 + attack + decay);
    amp.gain.setValueAtTime(peak * sustain, t1);
    amp.gain.exponentialRampToValueAtTime(0.0001, t1 + release);

    osc.start(t0);
    osc.stop(t1 + release + 0.05);
  };

  // 基本波
  makeVoice(freq, 1.0);

  // 倍音(和音/オーバートーン)
  for (const h of harmonics) {
    const ratio = Math.max(0.01, h.ratio || 2);
    const gain  = Math.max(0, h.gain ?? 0.3);
    makeVoice(freq * ratio, gain);
  }
}
</script>

3.2 波形切り替え(sine / square / sawtooth / triangle)

beep({ wave: "sine" });      // 柔らかい
beep({ wave: "square" });    // レトロ/主張強め
beep({ wave: "sawtooth" });  // 明るく鋭い
beep({ wave: "triangle" });  // ほどよく丸い(通知向け)

3.3 ADSRエンベロープ(立ち上がりと余韻)

// 短くキレ良く(クリック音寄り)
beep({ attack: 0.002, decay: 0.03, sustain: 0.2, release: 0.06 });

// 余韻のあるチャイム
beep({ wave: "sine", attack: 0.005, decay: 0.12, sustain: 0.7, release: 0.3, vol: 0.1 });

3.4 glide(ポルタメント:上昇/下降チュイーン)

// 上昇チャイム(通知っぽい)
beep({ freq: 660, glideTo: 990, dur: 0.18, wave: "triangle" });

// 下降=エラーっぽい
beep({ freq: 880, glideTo: 440, dur: 0.25, wave: "square", sustain: 0.3, release: 0.1 });

3.5 vibrato / tremolo(揺らぎで“生っぽさ”)

// ビブラート(音程が揺れる)
beep({ freq: 880, vibratoHz: 7, vibratoDepth: 6, dur: 0.4, wave: "sine" });

// トレモロ(音量が揺れる)
beep({ freq: 660, tremoloHz: 12, tremoloDepth: 0.4, dur: 0.35, wave: "square" });

// 両方
beep({ freq: 784, vibratoHz: 6, vibratoDepth: 5, tremoloHz: 8, tremoloDepth: 0.3, dur: 0.45 });

3.6 harmonics(倍音でリッチ化・コイン/ベル風)

// コイン音:短い/倍音強め
beep({
  freq: 1200, dur: 0.12, wave: "square", attack: 0.002, decay: 0.03, sustain: 0.2, release: 0.06,
  harmonics: [{ ratio: 2, gain: 0.25 }, { ratio: 3, gain: 0.18 }]
});

// ベルっぽい:三度・五度を薄く重ねる
beep({
  freq: 660, dur: 0.28, wave: "triangle", release: 0.2, vol: 0.11,
  harmonics: [{ ratio: 1.25, gain: 0.25 }, { ratio: 1.5, gain: 0.2 }]
});

運用ヒント

  • 効果音は 50〜250ms が使いやすい(長すぎるとUXが重く感じる)
  • うるい時は vol だけでなく release を短くして切り上げる
  • 倍音の gain は 0.15〜0.35 程度に抑えると上品

4. 実運用でのTips

現場投入でハマりがちなポイントと対策をまとめます。

4.1 Chromeのオートプレイ制限(解錠が必要)

ブラウザ(特にChrome/Edge)は ユーザー操作前の再生をブロック します。 最初のクリックやキー入力で AudioContext解錠 しておきましょう。

<script>
let audioCtx, audioUnlocked = false;

function unlockAudio() {
  if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  if (audioCtx.state === "suspended") audioCtx.resume();
  audioUnlocked = true;
  // 確認音(短く)
  try { beep({ freq: 660, dur: 0.05, vol: 0.08 }); } catch {}
}

// どれか1回でOK
["pointerdown","keydown","touchstart"].forEach(ev=>{
  window.addEventListener(ev, ()=>{ if (!audioUnlocked) unlockAudio(); }, { once:true, passive:true });
});
</script>

「テスト音」ボタンを1つ置くと、ユーザーが押した時点で確実に解錠できます。

4.2 効果音トグルと音量管理

「通知は欲しいけど音はイヤ」という状況に備えて、トグルと音量はUIで制御できるように。

<label><input id="soundToggle" type="checkbox" checked /> 効果音</label>
<input id="volume" type="range" min="0" max="1" step="0.01" value="0.12" />
<script>
const soundToggle = document.getElementById("soundToggle");
const volInput    = document.getElementById("volume");

// 高機能beepの vol を差し替えるだけ
function sfxNotify() {
  if (!soundToggle.checked) return;
  beep({ freq: 660, glideTo: 990, dur: 0.18, wave: "triangle",
         harmonics: [{ratio:1.5, gain:0.35}], vol: parseFloat(volInput.value) });
}
</script>

ポイント: うるさい場合は vol だけでなく release を短く すると耳に優しい。

4.3 「新着」と組み合わせる(既読キャッシュ)

ログ監視・通知UIでは、既読(seen) をローカルに持って「未既読だけ鳴らす」のが定番です。

const seen = new Set(JSON.parse(localStorage.getItem("seenIds") || "[]"));

function isNew(id) {
  if (seen.has(id)) return false;
  seen.add(id);
  localStorage.setItem("seenIds", JSON.stringify([...seen].slice(-5000)));
  return true;
}

// 例:フェッチ後の処理
items.forEach((rec) => {
  if (isNew(rec.id)) {
    // ここで音を鳴らす
    sfxNotify();
    // 併せてWeb通知するなら… notifyNew(rec)
  }
});

ログを削除・復元したときの“巻き戻り”対策として、最大タイムスタンプ(lastTs)で検知して既読をリセットするのが実用的。

const lastTs = JSON.parse(localStorage.getItem("lastTs") || "{}");

function onBatchFinished(sourceKey, maxTs) {
  if ((lastTs[sourceKey] || 0) > maxTs) {
    // ログ巻き戻り検知 → 既読リセット
    seen.clear();
    localStorage.removeItem("seenIds");
  }
  lastTs[sourceKey] = Math.max(lastTs[sourceKey] || 0, maxTs);
  localStorage.setItem("lastTs", JSON.stringify(lastTs));
}

別案: id に #${ts} を付けて 同URLでも時刻更新で新着扱い にするのも簡単で強い。

4.4 バックグラウンド/非アクティブ時の扱い

タブが 非表示 のときはタイマーが間引かれます。

「今は鳴らさない」ポリシーにする場合は visibilitychange を使ってミュート制御。

let muteWhenHidden = true;

document.addEventListener("visibilitychange", () => {
  if (document.hidden && muteWhenHidden) {
    // グローバルにミュートフラグを立てる等
  }
});

逆に「バックグラウンドでも鳴らしたい」場合は、通知はOKでも音はUX的に嫌がられることが多いので、音量を落とす・音はOFFで通知だけにするなど配慮を。

4.5 MP3等ファイル再生と併用する

ファイル不要のBEEPは軽快ですが、ブランド感を出したい時は MP3/WAV を併用。 Web Audio なら音量・重ね再生が扱いやすいです。

let sfxBuf = null;
async function loadSfx(url) {
  if (sfxBuf) return sfxBuf;
  const res = await fetch(url);
  const buf = await audioCtx.decodeAudioData(await res.arrayBuffer());
  sfxBuf = buf; return buf;
}
async function playMp3(url, { vol=0.8, rate=1.0 } = {}) {
  if (!audioUnlocked) return;
  const buf = await loadSfx(url);
  const src = audioCtx.createBufferSource();
  src.buffer = buf;
  src.playbackRate.value = rate;
  const g = audioCtx.createGain(); g.gain.value = vol;
  src.connect(g).connect(audioCtx.destination);
  src.start();
}

注意: HTMLAudioElement.play() は Promise を返すので、失敗時は catch して握りつぶす(自動再生制限などでエラーになるため)。

4.6 デバッグの見える化

原因切り分けが爆速になります。

function dbgBeepGuard() {
  if (!soundToggle?.checked) { console.log("[beep] toggle=OFF"); return false; }
  if (!audioUnlocked)        { console.log("[beep] audio locked"); return false; }
  if (!audioCtx)             { console.log("[beep] no audioCtx"); return false; }
  return true;
}

さらに ステータス表示 をUIに出すと安心。

statusEl.textContent = "audio: " + (audioCtx ? audioCtx.state : "none") +
                       (audioUnlocked ? " (unlocked)" : " (locked)");

4.7 プチUX指針

  • 効果音は 50〜250ms を目安に短く軽く
  • 連続新着時は 1バッチ1回 に抑制する(“鳴りまくる問題”対策)
  • 音量調整は UI + 永続化(localStorage)でユーザー任せに
  • うるさい時は リリース短縮 と 高域控えめ(triangle/sine) を選ぶ

これらを仕込んでおけば、「動くけど時々鳴らない」「環境で挙動が違う」系のトラブルをだいぶ未然に防げます。

5. まとめ

Web Audio API を使えば、音源ファイルなしで効果音を作れる ことが分かりました。 これはつまり 容量ゼロ・遅延ゼロ のサウンド生成です。ロード不要で即座に鳴るので、通知やゲーム向けには最適です。

今回紹介したテクニックを使えば、

  • 波形切り替えで音色を変えられる
  • ADSRエンベロープでアタック感や余韻を調整できる
  • glide・vibrato・tremolo・harmonics で“それっぽい”音を作れる

といった応用が可能です。

「ピッ」としたシンプルな通知から、「コイン音」「エラー音」まで、外部ライブラリやMP3ファイルを用意せずに表現できるのが大きな強みです。

もちろん、本格的にこだわりたい場合は MP3/WAV と併用するのもアリです。Web Audio を使えばファイル再生も自在に加工できるため、シンセサイザー的な実装へ発展させることも可能です。