概要
ディジタルフィルタの実装の例として、 C# 言語による実装を示します。
言語に C# を採用する理由は以下の通り。
-
ディジタルフィルタとオブジェクト指向は相性がよい
-
オブジェクト指向言語といえば、今の時代、Java か C#
-
このサイトは C# サイトなので、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つの基本中の基本となるフィルタである遅延器の説明に入る前に、 過去の信号の記憶について実装例を示します。
遅延器というと、何サンプルか前の信号を出力するフィルタのことです。 したがって、遅延器を作るためには、 過去何サンプルかの信号を記憶しておく必要があります。
そのための記憶領域としては、 以下のような「クラス」を作っておくと便利です。
-
n サンプル前までの値を保持している。
-
n + 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