第5章 見た目と音のつけ方/1 本目 / 全 5/約20分

効果音を用意する

こより

音のファイルは要らないよ。高さのある音と、高さのない雑音、その二つで紙の音まで作れる。

案内役 こより
この術でできるようになること

操作に音が返る

画像は押すと原寸で開きます。

なぜ要るか

いま作ったゲームを触ってみてください。押しても何も返ってきません。

画面は変わります。でも手応えがありません。 押したのに効いたのか分からない、あの感じです。

音が返るだけでこれが消えます。しかも効果音は、絵より安く作れます。

使う道具: なし(ブラウザだけ)

AIプロンプト

この術の作業をAIへ任せるときは、これをそのまま貼ってください。

index.html に効果音の土台を2つ作って。1つは指定した高さ・長さ・波形で鳴らす純音の関数。もう1つは高さのない雑音を短く鳴らす関数で、紙が擦れる音になるように高い成分だけ残して。どちらも鳴り終わりを絞ってブツッと切れないようにして。音量は小さめの既定値にして。

「紙が擦れる音」の部分は、自分の作品で出したい材質の音に置き換えてください。

音のファイルは用意しません

いちばん意外なところだと思います。効果音はコードで作れます。

ブラウザには音を合成する仕組み(Web Audio API)が最初から入っています。ピッという音、ポンという音、そういう単純な音はコードで足ります。

これには効く利点があります。

利点 中身
ファイルが増えない 3-2で決めた「1ファイルで動かす」が崩れない
音が鳴るのが速い 読み込み待ちがない。押した瞬間に鳴る
数字で調整できる 「もう少し高く」を数字1つで試せる
費用ゼロ 生成AIも素材サイトも要らない

凝った音が必要になったら、そのとき作ればいいです。 最初の1本では、これで十分に手応えが出ます。

音は3つの数字でできています

覚えるのは3つだけです。

要素 意味
高さ 周波数。数字が大きいほど高い 880 = 高い、262 = 低い
長さ 何ミリ秒鳴らすか 70 = 短くカツッと、200 = 少し伸びる
波の種類。音色が変わる square = ピコピコ、sine = やわらかい

周波数は、1秒間に何回振動するかを表す数で、単位はHz(ヘルツ)です。880Hzはピアノの高いラの音あたり、262Hzは真ん中のドあたりになります。

高いと軽い、低いと重い。短いと機敏、長いと鈍い。 この対応だけ掴んでおけば、あとは触って決められます。

ただし、これだけだと電子音にしかなりません

ここが分かれ目です。周波数の音(純音)をどう組んでも、出てくるのはピコピコした電子音です。ファミコンの音がまさにそれです。

このゲームは和紙の札を切って置くゲームなので、電子音だと世界観が合いません。必要なのは、もう1つの材料です。

材料 何に化けるか
純音(決まった高さ) 鈴、鐘、電子音、打音
雑音(高さの無い音) 紙、布、砂、風、擦れる音

紙の音は雑音でできています。 高さがないから「音階」に聞こえません。ここに気づくと、生活音のほとんどが作れるようになります。

このゲームで使うのは次の4つです。

場面 材料 狙い
札をえらぶ 純音を短く 軽い。押した合図だけ
札を置く 雑音 + 低い純音 紙が擦れて、盤に触れる
札をひらく 雑音だけ ぱっと開く紙
決着 鈴 / 低い鐘 勝ち負けをはっきり分ける

