サウンドプログラミングで音色を変えるとき、いちばん基本になる道具がフィルターです。フィルターは、音を「加工する魔法の箱」ではありません。波形の中から、速く変化する部分を弱めたり、ゆっくり変化する部分を弱めたりする計算です。
この記事では、ローパスとハイパスに絞ります。まずは「ローパスのカットオフ周波数を、デジタルの1サンプルごとの計算へどう変換するのか」をはっきりさせます。
ローパスは波形をならす
ローパスフィルターは、低い周波数を通し、高い周波数を落とすフィルターです。言い換えると、波形の急な変化をならします。矩形波やノコギリ波の角が丸くなり、音は明るさを失って、こもった方向へ変わります。
いちばん直感的なローパスは、移動平均です。いまの値だけでなく、少し前の値もまとめて平均します。たとえば4個の値を平均するなら、次のようになります。
$$ y[n] = \frac{x[n]+x[n-1]+x[n-2]+x[n-3]}{4} $$
$x[n]$ は入力、$y[n]$ は出力です。$n$ は「何番目の値か」を表す番号です。最後に4で割っているのは、4個の値を足して平均しているからです。3個を平均するなら3で割りますし、8個を平均するなら8で割ります。
この式は、波形の細かいギザギザをならします。細かいギザギザは高い周波数成分なので、高域が落ちます。まずはこれがローパスの出発点です。
実際のサウンドプログラミングでは、次のような形もよく使います。
$$ y[n] = ax[n] + (1-a)y[n-1] $$
これは、いまの入力 $x[n]$ と、ひとつ前の出力 $y[n-1]$ を混ぜる式です。$a$ が大きいほど入力に素早く追従し、$a$ が小さいほど前の出力を強く引きずります。前の値を引きずるほど、急な変化には追いつけません。だから高域が落ちます。
コードにすると、1サンプルごとの処理は次の形です。
let y = 0;
function lowpass(x, a) {
y = a * x + (1 - a) * y;
return y;
}
ここではまだ、カットオフ周波数をどう計算するかは置いておきます。まず大事なのは、ローパスが「前後の値を混ぜて、急な変化をならす」処理だということです。
ハイパスは低域を引き算で作る
ハイパスフィルターは、ローパスの逆です。高い周波数を通し、低い周波数を落とします。低域の濁りを減らす、風切り音や床振動を切る、音を細くする、といった用途で使います。
ハイパスは、ローパスを使うとかなり直感的に書けます。
$$ \mathrm{low}[n] = \mathrm{LPF}(x[n]) $$
$$ \mathrm{high}[n] = x[n] - \mathrm{low}[n] $$
入力から低い成分を取り出し、それを元の音から引く。すると、残るのは高い成分です。
別の書き方をすると、ハイパスは「ひとつ前の値からどれだけ変わったか」を見る計算でもあります。ゆっくりした変化では差が小さく、急な変化では差が大きくなります。だからハイパスは、ゆっくりした低域を落として、速い変化を残します。直接の式は、ローパスの係数を求めたあとで出します。
デジタルでは時間をサンプル数へ置き換える
ここから、ローパスとハイパスをデジタルの計算へ落とします。
コンピューターの中では、音は一定間隔で並んだ数の列です。44.1kHzなら1秒に44100個、48kHzなら1秒に48000個の値が並びます。この「1秒に何個の値を扱うか」がサンプリング周波数です。ここでは $f_s$ と書きます。
1サンプルぶんの時間は、次のようになります。
$$ \Delta t = \frac{1}{f_s} $$
一方、ローパスやハイパスで指定するカットオフ周波数は、音としての周波数です。ここでは $f_c$ と書きます。$f_c = 1000\text{Hz}$ なら、1秒に1000回振動するあたりを境目として考える、という意味です。
ただし、カットオフ周波数は「ここから先を完全に消す壁」ではありません。ローパスなら、そのあたりから高域が落ち始める目安です。ハイパスなら、そのあたりから低域が落ち始める目安です。一次フィルターでは、カットオフ周波数で振幅がおよそ $-3\text{dB}$ になります。
デジタルで必要なのは、$f_c$ を「1サンプル進む間に、どれくらい変化を許すか」という係数へ変換することです。その係数が、さきほどのローパス式に出てきた $a$ です。
カットオフ周波数から係数aを求める
一次ローパスは、「入力へ向かって、少しずつ近づく計算」と見られます。
$$ y[n] = y[n-1] + a(x[n] - y[n-1]) $$
$x[n] - y[n-1]$ は、いまの入力と、ひとつ前の出力の差です。$a$ は、その差のうち何割だけ近づくかを決めます。
- $a$ が大きい: 入力へ速く近づくので、高い成分まで残りやすい
- $a$ が小さい: 入力へゆっくり近づくので、急な変化がならされる
つまり、$a$ はただの謎の定数ではなく、「1サンプルごとに入力へ近づく割合」です。
では、この $a$ を $f_c$ からどう作るのか。ここで時定数 $\tau$ を使います。一次ローパスでは、カットオフ周波数と時定数に次の関係があります。
$$ \tau = \frac{1}{2\pi f_c} $$
RC回路で言えば $\tau = RC$ ですが、ここでは抵抗やコンデンサの話ではなく、「どれくらいの時間で反応するフィルターか」を表す量として見れば十分です。
連続時間の一次ローパスは、ざっくり言うと次の関係です。
$$ \frac{dy}{dt} = \frac{x-y}{\tau} $$
左側は「出力が時間に対してどれだけ変わるか」です。右側は「入力と出力の差が大きいほど、出力が速く動く」という意味です。
これをデジタルにするため、微分を1サンプルぶんの差に置き換えます。
$$ \frac{y[n]-y[n-1]}{\Delta t} \simeq \frac{x[n]-y[n]}{\tau} $$
ここから $y[n]$ について解きます。
$$ \tau(y[n]-y[n-1]) = \Delta t(x[n]-y[n]) $$
$$ (\tau+\Delta t)y[n] = \Delta t x[n]+\tau y[n-1] $$
したがって、
$$ \begin{aligned} y[n] &= \frac{\Delta t}{\tau+\Delta t}x[n] \ &+ \frac{\tau}{\tau+\Delta t}y[n-1] \end{aligned} $$
この式を、最初のローパス式と見比べます。
$$ y[n] = ax[n] + (1-a)y[n-1] $$
すると、
$$ a = \frac{\Delta t}{\tau+\Delta t} $$
です。これで、$a$ がどこから出てきたのかがつながります。連続時間の「なめらかに近づく動き」を、1サンプルごとの計算に置き換えた結果です。
さらに、$\Delta t = 1/f_s$、$\tau = 1/(2\pi f_c)$ なので、$a$ は $f_c$ と $f_s$ から計算できます。
$$ a = \frac{2\pi f_c}{f_s + 2\pi f_c} $$
たとえば、$f_s = 44100\text{Hz}$、$f_c = 1000\text{Hz}$ なら、
$$ a \simeq 0.125 $$
です。この値は、「1サンプルごとに入力との差の約12.5%だけ近づく」という意味です。
同じ係数でハイパスも作れる
ローパスの $a$ が決まれば、ハイパスも同じカットオフ周波数で作れます。いちばん分かりやすいのは、まず低域を取り出して、それを元の音から引く方法です。
$$ \begin{aligned} \mathrm{low}[n] &= \mathrm{low}[n-1] \ &+ a(x[n]-\mathrm{low}[n-1]) \end{aligned} $$
$$ \mathrm{high}[n] = x[n]-\mathrm{low}[n] $$
これは「ローパスで残した低域を、入力から引く」というだけです。同じ $a$ を使うので、ローパスとハイパスの境目も同じ $f_c$ になります。
ハイパスだけを直接書く式もあります。
$$ y_{\mathrm{HP}}[n] = \beta(y_{\mathrm{HP}}[n-1]+x[n]-x[n-1]) $$
ここで、
$$ \beta = \frac{\tau}{\tau+\Delta t} $$
です。ローパスの $a$ と見比べると、$\beta = 1-a$ です。ゆっくりした変化では $x[n]-x[n-1]$ が小さくなるので、低域が残りにくくなります。急な変化では差が大きくなるので、高域が残ります。
インパルス応答で尾の長さを見る
インパルス応答は、最初だけ1で、あとは0になる信号を入れたときの反応です。
$$ 1, 0, 0, 0, 0, \dots $$
4点移動平均にこれを入れると、出力は次のようになります。
$$ \frac{1}{4},\frac{1}{4},\frac{1}{4},\frac{1}{4},0,0,\dots $$
4個の値を同じ重みで平均するので、$1/4$ が4回だけ出ます。反応がそこで終わるので、移動平均はFIR、Finite Impulse Responseです。
一方、一次ローパスにインパルスを入れると、最初の反応は $a$ です。次のサンプルでは、その残りに $(1-a)$ がかかります。つまり、反応は次のように続きます。
$$ a,\ a(1-a),\ a(1-a)^2,\ a(1-a)^3,\ \dots $$
$a$ が大きいと、入力へすぐ近づくので尾は短くなります。これはカットオフ周波数が高い状態です。$a$ が小さいと、前の出力を長く引きずるので尾が長くなります。これはカットオフ周波数が低い状態です。
このように、前の出力を次の計算に戻すフィルターは、インパルスへの反応が理論上ずっと続きます。これがIIR、Infinite Impulse Responseです。ここでのIIRは難しい別物ではなく、単に「過去の出力を使うので、反応が尾を引く」という意味です。
デモで確認する
下のデモでは、ノコギリ波、矩形波、ノイズを、ローパスとハイパスへ通して比較できます。カットオフ周波数を動かすと、$f_c$ が $f_s$ に対してどの位置にあるかを計算し直し、音と周波数特性を更新します。
うまく表示されないときは、デモを別タブで開く 。
周波数特性の図では、上にある帯域ほど通り、下にある帯域ほど小さくなります。ローパスでは高域が落ち、ハイパスでは低域が落ちます。時間波形では、ローパスは角を丸め、ハイパスはゆっくりした揺れを削ります。
ここで扱った一次ローパスと一次ハイパスには、追加の鋭さ調整つまみは置いていません。カットオフ周波数を決め、サンプリング周波数と合わせて係数 $a$ を求める。まずはここが土台です。
参考文献
- 青木直史『サウンドプログラミング入門――音響合成の基本とC言語による実装 』技術評論社(2013)。音響合成やフィルター処理をC言語実装と合わせて学べる定番書。
- 青木直史『C言語ではじめる音のプログラミング――サウンドエフェクトの信号処理 』オーム社(2008)。フィルター、ディレイ、歪みなど、サウンドエフェクト側の信号処理を扱う姉妹編。
- RC回路からデジタル係数を導く話は、デジタル信号におけるRCローパスフィルタ とデジタル信号におけるRCハイパスフィルタ に分けています。この記事では、そこへ踏み込みすぎないようにしています。