ディジタルフィルタの実装の例として、 C# 言語による実装を示します。
言語に C# を採用する理由は以下の通り。
まず、このページでは、 様々なディジタルフィルタに共通のインターフェースと、 ディジタルフィルタの中でも基礎中の基礎になる「増幅器」および「遅延器」の実装例を示します。
移行のページでは、ディジタルフィルタの説明とともに、 サンプルプログラムを掲載(予定)。
まあ、とりあえず、先に C# のソースを示します。 (プログラミングの知識に関しては、 「インターフェース」 辺りを参照してください。)
/// <summary> /// ディジタルフィルタインターフェース。 /// </summary> public interface IFilter : ICloneable { /// <summary> /// フィルタリングを行い、その結果を返す。 /// </summary> /// <param name="x">フィルタ入力。</param> /// <returns>フィルタ出力。</returns> double GetValue(double x); /// <summary> /// フィルタの内部状態をクリアする。 /// </summary> void Clear(); }
完全版: IFilter.cs
線形フィルタの場合、 信号を1つ入力して、フィルタリング結果の信号を1つ得るようなモデルがよく使われます。 (「信号を1つ」というのは、 音声信号の場合なら「1サンプル」、 画像の場合なら「1画素」とかのことです。 このページでは、主に音声向けに説明しているので、 「1サンプル」という言い方を頻繁に使います。) この、「1サンプルの入力を与え、1サンプルの出力を得る」という操作を行うのが、 GetValue メソッドです。
また、ディジタルフィルタは、内部状態を持っている(過去数サンプル分の入力を記憶していたり)する場合がありますが、 時折、この内部状態を初期状態に戻したい場合があります。 そのための操作用に Clear メソッドも用意しておきます。
最も単純な作りのディジタルフィルタの例として、 増幅器という物を考えて見ます。 増幅器(amplifier)は、 本当に単純に、「入力信号の定数倍を出力する」というフィルタです。
この場合、状態を持つ(過去の信号を記憶したりする)必要も全くなく、 実装も非常に簡単です。 要点だけ抜き出すと、以下の通り。
public class Amplifier : IFilter { double amp; // 倍率 /// <summary> /// 倍率 /// </summary> public double Amplitude { get { return this.amp; } set { this.amp = value; } } public double GetValue(double x) { return this.amp * x; } public void Clear() { } }
完全版: Amplifier.cs
もう1つの基本中の基本となるフィルタである遅延器の説明に入る前に、 過去の信号の記憶について実装例を示します。
遅延器というと、何サンプルか前の信号を出力するフィルタのことです。 したがって、遅延器を作るためには、 過去何サンプルかの信号を記憶しておく必要があります。
そのための記憶領域としては、 以下のようなクラスを作っておくと便利です。
buf[i] と言うような書き方で、i サンプル前の値を読み書きできる。このようなクラスを、ここでは、循環バッファ(circular buffer)と呼びましょう。 (一般用語としては確立した物ではない。 でも、この意味で循環バッファという言葉を使う人が多いはず。)
循環バッファの実装方法は、例えば以下のようになります。
public class CircularBuffer : ICloneable { double[] buf; // 中略 /// <summary> /// n サンプル前の値の取得 /// </summary> /// <param name="n">何サンプル前の値を読み書きするか</param> /// <returns>n サンプル前の値</returns> public double this[int n] { get { return this.buf[n]; } set { this.buf[n] = value; } } /// <summary> /// 値の挿入 /// </summary> /// <param name="x">挿入したい値</param> public void Insert(double x) { for (int n = this.buf.Length - 1; n > 0; --n) { this.buf[n] = this.buf[n - 1]; } this.buf[0] = x; } /// <summary> /// 要素数 /// </summary> public double Count { get { return this.buf.Length; } } }
完全版: CircularBuffer1.cs
この場合、値の読み出しは高速に行えますが、 新しい値を挿入する際に、いちいち全要素を1つずつずらす作業が必要で、 効率がよくありません。 そこで、以下のような実装方法もあります。
public class CircularBuffer : ICloneable { double[] buf; int top; // 中略 /// <summary> /// n サンプル前の値の取得 /// </summary> /// <param name="n">何サンプル前の値を読み書きするか</param> /// <returns>n サンプル前の値</returns> public double this[int n] { get { return this.buf[(n + this.top) % this.buf.Length]; } set { this.buf[(n + this.top) % this.buf.Length] = value; } } /// <summary> /// 値の挿入 /// </summary> /// <param name="x">挿入したい値</param> public void Insert(double x) { --this.top; if (this.top < 0) this.top += this.buf.Length; this.buf[this.top] = x; } /// <summary> /// 要素数 /// </summary> public int Count { get { return this.buf.Length; } } }
完全版: CircularBuffer2.cs
これで、値の挿入時に、全要素を1つずつずらすといった重たい処理はなくなりました。 でも、剰余演算も結構重たい演算で、避けれるならば避けたいものです。 これに対して、バッファ長を2の冪に制限することで、 剰余演算を論理 AND 演算に置き換える方法もあります。
public class CircularBuffer : ICloneable { double[] buf; int length; int top; int mask; /// <summary> /// n サンプル前の値の取得 /// </summary> /// <param name="n">何サンプル前の値を読み書きするか</param> /// <returns>n サンプル前の値</returns> public double this[int n] { get { return this.buf[(this.top + n) & this.mask]; } set { this.buf[(this.top + n) & this.mask] = value; } } /// <summary> /// 値の挿入 /// </summary> /// <param name="x">挿入したい値</param> public void Insert(double x) { --this.top; this.top &= this.mask; this.buf[this.top] = x; } /// <summary> /// 要素数 /// </summary> public int Count { get { return this.length; } } }
完全版: CircularBuffer3.cs
バッファ長を2の冪に制限するために、
「n を以上の最小の整数」を返す関数 CeilPower2(n) を用意したりします。
実装例はこちら → Util.cs
循環バッファさえ出来れば、 遅延器の実装は簡単です。 過去数サンプル分のデータを循環バッファに記憶しておいて、 所望の位置のデータを出力するだけです。
public class Delay : IFilter { CircularBuffer buf; public Delay(int delaytime) { this.buf = new CircularBuffer(delaytime); } public double GetValue(double x) { double y = this.buf[this.buf.Count - 1]; this.buf.Insert(x); return y; } public void Clear() { for (int n = this.buf.Count; n > 0; --n) this.buf.Insert(0); }
完全版: Delay.cs