お風呂場で歌うと、いつもより上手く聞こえます。声が壁とお湯の面で何度も跳ね返り、少し遅れた自分の声が何重にも折り重なって、細かいアラを覆い隠してくれるからです。コンサートホールの豊かな響きも、カラオケのエコーの奥にあるあの「ふわっ」とした広がりも、正体は同じ。無数の反射が折り重なった音、つまり残響です。

ディレイの回 の終わりに、ディレイタイムをうんと短くすると、やまびこがにじんで残響のように聞こえ始める、という話をしました。今回はその残響を正面から作ります。そして残響を作ろうとすると、信号処理のど真ん中にある「畳み込み」という計算が、避けようもなく姿を現します。畳み込みには $y[n] = \sum x[k] \cdot h[n-k]$ といういかめしい数式がついて回りますが、あらかじめ言ってしまうと、この式がやっているのは「配って足す」という、拍子抜けするほど素朴な作業です。

手拍子1発が、部屋のすべてを語る

がらんとした教会の真ん中で、パンと1回だけ手を叩くところを想像してください。手拍子はほぼ一瞬の音、時間軸の上ではたった1本のパルスです。ところが部屋は、その一瞬に対して長い返事を返してきます。まず手拍子そのものが直接届き、続いて壁や床で1回跳ねた音が数発、そのあとは何度も跳ね返って細かくなった無数の反射が、密な尾を引きながら数秒かけて消えていく。

手拍子1発への部屋の返事。直接音、初期反射、無数の反射の尾が順に届き、この記録がインパルス応答になる

この「一瞬の衝撃(インパルス)への部屋の返事(応答)」を録音したものを、インパルス応答と呼びます。長い名前ですが、中身はただの手拍子の録音です。そして驚くべきことに、この録音がたった1本あれば、その教会で歌を歌った音も、ギターを弾いた音も、実際に足を運ばずに計算だけで作り出せます。実在のホールや名機と呼ばれる残響装置の「響きそのもの」を丸ごと持ち帰れるわけで、この方式のリバーブはコンボリューション・リバーブ(畳み込みリバーブ)という名前で、いまの音楽制作の定番になっています。

手拍子1発の録音から、なぜ任意の音を鳴らした結果が計算できてしまうのか。そのからくりが、今回の核心です。

配って足す

鍵になるのは、この連載で繰り返し出てくる前提 、つまりコンピューターにとって音は1秒あたり44100個の数の列だ、という事実です。数の一つ一つは「その瞬間にスピーカーの膜をどれだけ押すか」でした。ということは、入力の音は「高さの違う小さなパルスが44100分の1秒おきにびっしり並んだもの」だと見ることができます。歌もギターも、ミクロに見れば小さな手拍子の連打なのです。

そして部屋は、音が十分小さいうちは律儀です。2倍の強さで叩けば、返事もそっくり2倍になる。2発続けて叩けば、それぞれの返事がそのまま重なって聞こえる。返事の形そのものは、いつ叩いても変わりません。この律儀さがあるので、次のことが言えます。入力のサンプル1個1個が、それぞれ「自分の高さ倍にしたインパルス応答のコピー」を、自分の時刻から発生させる。そして同じ時刻に重なった返事たちは、ただの足し算で合わさる。これだけで、部屋の応答が完全に再現できてしまうのです。

畳み込みの仕組み。入力の各サンプルが、自分の高さ倍にしたインパルス応答のコピーを自分の位置から置いていき、縦にぜんぶ足すと出力になる

これを式に書き起こしたものが、冒頭のいかめしい数式です。

$$y[n] = \sum_{k} x[k] \cdot h[n-k]$$

読み方はこうです。$k$ 番目の入力サンプル $x[k]$ は、時刻 $k$ で部屋を叩きます。その返事のうち時刻 $n$ に届く分は、叩いてから $n-k$ サンプル後の返事、つまり $h[n-k]$ を $x[k]$ 倍したもの。時刻 $n$ の出力 $y[n]$ は、そこに返事を届けてくるすべての $k$ について足し合わせたもの。つまり $x[k] \cdot h[n-k]$ が「配る」で、$\sum$ が「足す」。数式は「配って足す」の記号化にすぎません。この計算の名前が畳み込み(コンボリューション)です。画像をぼかすフィルタも、画像認識で使われる畳み込みニューラルネットワーク(CNN)の「畳み込み層」も、次元こそ違えど中身はこれと同じ「配って足す」をやっています。

ディレイは、棒がまばらな畳み込みだった

ここでディレイの仕組みを思い出すと、面白いことに気づきます。ディレイに手拍子を1発入れると、何が返ってくるでしょうか。原音が1本、$D$ 秒後にやまびこが $g$ 倍で1本、$2D$ 秒後に $g^2$ 倍で1本……。つまりディレイのインパルス応答は、等間隔にまばらに立つ、等比で縮む棒の列です。

