マガジン

Web Audio APIで音を合成する:ファイルなしで効果音を作る

遊びをつくる

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

つなぎ目のない銅色のリボンとテープリールで、自然なBGMループを表した写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ゲームに効果音を付けたいとき、真っ先に思い浮かぶのは「音声ファイルを用意する」ことかもしれません。しかし、ブラウザにはWeb Audio APIという仕組みが標準で備わっており、JavaScriptだけで音を合成して鳴らすことができます。mp3やwavのファイルを一切用意しなくても、コインを取る音、爆発音、ジャンプ音といった効果音を数十行のコードで作れます。 この記事では、Web Audio APIの中でも「音の合成」に絞って説明します。オシレーター(振動を生み出す部品)とゲインノード(音量を制御する部品)の組み合わせ方、エンベロープ(時間変化のある音量カーブ)の付け方、そして実際のゲームで使いやすい形にまとめる手順を示します。

この記事の目次
  1. なぜファイルなしで作るのか
  2. Web Audio APIの最小構成を理解する
  3. エンベロープで「鳴り方」を変える
  4. 代表的な効果音を作る手順
  5. 実装で詰まりやすいポイント
  6. 次の行動
  7. 参考資料

なぜファイルなしで作るのか

音声ファイルを使う方法には「ファイルの読み込みが完了するまで鳴らせない」「配布するゲームに音声ファイルを同梱しなければならない」という実務上の手間があります。Web Audio APIで合成した音はJavaScriptのコードが実行された瞬間に生成されるため、ロード待ちがなく、ファイルサイズの増加もありません。HTMLファイル一枚で完結させたいゲームや、アセットの権利関係を気にせず使いたい場面に特に向いています。

一方で、人の声・楽器の演奏・録音された環境音など、複雑な波形は合成では再現しにくい側面があります。「シンプルな電子音でよいか、リアルな音が必要か」を最初に判断することが、アプローチ選択の出発点です。

Web Audio APIの最小構成を理解する

Web Audio APIは「AudioContext」と呼ばれる音の処理環境を中心に動きます。音の信号はノードと呼ばれる部品を経由して最終的にスピーカー(destination)へ届きます。効果音を合成する最小構成は次の3つのノードです。

  • OscillatorNode:サイン波・矩形波・ノコギリ波などの波形を生成します。周波数(Hz)を指定することで音の高さが決まります。
  • GainNode:音量を0〜1(またはそれ以上)の数値で制御します。エンベロープを付けるときにも使います。
  • AudioContext.destination:最終出力先(スピーカー)です。ノードのつなぎ先として使います。

接続の順番は「OscillatorNode → GainNode → destination」です。この一本道さえ守れば、最低限の音が出ます。

javascript
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.connect(gain);
gain.connect(ctx.destination);

osc.type = 'square';   // 矩形波:レトロゲーム風の音
osc.frequency.value = 440; // A4(ラの音)

osc.start();
osc.stop(ctx.currentTime + 0.2); // 0.2秒後に停止

AudioContextはユーザーの操作(クリック・タップ)をきっかけに初期化する必要があります。ページ読み込み時に自動的に生成しようとすると、ブラウザのオートプレイ制限により無音になるケースがあるため、最初の入力イベント内でnew AudioContext()するか、ctx.resume()を呼ぶのが確実です。

エンベロープで「鳴り方」を変える

単純に音をオン・オフするだけでは、電子音特有の「ブツッ」というノイズが生じます。これはクリックノイズと呼ばれ、音量が瞬時に0→1→0と変化することで起きます。これを防ぎ、かつ効果音らしい鳴り方を作るのがエンベロープの役割です。

エンベロープはADSR(Attack・Decay・Sustain・Release)という4段階で表現されることが多いですが、効果音では「Attack(立ち上がり)」と「Release(減衰)」の2段階だけ使うケースが大半です。

javascript
function playBeep(ctx, freq, duration) {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();

  osc.connect(gain);
  gain.connect(ctx.destination);

  osc.type = 'sine';
  osc.frequency.value = freq;

  const now = ctx.currentTime;

  // Attack:0から0.8へ0.01秒で立ち上げる
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.8, now + 0.01);

  // Release:0.8から0へduration秒かけて減衰
  gain.gain.linearRampToValueAtTime(0, now + duration);

  osc.start(now);
  osc.stop(now + duration + 0.01);
}

