読者です 読者をやめる 読者になる 読者になる

よなかのすうがく

Audio signal processing and Electronic kit, and hobby.

AudioUnitEffectExample読み解き #その1

こういうソース解説の記事って自分がよくお世話になったりしたので自分で書いてみるけど、基本は自分の自己満足兼備忘録。

今回の対象は、以前の記事で落としたであろうAudioUnitのサンプルコード群の中にAudioUnitEffectExample。
本来の目的はエフェクタな訳で、他のAudioUnitGeneratorExample(PinkNoise生成)やAudioUnitInstrumentExample(おそらくMIDI制御のsin波シンセ)には目もくれず、これを弄ろう。

最初、StarterAudioUnitExample(Tremolo)を弄ろうと思ったんだけど、何かぱっとみ書き方がややこしい
(読むのが面倒くせえ)ので、見た感じがすっきりしているFilterDemoさんを見ていこうという感じ。
あとCocoaGUIが用意されているので要らない場合はCocoaGUI関係のソースコードを消せば、StarterAudioUnitExample(Tremolo)のような初期のパラメータ設定ウィンドウが表示されるので、今回はそうします。
そのため、実はFilter.hをゴミ箱にポイーしても大丈夫です。ただでさえ中身が少ないのによく見たら全部CocoaGUIに関するものだったので。

ここから内部処理に触れていきますが、著者はC++を触ったことある程度の人間ですので何か間違いや勘違いなどがあると思われますが、その時はコメントか何かで報告を……。



開発環境 : Yosemite(10.10.3), Xcode 6.3


まずはFilter.cppから順に読み取っていきましょう。

・FilterKernel

class FilterKernel : public AUKernelBase
{
public:
    FilterKernel(AUEffectBase *inAudioUnit);
    virtual ~FilterKernel();
    virtual void        Process(    const Float32   *inSourceP,
                                    Float32         *inDestP,
                                    UInt32          inFramesToProcess,
                                    UInt32          inNumChannels,
                                    bool            &ioSilence );
    virtual void        Reset();
    void                CalculateLopassParams(double inFreq, double inResonance);
    double              GetFrequencyResponse(double inFreq);

private:
    // filter coefficients
    double	mA0;
    double	mA1;
    double	mA2;
    double	mB1;
    double	mB2;

    // filter state
    double	mX1;
    double	mX2;
    double	mY1;
    double	mY2;
    
    double	mLastCutoff;
    double	mLastResonance;
};

FilterKernelクラスから見ていくと、まずはこれはAUKernelBaseを継承していることが分かる。まあ基本的にAU作る時はベースとなるクラスを継承することで実装していくのでしょう。

・デストラクタのvirtual

まずFilterKernel( … )はコンストラクタ、virtual ~FilterKernel( )はデストラクタである。
ここでAudioUnitsとは関係ないけど、何でデストラクタにはvirtualがいるんだろう?(小並感)ということで調べると、"If your class has virtual methods, its destructor should be virtual.(virtual なメソッドを持つクラスのデストラクタは virtual でなくてはならない。)"とのこと。
詳しい内容はC++ でデストラクタを virtual にしなくてはならない条件と理由に詳しく書いてあるので参照してください。
つまり要約すると"virtualを使うと子クラスのインスタンスは親クラスのポインタに格納されるので、デストラクタにvirtualがないと子クラスのデストラクタではなく親クラスのデストラクタが暗黙的に呼び出される"という感じ。

・FilterKernel::Process

次にvirtual void Process( … )という関数があるのですが、これがこのAudioUnitsで最も重要な音響処理を行うメソッドです。なのでここの処理が書かれている部分に移りましょう。

// We process one non-interleaved stream at a time
void FilterKernel::Process( const Float32 	*inSourceP,
                            Float32 		*inDestP,
                            UInt32 		inFramesToProcess,
                            // for version 2 AudioUnits inNumChannels is always 1
                            UInt32		inNumChannels,
                            bool		&ioSilence )
{
    // [1]
    double cutoff = GetParameter(kFilterParam_CutoffFrequency);
    double resonance = GetParameter(kFilterParam_Resonance);
    
    if(cutoff < kMinCutoffHz) cutoff = kMinCutoffHz;    
    if(resonance < kMinResonance) resonance = kMinResonance;
    if(resonance > kMaxResonance) resonance = kMaxResonance;
    
    // [2]
    float srate = GetSampleRate();
    cutoff = 2.0 * cutoff / srate;
    if(cutoff > 0.99) cutoff = 0.99;
    if(cutoff != mLastCutoff || resonance != mLastResonance)
    {
        CalculateLopassParams(cutoff, resonance);
        mLastCutoff = cutoff;
        mLastResonance = resonance;		
    }

    // [3]
    const Float32 *sourceP = inSourceP;
    Float32 *destP = inDestP;
    int n = inFramesToProcess;
    while(n--)
    {
        float input = *sourceP++;
        float output = mA0*input + mA1*mX1 + mA2*mX2 - mB1*mY1 - mB2*mY2;

        mX2 = mX1;
        mX1 = input;
        mY2 = mY1;
        mY1 = output;   
        *destP++ = output;
    }
}

とりあえず大体処理を3分割して順繰り読み取っていこう……となる前に最初は処理に行く前に引数を見ていきます。
const Float32 *inSourcePFloat32 *inDestPは入力信号ポインタと出力信号ポインタ。入力ポインタにはしっかりとconstで変更されないようにされている。まあ当然だよね。
そしてもう一つがこの入力された信号は先頭のコメントアウトにあるようにnon-interleavedな信号であると書かれている。

