
やまびこはリングバッファ1本 ── ディレイの仕組み
山に向かって「ヤッホー」と叫ぶと、少し遅れて自分の声が返ってきます。返ってきた声は元の声よりも小さく、運が良ければ2回目、3回目と、だんだんかすかになりながら繰り返します。カラオケのエコーも、ギタリストが足元で踏むディレイペダルも、正体はこのやまびこです。音を豊かに響かせているように聞こえますが、やっていることを言葉にすると拍子抜けするほど単純で、いまの音に「少し前の自分の音」を弱めて混ぜているだけなのです。 山びこの場合、この「少し前の自分」を届けてくれるのは山です。声が山まで飛んで戻ってくるのに時間がかかるから遅れる。空気の中で減衰するから弱くなる。ではプログラムの中でこれを再現したいとき、山の代わりは何が務めるのか。音を一定時間だけ「ためておいて」、あとから取り出せる置き場所があればいい。今回の主役はその置き場所、リングバッファです。 少し前の自分を、輪にためておく 前提をひとつだけ確かめておくと、コンピューターにとって音とは1秒あたり44100個流れてくる数の列です。スピーカーの膜をどれだけ押し出すかを表す数が、毎秒4万個あまり届いているだけで、サイン波も人の声もこの数の並びにすぎません。だから「少し前の自分の音」とは、少し前に流れていった数のことです。0.3秒前の自分が欲しければ、直近13230個(44100 × 0.3)の数をためておいて、いちばん古いものから取り出せばいい。 ここで使うのが、固定長の配列を端まで来たら先頭へ戻る「輪」に見立てて使う、リングバッファと呼ばれるデータ構造です。当ブログでは以前、Queue の先頭削除からシフトを消す道具 として仕組みを紹介しました。要素を1つも動かさず、位置を指す目印だけをぐるぐる回すのが持ち味でしたが、今回はそのリングバッファが音響エフェクトの心臓部として働く、いわば実戦編です。 ディレイでの使い方はこうです。輪の上に、針を2本立てます。1本は書き込み針で、いま入ってきた音を毎サンプル、輪のマスに書き込んでは隣へ進む。もう1本は読み出し針で、書き込み針から一定の距離だけ離れた後ろを、同じ速さでついて回ります。 書き込み針がいまマスに置いた音を、読み出し針は自分がそのマスに到着したときに拾います。2本は同じ速さで回っているので、到着するのはきっかり「針の間隔ぶん」あとです。つまり読み出し針が拾う音は、いつでも「間隔ぶん昔の音」。この間隔こそがディレイタイム、やまびこが返ってくるまでの時間です。0.3秒のエコーが欲しければ針を0.3秒ぶん(13230マス)離しておく。エフェクターのつまみを回してディレイタイムを変えるという操作は、輪の上では針の間隔を広げたり狭めたりしているだけなのです。 追いかけっこという言葉を使いましたが、2本の針の間隔は永遠に縮まりません。等間隔を保ったまま輪を回り続ける。この安定した追いかけっこが、「常に一定時間前の音が取り出せる」という保証そのものになっています。 やまびこを連ねる ── フィードバック 読み出し針が拾った「少し前の音」を弱めて、いまの音に足す。これで1回だけのやまびこができます。しかし本物のやまびこは2回、3回と続きますし、ディレイペダルの音の魅力も、あの減衰しながら連なる尾にあります。やまびこを連ねるには、どうすればいいのでしょうか。 素朴に考えると、0.6秒前・1.2秒前・1.8秒前……と読み出し針を何本も立てて、それぞれ弱めて足したくなります。それでも作れるのですが、もっとうまい一手があります。混ぜ終わった出力のほうを輪に書き込むのです。 書き込み針が輪に書くのを「入ってきた生の音」ではなく「やまびこを混ぜたあとの出力」にする。すると出力は0.6秒後に読み出し針に拾われて、また弱められて出力に混ざり、それがまた書き込まれて……と、ぐるぐる回り始めます。1回目のやまびこには2回目が、2回目には3回目が、勝手についてくる。この、出力を入口へ戻す配線をフィードバックと呼びます。針を1本も増やさずに、無限に続くやまびこの列が手に入るわけです。 弱める割合を $g$ とすると、やまびこは1周ごとに $g$ 倍されるので、音量は $g,\ g^2,\ g^3,\ \dots$ と等比数列で減っていきます。$g$ が1より小さい限り、この列は必ずゼロへ向かうので、やまびこは自然に消えてくれます。逆に $g$ を1以上にすると、回るたびに音が減らない、あるいは膨らんでいくことになり、音は永遠に止まらずやがて轟音になります。これが発振です。カラオケでマイクをスピーカーに向けたときの「キーン」というハウリングは、まさに空気の上で起きた $g \geq 1$ のフィードバックです。ディレイを実装するときにフィードバック量へ上限を設けるのは、この事故を防ぐためで、このあとのデモでも85%を上限にしています。 触ってみる ここまでの絵が実際に動くデモを用意しました。ボタンで短いプラック音(弦をはじいたような音)をひとつ鳴らすと、リングバッファのディレイを通って、やまびこつきで聞こえます。真ん中の輪では、赤い書き込み針と青い読み出し針が間隔を保って回るのを見られます。ディレイタイムのつまみを回してから鳴らし直すと、針の間隔が広がったり狭まったりするはずです。 うまく表示されないときは、デモを別タブで開く 。 フィードバックを0%に絞ると、混ぜる量がゼロになるのでやまびこは消え、素のプラック音だけが聞こえます。少し上げると1回だけかすかに返り、さらに上げていくと、いちばん下の出力波形に山が増えて、等間隔に並びながら等比で小さくなっていくのが見えます。上限の85%まで振ると、尾が数秒続く深いエコーになります。ディレイタイムを短く(100ms以下に)すると、やまびことしてより、音がにじんで響くように聞こえてくるのも試してみてください。反射が細かく密になっていくと、耳にはエコーではなく残響として届き始めます。お風呂場の響きとやまびこが親戚どうしだと実感できるはずです。 どう実現するか コードにしても、核心部は数行です。輪(配列)を1本と、書き込み位置 w を用意して、読み出し位置は w からディレイぶん後ろを指すだけ。毎サンプル、「読んで、混ぜて、書いて、進む」を繰り返します。 /* リングバッファ1本のディレイ(C風の擬似コード) */ int L = sr * 1; /* 輪の長さ:1秒ぶん(最大ディレイ) */ int D = sr * 0.3; /* 針の間隔=ディレイタイム 0.3秒 */ float g = 0.5f; /* フィードバック(1未満にする) */ float buf[L]; /* リングバッファ */ for (i = 0; i < L; i++) buf[i] = 0; /* まず無音で満たしておく */ int w = 0; /* 書き込み針 */ for (n = 0; n < length; n++) { int r = (w - D + L) % L; /* 読み出し針=書き込み針のD個うしろ */ float out = x[n] + g * buf[r]; /* いまの音に「少し前の自分」を混ぜる */ buf[w] = out; /* 出力を書き込む→D個あとにまた読まれる */ y[n] = out; w = (w + 1) % L; /* 2本の針が同時に1つ進む */ } % L(剰余)が、端まで来た針を先頭へ戻す役です。読み出し位置 r は w から計算で求まるので、変数として持ち歩く必要すらありません。数式で書けば、出力 $y[n]$ は ...






