![[JavaScript] 効果音はBEEPで十分!Web Audioで作るゲームサウンド](https://humanxai.info/images/uploads/javascript-web-audio-beep.webp)
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 を使えばファイル再生も自在に加工できるため、シンセサイザー的な実装へ発展させることも可能です。
💬 コメント