linearRampToValueAtTimeは直線的な変化、exponentialRampToValueAtTimeは指数的な変化です。爆発や衝撃音には指数的な急激な減衰が自然に聞こえます。コインや決定音には直線的な減衰が合いやすい傾向があります。

代表的な効果音を作る手順

ジャンプ音

周波数を時間とともに上げていくと「ピュイン」というジャンプ音になります。OscillatorNodeのfrequencyパラメーターにもlinearRampToValueAtTimeが使えます。

javascript
function playJump(ctx) {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.connect(gain);
  gain.connect(ctx.destination);

  const now = ctx.currentTime;
  osc.type = 'square';
  osc.frequency.setValueAtTime(200, now);
  osc.frequency.linearRampToValueAtTime(600, now + 0.15);

  gain.gain.setValueAtTime(0.5, now);
  gain.gain.linearRampToValueAtTime(0, now + 0.15);

  osc.start(now);
  osc.stop(now + 0.2);
}

爆発・ダメージ音

ノイズ(ランダムな波形)を使うと衝撃系の音になります。Web Audio APIにはcreateBufferでホワイトノイズを手動生成する方法があります。

javascript
function playExplosion(ctx) {
  const bufferSize = ctx.sampleRate * 0.3;
  const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = Math.random() * 2 - 1; // -1〜1のランダム値
  }

  const source = ctx.createBufferSource();
  source.buffer = buffer;

  const gain = ctx.createGain();
  source.connect(gain);
  gain.connect(ctx.destination);

  const now = ctx.currentTime;
  gain.gain.setValueAtTime(1, now);
  gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);

  source.start(now);
}

ホワイトノイズの後にBiquadFilterNode(フィルター)を挟み、低域だけ通すローパスフィルターにすると、爆発音らしい重さが増します。

コイン・アイテム取得音

短い上昇音を2音連続で鳴らすパターンが定番です。ctx.currentTimeにオフセットを足して2つ目のオシレーターを少し遅らせます。

javascript
function playCoin(ctx) {
  [600, 900].forEach((freq, i) => {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.connect(gain);
    gain.connect(ctx.destination);

    const t = ctx.currentTime + i * 0.1;
    osc.type = 'sine';
    osc.frequency.setValueAtTime(freq, t);

    gain.gain.setValueAtTime(0.5, t);
    gain.gain.linearRampToValueAtTime(0, t + 0.08);

    osc.start(t);
    osc.stop(t + 0.1);
  });
}

実装で詰まりやすいポイント

オシレーターは使い捨てです。一度stop()したノードは再利用できません。効果音を鳴らすたびにcreateOscillator()で新しいインスタンスを作る設計にしてください。

AudioContextはひとつで十分です。呼ばれるたびにnew AudioContext()するとコンテキストが増え続け、ブラウザによってはエラーや音の遅延が起きます。ゲームの初期化時に一度だけ作り、モジュール変数やクロージャで保持するのが一般的な作り方です。

音量の単位に注意が必要です。GainNodeのgain.valueは0〜1の比率ですが、複数の音を同時に鳴らすと合計音量がクリッピング(音割れ)することがあります。最終出力の直前に全体のGainNodeを一つ置き、0.3〜0.5程度に絞るマスターボリューム設計が安定します。

モバイルSafariのAutoPlay制限は特に厳格です。touchstartまたはtouchendイベントの中でctx.resume()を呼び出し、コンテキストがrunning状態になっていることを確認してから音を鳴らすようにしてください。

次の行動

まずplayBeep関数をHTMLファイルに貼り付け、ボタンクリックで呼び出す動作確認から始めてください。音が出たら周波数・波形の種類・エンベロープの時間を変えながら、自分のゲームに合う音を探してみてください。作成した効果音付きのゲームはアソバースにHTMLファイルとして直接投稿できます。

参考資料

  • MDN Web Docs「Web Audio API」
  • MDN Web Docs「OscillatorNode」
  • MDN Web Docs「GainNode」
  • MDN Web Docs「AudioContext」