何かあれば GitHub のリポジトリに issue を作るか ryukau@gmail.com までお気軽にどうぞ。
Update: 2025-01-07
周波数帯域の分割に用いるクロスオーバーフィルタを実装します。
かなりややこしいので概要を設けました。ページ上部の Table of Contents も多少は役に立つかと思います。
以下の 3 つのフィルタを実装します。
IIR の Linkwitz-Riley フィルタは以下の属性の組み合わせによって実装の詳細が 6 つに分かれます。
偶数次、奇数次によってフィルタ係数の計算が変わります。偶数次は 2 次セクションだけで構築できますが、奇数次では 1 次のフィルタが現れます。
2 バンドでは補正オールパスフィルタが不要ですが、 3 バンド以上では補正オールパスフィルタが必要となってきます。また補正オールパスフィルタを計算するタイミングで汎用性と計算効率のトレードオフがあります。
FIR の Linkwitz-Riley フィルタの節では小さい部品から順にクロスオーバーフィルタを組み立てています。つまり:
という流れになっています。
フィルタ係数が左右対称な線形位相の FIR フィルタはクロスオーバーフィルタとして使えます。
素朴な線形位相 FIR フィルタの利点は多様な設計方法が確立していることと実装が簡単なことです。欠点はカットオフ周波数の変更時の計算量が多く、レイテンシが生じることです。カットオフ周波数を変更するとフィルタ係数をすべて書き換える必要があります。また線形位相フィルタは出力の遅れを補正するためにレイテンシが生じます。
窓関数法 (windowed sinc) で使うローパスの線形位相 FIR フィルタ係数は以下の式で計算できます。
さらに窓関数をかけることで周波数特性を変更できます。以下はパラメータ
式 <cmath>
の
cyl_bessel_k
で直接 Kaiser
窓を実装することができます。しかし、この記事を書いた時点では Mac の
Xcode (Apple Clang) では数学特殊関数が使えなかったので式
FIR の畳み込みの計算については「レイテンシのない畳み込み」に詳細を掲載しています。ここでは省略します。
以下は C++ 20 による実装へのリンクです。
フィルタ係数が短いときは上のリンク先のように
std::inner_product
や単純な for
によって簡単に実装できます。フィルタ係数が長いときは「レイテンシのない畳み込み」で紹介しているような
FFT
を用いた畳み込みで効率よく計算しなければリアルタイムの締め切りに間に合いません。
Linkwitz-Riley フィルタは Butterworth を 2 つ直列につないだ IIR フィルタです。以下は Linkwitz-Riley フィルタのブロック線図です。
任意の次数の Butterworth フィルタについて、ローパスを
LP
、ハイパスを HP
とすると Linkwitz-Riley
フィルタによるクロスオーバーは
(LP -> LP) + (HP -> HP) = AP
と表せます。ここで
->
は直列接続、 AP
はオールパスです。つまり、分割した (LP -> LP)
と
(HP -> HP)
を足し合わせても振幅特性が平坦なまま、ということです。
Linkwitz-Riley フィルタの次数は内部で使われる Butterworth
フィルタの 2 倍となります。以降では Linkwitz-Riley フィルタの次数を
3 バンド以上の Linkwitz-Riley フィルタの実装は
分割した信号を投げっぱなしにせず、併合時に再び処理を行えるときは補正オールパスフィルタの数を減らして効率よく計算できます。
2 次セクションは biquad フィルタのローパスとハイパスが使えます。「Biquad フィルタの比較」で実装を紹介しています。
以下は C++ による実装へのリンクです。テンプレートパラメータの
filterType
を lowpass
あるいは
highpass
とすると 2 つ直列につないだ Butterworth
フィルタとなります。
2 次セクションは biquad オールパスを使います。 TPT (topology-preserving transform) という離散化に基づいた biquad オールパスの実装を以下のリンク先に掲載しています。 Audio EQ Cookbook のレシピでも動きます。
2 次セクションの数は
以下は C++ による実装へのリンクです。テンプレートパラメータの
filterType
を allpass
とすると
1 つの 1 次オールパスと
1 次オールパスは以下の伝達関数で表されます。
差分方程式にします。
y0 = a * (x0 - y1) + x1;
x0
: 現在の入力値。x1
: 1 サンプル前の入力値。y0
: 現在の出力値。y1
: 1 サンプル前の出力値。これで計算できる形になりました。
2 次セクションの
以下は Python 3 による実装へのリンクです。
3 バンド以上の分割と併合について 2 つの方法を紹介します。原則としては汎用的な実装を行い、クロスオーバーがボトルネックとなっていれば効率のいい実装を使うことが考えられます。
2 バンドでは補正オールパスフィルタが不要なので以下の処理は実装しなくていいです。
汎用的な帯域分割は補正オールパスの総数が分割するバンド数に応じた三角数 (triangular number) となりますが、単純な加算で併合を行える特長があります。つまりバンド数が多いと分割の計算が重たくなりますが、分割後の使い勝手はこちらのほうがいいです。
処理を書き下します。
以下は Python 3
による実装へのリンクです。クロスオーバー周波数のリスト
cutoffsHz
の長さがバンド数となります。
分割後の信号を再取得できるときは以下のブロック線図のようにオールパスの数を
(バンド数) - 1
に減らすことができます。例えば 1
つの音のプラグインの内部で分割と併合の処理を両方行うときに使えます。この方法は出力を投げっぱなしにできないため汎用性は落ちます。
処理を書き下します。まずは分割します。
キューに格納された各帯域に任意の処理を施したあと、以下の併合の処理を行います。
y
を用意して 0 に初期化。y
に加算。y
に以下の係数を乗算。
y
に加算して出力とする。Martin Vicanek さんによる “A New Reverse IIR Filtering Algorithm” で Linkwitz-Riley フィルタを線形位相の FIR フィルタとして効率よく近似する方法が紹介されています。
Vicanek の論文では 2 次の複素共役フィルタの FIR を効率よく近似して、時間反転フィルタに変換する方法が紹介されています。ここで時間反転 (time reversal) とはインパルス応答の前後を逆向きにすることです。元の前向きフィルタと時間反転フィルタを直列につなぎ合わせることで線形位相フィルタを構築することができます。
Linkwitz-Riley フィルタは、次数の同じ 2 つの Butterworth フィルタを直列につないだフィルタです。つまり 2 つの Butterworth フィルタの片方を前向きフィルタ、もう片方を逆向きフィルタとすることで線形位相 FIR にできます。また Butterworth フィルタは 2 次の複素共役フィルタの組み合わせとして実装できます。
まずは以降で部品として使う 1 次の複素数フィルタの FIR
近似を実装します。以下は “A New Reverse IIR Filtering Algorithm” の式
1 として記載されている、 1 次の複素数フィルタの伝達関数です。
何をしているのかというと、
時間反転を行うときは式
以下は式
この節の式やブロック線図では
2 次の複素共役フィルタの FIR 近似は 1 次の複素数フィルタの FIR
近似を使って効率よく実装できます。以下は 2
次の複素共役フィルタの伝達関数です。
ここで 1 次の複素数フィルタの出力を
上の式は “A New Reverse IIR Filtering Algorithm” の式 11 で示されている 2 次の複素共役フィルタの FIR 近似の効率のいい計算方法です。以下はこの計算のブロック線図です。
以下は C++ と Python 3 による実装へのリンクです。
ComplexIIR
クラスの process1PoleForward
と
process1PoleReversed
が 1 次の複素数フィルタの FIR
近似の計算、 process2PoleForward
と
process2PoleReversed
が 2 次の複素共役フィルタの FIR
近似の計算です。
C++ の実装はステージ数をテンプレートパラメータ stage
として指定するために、テンプレートの再帰を使っています。理由はよくわかりませんが、再帰を展開した実装よりも、テンプレートの再帰を使った実装のほうが
cl.exe
(Version 19.36.32532 for x64)
では速くなりました。
2 次の複素共役フィルタの FIR 近似を用いて Butterworth フィルタの FIR 近似を行います。この節では「ステップ応答が S 字を描くフィルタ」に掲載した Bessel フィルタを 2 次セクションに分割する手法を流用しています。 Butterworth フィルタも Bessel フィルタと同様に全極フィルタなので同じ手法で 2 次セクションに分割できます。
バイリニア変換した離散系の
ここで Butterworth の 2 次セクションと 2
次の複素共役フィルタに違いがあります。 Butterworth の 2
次セクションは分子が
Butterworth フィルタの複素共役を除いた極
さらに計算の途中で内部の値が大きくなりすぎることを防ぐために
式
ここまでをまとめると以下のブロック線図となります。フィルタは線形なので
Output
の直前に動かしても動作します。
以下は C++ による実装へのリンクです。
線形位相なのでハイパスは遅延した入力からローパスを減算すれば得られます。つまりハイパスの計算は省略できます。分割と併合についても IIR の Linkwitz-Riley のような補正オールパスは不要です。
以下は C++ による実装へのリンクです。
😀 は良、 👹 は悪を表しています。
素朴な FIR | Linkwitz-Riley IIR | Linkwitz-Riley FIR | |
---|---|---|---|
線形位相 | 😀 | 👹 | 😀 |
レイテンシ | 👹 | 😀 | 👹 |
実装の容易さ | 😀 | 2 バンド 😀 / 3 バンド以上 👹 | 👹 |
クロスオーバーの対称性 | 👹 | 😀 | 😀 |
低レイテンシが要件なら Linkwitz-Riley IIR が適しています。線形位相が要件かつ手軽に実装したいなら素朴な FIR 、実装に手間をかけてもいいなら Linkwitz-Riley FIR が適しています。
クロスオーバーの対称性はフィルタの振幅特性のスロープやストップバンドでの低減率などがローパスとハイパスで対称かということです。非対称を 👹 としています。
素朴な FIR 、 Linkwitz-Riley IIR 、 FIR Linkwitz-Riley の 3 つについて、 2 バンドの場合の簡単なベンチマークを取りました。
Name | Elapsed [ms] |
---|---|
LinkwitzRileyIIR2Band4n | 1.4590 |
LinkwitzRileyFIR2Band4n | 2.4562 |
WindowedFIR2Band | 2.8668 |
以下はベンチマークに使ったコードへのリンクです。
以下はパラメータの設定です。
結果を見ると IIR の Linkwitz-Riley が速く、 FIR の実装はあまり差がありません。ただし、素朴な FIR は畳み込みの計算について大いに改善の余地があります。また Linkwitz-Riley FIR のステージ数や、素朴な FIR のフィルタ係数の数によって結果が変わることが予想されます。
このベンチマークは実は素朴な FIR で十分ではないか、という疑問を解消するために行ったのでパラメータを細かく変えてトレンドを調べていません。クロスオーバー周波数をリアルタイムで変更する用途であれば、素朴な FIR のフィルタ係数の更新がかなりの負荷になるので、 Linkwitz-Riley FIR の利用価値はあるように見えます。クロスオーバー周波数が固定なら素朴な FIR でも十分な気がします。