How to make FIRFilter Audio Plugin

dsp
projucer
日本語

#1

初めまして。
当方、JUCE開発、デジタルフィルタ開発、プログラミング初心者です。
今回私はJUCEでダウンサンプリングをするオーディオプラグインの開発をしようとしています。そこで、私はまずフィルタを設計することからはじめました。
作成しようとしたフィルタはFIRフィルタです。しかし、作成したオーディオプラグインを使用して見ると、出力音声が一定間隔で0になってしまうという現象が起こってしまいました。

写真はサイン波を作成したFIRFilterに通して.wavで出力した波形の一部です。


void FirFilter4AudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    const int totalNumInputChannels  = getTotalNumInputChannels();
    const int totalNumOutputChannels = getTotalNumOutputChannels();

    // In case we have more outputs than inputs, this code clears any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    // This is here to avoid people getting screaming feedback
    // when they first compile a plugin, but obviously you don't need to keep
    // this code if your algorithm always overwrites all the output channels.
    for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());
    
    
    
    if(UserParams[MasterBypass] >= 1.0f)
        return;
    
    

    // This is the place where you'd normally do the guts of your plugin's
    // audio processing...
    for (int channel = 0; channel < totalNumInputChannels; ++channel)
    {
        // float* channelData = buffer.getWritePointer (channel);
        
        // ..do something to the data...
        
        
        
        
        ///////////////////イコライザー処理の開始///////////////////////
        // 関数SetParameterに渡す引数の値を算出
        float _sampleRate = getSampleRate(); 
        UserParams[SampleRate] = _sampleRate;
        
        float _frequency;
        
        
    
        UserParams[Frequency] = 0.5664f; // カットオフ周波数を 1000Hz に設定
        _frequency = 20.0f * pow(1000.0f, UserParams[Frequency]); // 中心周波数を決定する(範囲 20~20000 Hz)
        

        if(channel < 2)
        {
            // フィルタ係数を算出して保持する関数を実行
            // firFilter[channel].SetParameter(_sampleRate, _frequency, _q);
            
            
            
            
            
            ///////////////////////////////// 遅延器の数を設定 /////////////////////////////////////
            
            
            
            
            //        j = numDelayer(_frequency / _sampleRate);
            j = firFilter[channel].numDelayer(1000. / _sampleRate);
           
            /////////////////////////////////// ハニング窓 ///////////////////////////////////
            
            
            float* w = new float[j+1];
            float* b = new float[j+1];
            
            
            
            firFilter[channel].hammingWindow(w, j+1);
          
            /////////////////////////////////// fir LPF ///////////////////////////////////
            
            
            firFilter[channel].SetParameter(_sampleRate, _frequency, j, w, b);
            
        
            /////////////////////////////////// 書き込み ///////////////////////////////////
        
            firFilter[channel].DoProcess(buffer.getWritePointer(channel), buffer.getNumSamples(), j, b);
            
            
            delete[] w;
            delete[] b;
        }
    }
}

以上がprocessBlockの中身です。


#include "FIR_filter.h"
#include 


// コンストラクタと変数の初期化、インスタンス生成と同時にヘッダで宣言した変数を初期化する
FIR_filter::FIR_filter()
{}


// デストラクタ
FIR_filter::~FIR_filter() {}


// LPF
void FIR_filter::SetParameter(float samplerate, float frequency, int j, float w[], float b[])
{
    int offset, m;
    
    offset = j / 2;
    for(m = (-j / 2); m < (j / 2) + 1; m++)
    {
        b[offset + m] = 2.0 * (frequency / samplerate) * sinc(2.0 * M_PI * (frequency / samplerate) * m);
        
    }
    
    for(m = 0; m < j+1; m++)
    {
        b[m] *= w[m];
    }
}


// オーティオデータにフィルタを適用する関数
// float bufferPtr ... オーディオバッファのポインタ
// int bufferSize ... オーディオバッファのサイズ おそらく197サンプル
void FIR_filter::DoProcess(float* bufferPtr, int bufferSize, int j, float b[])
{
    float* tmp = new float[bufferSize];
    float* win = new float[bufferSize];
    
    // hammingWindow(win, bufferSize);
    
    for(int n = 0; n < bufferSize; n++) // オーディオバッファのサンプル数まで回す
    {
        float d = 0.0;
        for(int mm = 0; mm < j+1; mm++) // タップ数回す
        {
            if((n - mm) >= 0)
            {
                
                d += b[mm] * bufferPtr[n - mm];
               
            }
        }
        tmp[n] = d;
    }
    for(int n = 0; n < bufferSize; n++)
    {
        // bufferPtr[n] = tmp[n] * win[n]; // ここに窓をかけた
        bufferPtr[n] = tmp[n];
    }
    
    
    delete[] tmp;
    delete[] win;
    
}









float FIR_filter::sinc(float x)
{
    float y;
    
    if(x == 0.0)
    {
        y = 1.0;
    }
    else
    {
        y = sin(x) / x;
    }
    
    return y;
}