ディレイのインパルス応答は等間隔にまばらな棒の列、部屋のインパルス応答は無数の棒がびっしり並んだ列。同じ畳み込みで、hの形だけが違う

やまびこと残響は、同じ「配って足す」機械に差し込む $h$ の形が違うだけだったのです。棒がまばらで間隔が広ければ、返事を1発ずつ聞き分けられて、やまびこに聞こえる。棒が無数にびっしり詰まっていれば、返事は溶け合って、残響に聞こえる。ディレイの回で「ディレイタイムを短くすると残響のように聞こえてくる」と書いたのは、棒の間隔を詰めていくと $h$ が部屋のインパルス応答の形へ近づいていく、ということだったわけです。

触ってみる

同じ音に別々のインパルス応答をまとわせて聴き比べられるデモを用意しました。プラック音(弦をはじいたような短い音)を鳴らしてから、「部屋」を小部屋・ホール・バネ風と切り替えてみてください。音の素材はまったく同じなのに、鳴っている場所だけが変わります。「手拍子風」のボタンはほぼパルスの音なので、これを押すと選んだ部屋のインパルス応答そのものに近い音、つまり「部屋の返事」の生の姿が聞けます。

うまく表示されないときは、デモを別タブで開く

ここで種明かしをひとつ。このデモのインパルス応答は、実際の部屋で録音したものではなく、コードで合成したものです。密な残響の尾は「指数関数的に減衰していくノイズ」でかなりそれらしくなり、そこに初期反射を数発と、時間とともに高音がこもっていく処理を足すと、小部屋やホールの返事が出来上がります。バネ風だけは作りが違っていて、カープラス・ストロングの回 の最後に顔を出した、ギターアンプのスプリングリバーブの「ビョン」という返事を、繰り返すチャープ(音程が滑り落ちる短い音)で真似ています。返事の形さえ用意すれば、実在しない部屋の響きすら作れる、というのも畳み込みの面白いところです。

いちばん下のパネルでは、「配って足す」そのものをステップ実行できます。入力4サンプル、返事5タップのミニチュアで、ボタンを押すたびに1サンプルずつコピーが配られ、いちばん下の出力に積み上がっていきます。マイナスのサンプルがコピーごと上下反転するところも見どころです。

ところで、素のプラック音と残響つきを聴き比べると、乾いた合成音が空間をまとった瞬間にぐっと生々しくなるのが分かるはずです。合成した音の仕上げに空間の響きを着せるのは、音作りの定番の一手です。私事ですが、筆者が個人開発している音感トレーニングアプリ Harmonize も、楽器の音をすべてプログラムの合成で鳴らしていて、この連載はその音作りで学んだ知識の棚卸しとして書いています。

どう実現するか

畳み込みのコードは、図の手順をそのまま二重ループにするだけです。外側のループが「配る」、+= が「足す」に当たります。

/* 畳み込み(C風の擬似コード): x[N] と h[M] から y[N+M-1] を作る */
for (n = 0; n < N + M - 1; n++) y[n] = 0;

for (k = 0; k < N; k++)            /* 入力の1サンプルごとに */
    for (j = 0; j < M; j++)        /* 返事 h のコピーを */
        y[k + j] += x[k] * h[j];   /* 自分の位置 k から配って、足す */

ただし、このループには重さという代償があります。ホールのインパルス応答が2秒あるとすると、$h$ はおよそ88200タップ。1秒ぶんの音を処理するには、44100サンプルそれぞれに88200回の掛け算、合わせて約39億回の掛け算が要ります。素朴に回すとリアルタイムでは間に合いません。実際のコンボリューション・リバーブは、周波数の世界 へ変換すると畳み込みがただの掛け算に化けるという性質(FFTによる高速畳み込み)を使って、この計算を何桁も軽くしています。さらに歴史をさかのぼると、そんな計算力がまだ夢だった1960年代には、Schroeder が短いディレイの再帰(コムフィルタ)と位相だけを散らすフィルタ(オールパス)を数個組み合わせて、それらしい密な残響を安く作る方式を考案していて、長らくデジタルリバーブの主流はこちらでした。畳み込みが正攻法、Schroeder 方式は畳み込みが重かった時代の見事な工夫、という関係です。

ブラウザでは、この正攻法が ConvolverNode という部品として最初から用意されています。インパルス応答を渡すだけで、高速畳み込みまで面倒を見てくれます。

const convolver = ctx.createConvolver();
convolver.buffer = impulseResponse;   // 部屋の返事(AudioBuffer)
source.connect(convolver);
convolver.connect(ctx.destination);

上のデモもこの部品を使っていて、JavaScript 側で書いているのは、インパルス応答を合成する部分だけです。

参考文献

手拍子1発の返事に、部屋のすべてが入っている。まずはデモで、同じ「ポン」が小部屋とホールでまるで別の場所の音になるのを聴いてみてください。