Non-interleaved

ここで"non-interleavedってなんだよ?"と疑問に思ったので調べると、ステレオ入力にする際に入力信号が"LRLRLRLRLRLR"のようにLとRが交互に書き込むのがinterleaved。"LLLLLLRRRRRR"とチャンネル毎に分けたものをnon-interleavedというらしい。なのでAudioUnitsでステレオ出力をする際にはnon-interleavedで書き込まなきゃダメみたい。

そこで、どうやってステレオ出力するんだろう(何かコメントにfor version 2 AudioUnits inNumChannels is always 1ってあるし)と調べてみたら、AUKernelBaseのvirtual OSStatus ProcessBufferLists( … )をこのクラスでオーバーライドする必要があるらしい。ただよく分かってないので、ここは実際に試すことにして後回し。


では[1]の処理を見ていくとここはcutoffとresonanceの値をGetParameter()で取ってるようだ。引数のkFilterParam_CutoffFrequencyやkFilterParam_Resonanceってのは後々のソースコードでわかると思うがenumで定義された構造体で中身はただの整数(パラメータのIDみたいなもの)。つまりそのIDのパラメータを引数にGetParameter()でとっていることが分かる。その後の分岐はただの制限値内にパラメータが収まっているかを判定しているだけである。

次に[2]の処理は、簡単に言うとフィルタ係数設計。最初にサンプリングレートをGetSampleRateをsrateに代入して、cutoffとresonanceとsrateでフィルタ係数を設計している。
そしてcutoffの上限設定やcutoffやresonanceが変更されたら係数を更新したりしていることが分かる。

最後に[3]の処理では入力信号にフィルタ係数を掛けたものを出力信号のポインタに出力している。LPFの実装方法は今回の説明では割愛するけど、現在の値と一つ前と二つ前の値に対して係数をかけて足し合わせることと思ってもらえれば大体正解である。その係数を生成している関数がCalculateLopassParams( … )であり以下のような処理をしている。

void FilterKernel::CalculateLopassParams(   double inFreq,
                                            double inResonance )
{
    double r = pow(10.0, 0.05 * -inResonance);	// convert from decibels to linear
    
    double k = 0.5 * r * sin(M_PI * inFreq);
    double c1 = 0.5 * (1.0 - k) / (1.0 + k);
    double c2 = (0.5 + c1) * cos(M_PI * inFreq);
    double c3 = (0.5 + c1 - c2) * 0.25;
    
    mA0 = 2.0 *   c3;
    mA1 = 2.0 *   2.0 * c3;
    mA2 = 2.0 *   c3;
    mB1 = 2.0 *   -c2;
    mB2 = 2.0 *   c1;
}

まあ色々な計算をしてそれぞれの結果をmA0, mA1, mA2, mB1, mB2に代入しているという認識でいいと思われる。詳しく調べたいなら自分で調べよう。とにかくこれを入力信号に掛け合わせてLPFの完成。

・CocoaGUI処理の削除

今回はCocoaGUIの処理は使わないので、それに関する余分なソースコードは省いていく必要があります。
まず削っていくのは、FilterKernel::Process()内のGetFrequencyResponse( … )という関数。ばっさりとクラス内の宣言と処理の部分を削っていきましょう。
次に今回では触れていなかったFilterクラスのメソッドFilter::GetParameterInfo( … )Filter::GetPropertyInfo( … )の内部処理やらFilter::Initialize( )などをStarterAudioUnitExample(Tremolo)を参考にしながら以下のように削っていきましょう。
一応しっかりと動作しているか確認したけど大丈夫そうだったので、このままで進めていきましょう。

/* CocoaGUIでしか使わない処理
OSStatus    Filter::Initialize()
{
	OSStatus result = AUEffectBase::Initialize();
	
	if(result == noErr )
	{
		// in case the AU was un-initialized and parameters were changed, the view can now
		// be made aware it needs to update the frequency response curve
		PropertyChanged(kAudioUnitCustomProperty_FilterFrequencyResponse, kAudioUnitScope_Global, 0);
	}
	
	return result;
}
*/

OSStatus    Filter::GetPropertyInfo (   AudioUnitPropertyID inID,
                                        AudioUnitScope      inScope,
                                        AudioUnitElement    inElement,
                                        UInt32              &outDataSize,
                                        Boolean             &outWritable )
{
	/* CocoaGUIでしか使わない処理
	if (inScope == kAudioUnitScope_Global)
	{
                   :
	}
	 */ 
	return AUEffectBase::GetPropertyInfo (inID, inScope, inElement, outDataSize, outWritable);
}

OSStatus    Filter::GetPropertyInfo (   AudioUnitPropertyID inID,
                                        AudioUnitScope      inScope,
                                        AudioUnitElement    inElement,
                                        UInt32              &outDataSize,
                                        Boolean             &outWritable )
{
	/* CocoaGUIでしか使わない処理
	 if (inScope == kAudioUnitScope_Global)
	 {
                 :
	 }
	 */
	return AUEffectBase::GetPropertyInfo (inID, inScope, inElement, outDataSize, outWritable);
}

これがこのAudioUnitsにおけるメインの処理。後はパラメータ情報の管理とかそういう類の処理なので今回はここら辺で区切るとします。
結構疲れたけど、また次回。