なぜ要るか
いま作ったゲームを触ってみてください。押しても何も返ってきません。
画面は変わります。でも手応えがありません。 押したのに効いたのか分からない、あの感じです。
音が返るだけでこれが消えます。しかも効果音は、絵より安く作れます。
使う道具: なし(ブラウザだけ)
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つです。
| 場面 | 材料 | 狙い |
|---|---|---|
| 札をえらぶ | 純音を短く | 軽い。押した合図だけ |
| 札を置く | 雑音 + 低い純音 | 紙が擦れて、盤に触れる |
| 札をひらく | 雑音だけ | ぱっと開く紙 |
| 決着 | 鈴 / 低い鐘 | 勝ち負けをはっきり分ける |
やってみる
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)。 低い成分を捨てないと「ザー」というホワイトノイズのままで、紙になりません。鈴を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.76と5.4が鈴らしさの正体です。 ピアノやギターは倍音が2倍・3倍と整数で並びますが、金属の打楽器は整数になりません。だから澄んで聞こえます。数字を2と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つの鈴、負けは低い音を重ねる。 これだけで意味が伝わります。音が上がると期待、下がると落胆に聞こえるからです。
置く音が
noiseとtoneの2つ同時なのに注目してください。紙が擦れる音と、盤に触れる音を重ねています。 実物の音はたいてい2つ以上の材料でできています。音の入口を作る
ここが一番つまずくところです。ブラウザは、利用者が操作するまで音を鳴らしません。
勝手に音が出るサイトを防ぐための仕組みです。だから押してもらう場所が必要です。
<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を作りません。ページを開いた瞬間に作ると、ブラウザに止められた状態で残ってしまいます。置いた場所がこれです。
画面のいちばん下です。 遊ぶ邪魔にならず、探せば見つかる位置に置きます。
操作に音をつなぐ
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で入れた間が、ここで音の間にもなります。
鳴らして確かめる
open index.html「音を出す」を押してから、札を置いてください。紙の音 → 0.5秒の沈黙 → 開く音の並びになっていれば成功です。
保存する
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つです。
- 最大の振れが0でない → 音が出ている
- 終わりの振れが0 → 絞りが効いている(ブツッと切れていない)
- 音どうしの振れが揃っている → 一部だけ突出して大きくない
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側の音量ミキサーでブラウザが消音になっていないか見てください。
次の一歩
noise の highpass を 2200 から 800 に下げて、札を置いてみてください。
紙が布に変わります。1つの数字で材質が変わるという感覚を1回持っておくと、以降は狙って選べます。
音が返るようになると、同じゲームが急に「触れる物」に変わります。次はBGMを敷いて、場の温度を作ります!
ここまでの三つの的
この講座は、見本ゲーム「三つの的」を術ごとにすこしずつ育てながら進みます。下にあるのはこの術を終えた時点の実物です。そのまま遊べます。