やってみる

  1. AIに材料を2つ作らせる

    人間が決めるのは「純音と雑音の2つを作る」という設計だけです。中身の書き方はAIが知っています。

    claude -p "index.html に効果音の土台を2つ作って。1つは指定した高さ・長さ・波形で鳴らす純音の関数。もう1つは高さのない雑音を短く鳴らす関数で、紙が擦れる音になるように高い成分だけ残して。どちらも鳴り終わりを絞ってブツッと切れないようにして。音量は小さめの既定値にして。"
    

    「紙が擦れる音になるように」と用途で言うのが要点です。 「ハイパスフィルタを掛けて」と技術で指定しなくても、AIは用途から手段を選べます。

    出てきたのがこれです。純音のほう。

    function tone(freq, dur, type, vol) {
      if (!soundOn) return;
      var c = ctx();
      var osc = c.createOscillator();
      var gain = c.createGain();
      osc.type = type || 'sine';
      osc.frequency.value = freq;
      // 鳴り終わりを絞る。これが無いと「ブツッ」と鳴る
      gain.gain.setValueAtTime(vol || 0.08, c.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur);
      osc.connect(gain); gain.connect(c.destination);
      osc.start(); osc.stop(c.currentTime + dur);
    }
    

    読み方はこうです。createOscillator が音の元を作る発振器、createGain が音量つまみです。発振器を音量つまみへ、音量つまみをスピーカーへ順につないでから、start で鳴らして stop で止めています。

    0.08 が音量です。1.0にすると耳が痛いです。 0.05〜0.15から始めてください。

    雑音のほう。

    function noise(dur, vol) {
      // 紙をこする音。短い雑音を高い方だけ通す
      if (!soundOn) return;
      var c = ctx();
      var len = Math.floor(c.sampleRate * dur);
      var buf = c.createBuffer(1, len, c.sampleRate);
      var data = buf.getChannelData(0);
      for (var i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / len);
      var src = c.createBufferSource(); src.buffer = buf;
      var hp = c.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 2200;
      var gain = c.createGain(); gain.gain.value = vol || 0.12;
      src.connect(hp); hp.connect(gain); gain.connect(c.destination);
      src.start();
    }
    

    やっていることは3つです。乱数を並べて(雑音)、だんだん小さくして(1 - i / len)、低い音を捨てる(highpass)。 低い成分を捨てないと「ザー」というホワイトノイズのままで、紙になりません。

  2. 鈴を1つ足す

    決着の音だけは、少し手をかける価値があります。

    function bell(freq, dur) {
      // 鈴。基音と倍音を重ねて減衰させる
      tone(freq, dur, 'sine', 0.09);
      tone(freq * 2.76, dur * 0.6, 'sine', 0.035);
      tone(freq * 5.4, dur * 0.35, 'sine', 0.015);
    }
    

    倍音は、基本の高さの何倍かの高さで一緒に鳴っている音です。実際の楽器の音は、単独の高さではなくこの重なりでできていて、混ざり方が音色になります。

    2.765.4 が鈴らしさの正体です。 ピアノやギターは倍音が2倍・3倍と整数で並びますが、金属の打楽器は整数になりません。だから澄んで聞こえます。数字を2と3に変えて鳴らすと、鈴が笛になります。

  3. 音を4つだけ決める

    増やさないでください。4つで足ります。

    var SE = {
      eranbu: function () { tone(880, 0.05, 'triangle', 0.05); },
      oku:    function () { noise(0.09, 0.08); tone(180, 0.09, 'sine', 0.05); },
      hiraku: function () { noise(0.06, 0.09); },
      win:    function () { bell(1046, 1.1); setTimeout(function () { bell(1568, 1.3); }, 160); },
      lose:   function () { tone(160, 0.7, 'sine', 0.09); tone(80, 0.9, 'sine', 0.05); },
    };
    

    勝ちは上がる2つの鈴、負けは低い音を重ねる。 これだけで意味が伝わります。音が上がると期待、下がると落胆に聞こえるからです。

    置く音が noisetone の2つ同時なのに注目してください。紙が擦れる音と、盤に触れる音を重ねています。 実物の音はたいてい2つ以上の材料でできています。

  4. 音の入口を作る

    ここが一番つまずくところです。ブラウザは、利用者が操作するまで音を鳴らしません。

    勝手に音が出るサイトを防ぐための仕組みです。だから押してもらう場所が必要です。

    <p class="sound">
      <button type="button" id="soundBtn">音を出す</button>
    </p>
    
    var soundOn = false;
    var audioCtx = null;
    function ctx() {
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      return audioCtx;
    }
    el('soundBtn').addEventListener('click', function () {
      soundOn = !soundOn;
      el('soundBtn').textContent = soundOn ? '音を止める' : '音を出す';
      SE.eranbu();   // 押した音を鳴らして、音が出ることを見せる
    });
    

    addEventListener('click', ...) は、そのボタンが押されたときに中身を実行する登録です。soundOn = !soundOn! は真偽を逆にする印なので、押すたびにONとOFFが入れ替わります。ボタンの文字も同時に書き換えているので、いまどちらの状態かが見て分かります。

    ctx() を関数にしておくのが要点です。 最初に音を鳴らす瞬間まで AudioContext を作りません。ページを開いた瞬間に作ると、ブラウザに止められた状態で残ってしまいます。

    置いた場所がこれです。

    ゲーム画面の下に「音を出す」ボタンが置かれている

    画面のいちばん下です。 遊ぶ邪魔にならず、探せば見つかる位置に置きます。

  5. 操作に音をつなぐ

    3章で作った3つの関数に、1行ずつ足すだけです。place() が札を置く処理、reveal() が伏せた札をひらく処理、finish() が勝敗を出す処理です。その処理が起きる場所へ、対応する音を1行置いていきます。

    // 札をえらんだとき
    SE.eranbu();
    
    // place() の中。的に置いた瞬間。0.5秒の間より先に鳴らす
    SE.oku();
    
    // reveal() の中。伏せた札をひらく瞬間
    SE.hiraku();
    
    // finish() の中
    if (myTargets > cpuTargets) { SE.win(); } else { SE.lose(); }
    

    置いた音を、0.5秒の間より先に鳴らすのが要点です。 順番を逆にすると、押した実感が結果に埋もれます。3-3で入れた間が、ここで音の間にもなります。

  6. 鳴らして確かめる

    open index.html
    

    「音を出す」を押してから、札を置いてください。紙の音 → 0.5秒の沈黙 → 開く音の並びになっていれば成功です。

  7. 保存する

    git add .
    git commit -m "操作に効果音をつけた"
    git push
    