void FIR_filter::hammingWindow(float w[], int N)
{

    
    
    int n;
    
    
    
//    if(N % 2 == 0) // (j+1)が偶数の時
//    {
//        for(n = 0; n < N; n++)
//        {
//            w[n] = 0.5 - 0.5 * cos(2.0 * M_PI * n / N);
//        }
//    }
//    else
//    {
//        for(n = 0; n < N; n++)
//        {
//            w[n] = 0.5 - 0.5 * cos(2.0 * M_PI * (n + 0.5) / N);
//            // w[n] = 0.5 - 0.5 * cos(2.0 * M_PI * (n) / N);
//        }
//    }
    
    
    
    for(n = 0; n < N; n++)
    {
        w[n] = 0.54 - 0.46 * cos(2.0 * M_PI * n / N);
    }
    
    
    
    
}






int FIR_filter::numDelayer(float delta)
{
    int j = (int)(3.1 / delta + 0.5) - 1; // 遅延器の数 // 136.21
    // int j = (int)(3.1 / delta) - 1;
    
    if(j % 2 == 1)
    {
        j++; // j+1が奇数なるように調整
    }
    
    return j;
}


以上が作成したFIRFilterのコード部分です。

私の考えでは、processBlockで呼ばれる入力が197サンプル(おそらく… はっきりわかってないです)なので、この関数が呼び出されるごとにFIRFilterの係数がリセットされているからだと考えているのですが、もしこの考えがあっているとするならば解決策などはあるのでしょうか。

初歩的な質問で申し訳ないのですが、よろしくお願いいたします。


#2

この問題は初心者にしょっちゅう起きる現象ですね。やはりこのようにチャンネルごとにサンプルブロックをプロセスすると間違った係数で次のブロックが計算されてしまう。

以上のやり方でフィルタを設計すると例えば左チャンネルのブロックが計算された後、そのまま同じリセットされていない係数で右チャンネルが計算されてしまう。

そのため、解決する方法は二つ。あるいは係数をブロックごとにリセットするか、もしくはサンプルブロックをチャンネルごとにプロセスすること。

内側のループがチャンネルであれば、係数はブロックごとに同じなので左右のチャンネルを同時にプロセスできます。


#3

あなたのコードに見られる他の問題は、ProcessBlockメソッドでメモリを動的に割り当てることです。 あなたはそれを避けるべきです。

Note: Used Google Translate. Hope it translate well.


#4

@noahdayan
返信ありがとうございます。

係数をブロックごとにリセットするというのはdeleteでリセットできてないのでしょうか。
また、係数をブロックごとにリセットするにはどのような処理を実装すれば良いのでしょうか。
for (int channel = 0; channel < totalNumInputChannels; ++channel){}
の前に係数を計算する以下の処理を追加すれば良いのでしょうか。
j = firFilter[channel].numDelayer(1000. / _sampleRate);
firFilter[channel].hammingWindow(w, j+1);
firFilter[channel].SetParameter(_sampleRate, _frequency, j, w, b);
firFilter[channel].DoProcess(buffer.getWritePointer(channel), buffer.getNumSamples(), j, b);


#5

@Godwin
Thank you for your reply
understood.
I will deal with it.

Used Google Translate.


#6

投稿から時間が経ってからのレスで申し訳ありません。
私は日本人ですので、日本語でやり取りをすることができます。

貴方のコードを未だ私の方でテストはしていないのですが、一点気になるところがありまして、以下の点ご確認いただければと思います。
こちらの実装コードのVSTプラグインですが、ホストでロードしたときのVSTの負荷モニタはいくつぐらいを示していますでしょうか?
これは私の推測なのですが、 processBlock内でバッファサイズ×チャンネル数のnewを実行しているため、processBlockの負荷も比較的高いのではないかと思っています。

というのも、私も似た経験がありまして、一般的なDAWでは、VST負荷が高い時にはレンダリングサイクルを途切れさせないように、あえて処理をスキップする間引き運転のような挙動をすることがあります。もしかしたら、FIRフィルターの計算に対して間引き運転がされてしまってい、そのような波形出力になっているのかもしれません。


#7

返信ありがとうございます。
09
FLStudioでプラグインを読み込ませて440Hzのサイン波を流して見たのですが、それほど負荷はかかってないと思います。
他の人にも聞いてみたところ、処理が追いついて無いのではなどの意見もありました。

あれから色々改良してみたのですが、DAW上で作成したプラグインをかませて音源を鳴らしてみると大きなノイズが入るのですが、書き出して聞いてみるとノイズは入っているのですがほぼほぼ聞こえず、ちゃんとカットオフ周波数の部分で落ちているというよくわからない状態になってしまいました・・・。(参考:http://blog.livedoor.jp/sce_info3-craft/archives/8635081.html)

P.S.
JUCE JAPAN の本読ませていただいてます。
vol.3 楽しみにしています。