「こだま」を作る:遅らせて小さくする処理を自分で書く
遊びをつくる執筆にClaudeを使用しています。

山びこは、声が遠くの壁に当たって、少し遅れ、少し小さくなって戻ってくる現象です。これをプログラムで作るには、音のデータを「遅らせて」「小さくして」「元の音に足す」だけで足ります。この記事では、その処理を配列の計算として自分で書き、ゲームの音として使えるかを確かめる実験を組みます。 題材は、光る葉の順番を覚えて4つの音色で返す記憶ゲーム「こだまの森」のような、音を聴いて順番を答える遊びです。ここで問うのは、こだまを足した音列が聞き分けやすさを保てるのか、保てるならどこまでかです。こだまは雰囲気を作りますが、次の音と重なれば順番の手がかりを濁らせるかもしれません。 この記事に結果は載せていません。コードと記録表を用意しているので、読者が自分の耳で境目を見つける形で進めます。
この記事の目次
こだまは「遅らせて小さくして足す」だけでできる
処理の中身は、1行で書けます。
出力の i 番目 = 入力の i 番目 + 出力の (i − 遅れ) 番目 × 減衰率
ここで「出力の」過去の値を足している点が要です。入力だけを遅らせて足すと、こだまは1回しか返りません。出力に含まれるこだまをさらに遅らせて小さくして足すので、2回目、3回目と繰り返し返ってきます。この「出力を入力側へ戻す」仕組みを、フィードバックと呼びます。減衰率(フィードバック量)が1未満なら、こだまは回を重ねるごとに小さくなって消えます。1以上だと消えるどころか大きくなり続けます。
ブラウザには標準で遅延用の部品もあります。この記事で配列計算として書くのは、処理の意味を手元で見られるようにするためです。「遅れ」は秒ではなくサンプル数(音を1秒あたり何個の数字で表すか、その数字の個数)で数えます。44,100サンプル/秒の音で0.3秒遅らせるなら、13,230個ぶん前の値を見ることになります。
準備するもの:60行未満の実験装置
用意するのはブラウザとイヤホンかヘッドホン、メモ用の紙だけです。下のコードを echo.html という名前で保存し、ブラウザで開いてください。外部ライブラリもサーバーも使わず、1ファイルで動きます。
<button id="go">鳴らす</button>
<script>
const SR = 44100;
const DELAY = 0.3; // 遅らせる秒数
const FEEDBACK = 0.5; // 1回ごとに残す割合(1未満)
const GAP = 0.6; // 音の間隔(秒)
const NOTES = [440, 523, 659];
function tone(freq, sec) {
const n = Math.floor(SR * sec), out = new Float32Array(n);
for (let i = 0; i < n; i++) {
out[i] = Math.sin(2 * Math.PI * freq * i / SR) * Math.exp(-6 * i / n) * 0.5;
}
return out;
}
function sequence(freqs, gap) {
const out = new Float32Array(Math.floor(SR * (gap * freqs.length + 0.5)));
freqs.forEach((f, k) => {
const s = Math.floor(SR * gap * k);
tone(f, 0.4).forEach((v, i) => { out[s + i] += v; });
});
return out;
}
function echo(input, delaySec, feedback, tailSec) {
const d = Math.floor(SR * delaySec);
const out = new Float32Array(input.length + Math.floor(SR * tailSec));
for (let i = 0; i < out.length; i++) {
const dry = i < input.length ? input[i] : 0;
out[i] = dry + (i >= d ? out[i - d] * feedback : 0);
}
return out;
}
let ctx;
document.getElementById('go').onclick = () => {
ctx ||= new AudioContext();
const data = echo(sequence(NOTES, GAP), DELAY, FEEDBACK, 2);
let peak = 0;
for (const v of data) peak = Math.max(peak, Math.abs(v));
console.log('最大の振幅', peak.toFixed(3));
const buf = ctx.createBuffer(1, data.length, SR);
buf.copyToChannel(data, 0);
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.start();
};
</script>コードの役割は3つです。tone は鳴らしてすぐ小さくなる単音を作り、sequence はそれを一定間隔で並べます。echo が本題で、for文の中の1行が先ほどの式そのものです。dry が元の音、out[i - d] * feedback が遅らせて小さくしたこだまです。
最大の振幅は、開発者ツールのコンソールに表示されます。この値が1.0を超えると、再生時に音が割れる(クリップする)目安になります。
条件を決めて記録する
動かすのは、冒頭の DELAY、FEEDBACK、GAP、NOTES の4つの定数だけです。まず次の5条件を用意します。
- A、DELAY(秒):0.3、FEEDBACK:0.5、GAP(秒):0.6、ねらい:基準。こだまが次の音の前に返る
- B、DELAY(秒):0.6、FEEDBACK:0.5、GAP(秒):0.6、ねらい:こだまが次の音の開始と重なる
- C、DELAY(秒):0.03、FEEDBACK:0.5、GAP(秒):0.6、ねらい:遅れをごく短くする
- D、DELAY(秒):0.3、FEEDBACK:0.8、GAP(秒):0.6、ねらい:長く残す(最大の振幅に注意)
- E、DELAY(秒):0.3、FEEDBACK:0.0、GAP(秒):0.6、ねらい:こだまなしの対照
条件Eは必ず入れてください。こだまを足していない音列で聞き分けられる度合いが基準になり、他の条件はそれとの差で読むことになります。
1回の手順
- 4〜5個の音を並べた音列を NOTES に書きます。周波数は、たとえば 440、494、523、587、659 のように、隣り合う音が聞き分けられる程度に離します。
- 条件を設定し、ファイルを再読み込みして「鳴らす」を押します。
- 聞き終えたら、聞こえた音の順番を、高い・低いの相対的な形で紙に書きます。
- 実際の NOTES と見比べ、順番が合った音の数を数えます。
- 「こだまらしさ」を1〜5点で付け、コンソールの最大の振幅を書き写します。
記録の単位と回数
記録は「条件 × 音列」の1回を1行にして、次の列で取ります。
- 条件(A〜E)
- 音列(何番の音列か)
- 順番が合った音の数(例:5音中4音)
- こだまらしさ(1〜5)
- 最大の振幅
- ひとこと(「重なって2音目が埋もれた」など)
各条件は3回以上、音列は毎回変えます。同じ音列を繰り返すと、耳が聞き分けたのか記憶が答えたのか区別できなくなります。音列は5〜6通り用意して条件にばらして割り当てるのが確実です。
偏りを減らすには、条件の順番を紙に書いてくじで決めるか、他の人に条件と音列の組み合わせを設定してもらい、自分は条件を知らないまま聞く方法が使えます。耳の疲れが出るので、1回の実験は10〜15回程度で区切り、日を改めて繰り返してください。
結果の読み方:どこで「こだま」が「じゃま」になるか
記録がたまったら、条件ごとに「順番が合った音の数」の平均を出し、条件Eの値と並べます。見るのは次の3点です。
- Eと比べて正答が落ちる条件はどれか:その条件が、こだまによって聞き分けが難しくなる側です。
- 正答が落ちず、こだまらしさが高い条件はどれか:雰囲気と聞き取りやすさが両立できている候補です。
- こだまらしさが低い条件はどれか:C(遅れが短い)のような条件が、こだまとして聞こえるのか、音色が変わっただけに聞こえるのかを、ひとことメモで確かめます。遅れが数十ミリ秒より短いと、別の音としては聞こえにくいと一般に言われています。ただ、その感じ方は音や人によって違うので、自分の耳の記録で判断してください。
差が出ないときは、音列が易しすぎる可能性があります。音数を増やす、音の間隔 GAP を縮めるなど、課題を難しくしてから条件を比べ直してみてください。逆に全条件で正答が低いなら、音が近すぎて聞き分けられていないので、周波数の離し方を見直します。
各条件3回程度では、統計的に言える結論は出ません。ここで得られるのは「この耳とこのイヤホンでの傾向」までです。他の人に聞いてもらうときも同様に、人数が少なければ、一般化せず参考値として扱ってください。
最大の振幅が1.0を超えた回は、こだまの影響と音割れの影響が混ざっています。その回は別枠で記録し、音量を 0.5 から下げて取り直すのが安全です。
起こりやすい誤りと対処
- 秒とサンプル数の混同:DELAY は秒ですが、配列の添字はサンプル数です。コードの SR * delaySec の掛け算を消すと、遅れが 0.3 サンプルになり、こだまは聞こえません。
- FEEDBACK を1以上にする:こだまが減衰せず、増え続けて音割れします。0.9 前後でも、最大の振幅が1を超えることがあります。試すなら console.log の値を見ながら少しずつ上げてください。
- 余韻の切れ目でぷつっと音が途切れる:echo の最後の引数 tailSec(こだまの余韻を確保する秒数)が短すぎると起こります。FEEDBACK が大きいほど、余韻は長く必要です。
- 再生ボタンを押す前に音が出ない:多くのブラウザでは、クリックなどの操作の後でないと音声の再生を始められません。コードで AudioContext をクリック後に作っているのはそのためです。
- 音量の上げすぎ:イヤホンで聞くときは、最初は小さい音量から始めてください。D(長く残す)の条件は特に音が重なって大きく聞こえることがあります。
- スピーカーで聞く:部屋の反響が混ざり、自分で足したこだまとの区別がつかなくなります。ヘッドホンかイヤホンを使ってください。
作品に持ち込むときの考え方
「こだまの森」の確認済みの仕様から、設計として読み取れることを整理します。光る葉の順番を、4つの音色で返す遊びです。聴き直しは15点減、間違いは30点減で3回までなので、聴き直しにも間違いにも代償がある作りです。一方で、音なしでも遊べる設定があります。
ここから読み取れるのは、音が手がかりの中心になっている場面でも、聞き取りの難しさが遊びの公平さに関わるだろうということです。音以外でも順番を確かめられる経路があるのも、設計から見えます。こだまを足す作品を作るときは、自分の実験で見つけた「正答が落ちない側」の設定に収め、音を使わない補助の手がかりも並べるのが堅実でしょう。作品が実際にどんな音処理や画面を使っているかは、作品を開いて確かめてください。
次の行動
まず上のコードを保存し、条件EとAだけを3回ずつ聞いて記録を取ってください。次にB・C・Dを足し、境目の見える条件が見つかったら、その DELAY と FEEDBACK を自作ゲームの音に使ってみてください。アソバースへ投稿する場合、このコードのような1ファイルのHTMLなら index.html として直接アップロードできます。
参考資料
- MDN Web Docs「Web Audio API」