音が出ているかを機械で確かめる

耳で聞けば分かる、と思うところですが、音は「出ていないこと」に気づきにくい部分です。音量ゼロのイヤホン、消音のパソコン、鳴っているつもりで鳴っていない。

僕は自分の音を1回、数字で確かめました。同じ音をファイルへ書き出して、波の大きさを見るやり方です。

音       サンプル数   最大の振れ   終わりの振れ
えらぶ      5292      0.0472        0
置く        8820      0.1975        0
ひらく      6615      0.1409        0
勝ち       57330      0.1360        0
負け       44100      0.1257        0

表の言葉はこう読みます。サンプル数は音を細かく刻んだ点の数で、長さに比例して増えます。振れは波の大きさで、そのまま音量に当たります。0なら無音です。

見たいのは3つです。

3つ目で引っかかりました。 置く音だけ 0.1975 で、いちばん小さい「えらぶ」の4倍あります。雑音と純音を重ねたぶん、音量が足し算になっていたからです。

耳で聞いたときは「少し大きいかな」で流していました。数字にすると流せません。 音量を下げて測り直します。

oku: function () { noise(0.09, 0.08); tone(180, 0.09, 'sine', 0.05); },
置く        8820      0.1437        0

他の音と並びました。 直したのは数字2つです。

なお雑音は毎回乱数から作るので、測るたびに少しぶれます。小数第2位まで見れば十分で、そこから先を追う意味はありません。

耳で「鳴っている気がする」より確実です。 7章でテストを扱いますが、考え方は同じで、目と耳に頼る確認を機械に置き換えていきます。

つまずきどころ

まったく音が出ない

音の解禁がされていません。必ず1回、利用者が押す操作が必要です。 押していないのに鳴らそうとしても、ブラウザが黙って無視します(エラーも出ません)。

「ブツッ」と鳴る

絞りが入っていません。exponentialRampToValueAtTime の行を消すと、この音になります。1回消して聞いてみると、何のための行か分かります。

うるさい

vol を下げてください。効果音は「気づく最小」で十分です。 大きくすると、それだけで安っぽくなります。

紙の音に聞こえない。ザーというノイズになる

highpass の周波数が低すぎます。2200 を 3000 まで上げると乾いた紙に、1000 まで下げると布や風に近づきます。捨てる低音の量が、材質そのものです。

連打すると音が重なって濁る

音が重なるのは正常です。気になるなら、鳴らす間隔に下限を入れます。

var lastAt = 0;
function tone(freq, dur, type, vol) {
  var now = Date.now();
  if (now - lastAt < 40) return;   // 40ミリ秒は間を空ける
  lastAt = now;
  ...
}

音を止めたい人がいる

当然の要求です。ON/OFFは次の術で入れます。 BGMと一緒に切れる方が自然だからです。

Windows の場合

コードは同じです。open index.html の代わりに start index.html を使ってください。

音が出ない場合は、Windows側の音量ミキサーでブラウザが消音になっていないか見てください。

次の一歩

noisehighpass2200 から 800 に下げて、札を置いてみてください。

紙が布に変わります。1つの数字で材質が変わるという感覚を1回持っておくと、以降は狙って選べます。

音が返るようになると、同じゲームが急に「触れる物」に変わります。次はBGMを敷いて、場の温度を作ります!

ここまでの三つの的

この講座は、見本ゲーム「三つの的」を術ごとにすこしずつ育てながら進みます。下にあるのはこの術を終えた時点の実物です。そのまま遊べます。

別の画面でこのゲームを開く

術の一覧へ戻る

メルマガ(無料)忖度なしのAI事情を、メールでお届けします。ご登録の方に、プレゼントをお渡ししています