プログラミング」カテゴリーアーカイブ

Core Audio その7 IOProc

Core Audioのオーディオデバイスと入出力のデータをやり取りするには、入出力を行う関数を登録したあと、デバイスの動作を開始させます。入出力の関数の登録を行う関数は<CoreAudio/AudioHardware.h>で宣言されていて、AudioDeviceCreateIOProcIDという関数になります。Tiger以前はAudioDeviceAddIOProcという関数でしたが、LeopardからはDeprecatedになって、こちらに変わっています。

extern OSStatus
AudioDeviceCreateIOProcID(  AudioDeviceID           inDevice,
                            AudioDeviceIOProc       inProc,
                            void*                   inClientData,
                            AudioDeviceIOProcID*    outIOProcID)

inDeviceはAudioDeviceID、inClientDataは任意のデータ、inProcには入出力に使う関数名を渡します。outIOProcIDには登録ごとにIDナンバーが返ってきます。これがLeopardの追加部分なのですが、関数名でなくIDで登録を管理するように
なり、同じIOProcに複数のデバイスを登録しても大丈夫みたいな感じのようです。IOProc関数の引数の構成は決められていて以下のように宣言されています。

typedef OSStatus
(*AudioDeviceIOProc)(   AudioDeviceID           inDevice,
                        const AudioTimeStamp*   inNow,
                        const AudioBufferList*  inInputData,
                        const AudioTimeStamp*   inInputTime,
                        AudioBufferList*        outOutputData,
                        const AudioTimeStamp*   inOutputTime,
                        void*                   inClientData);

AudioUnitと違って、オーディオデバイスにインプットとアウトプット両方ある場合、両方のデータが一回で渡ってきます。ただ当然レイテンシーなんてモノがありますから、インプットのデータは少し前に取り込まれたデータですし、アウトプットに渡すデータは少し後に再生されますので、それらの時間がAudioTimeStampで渡ってきます。DAWみたいに再生しながら録音する場合などは、これらの時間のギャップを計算して、録音されたデータを再生したものに合わせる事が必要になってくると思います。

IOProc関数を解除するのはAudioDeviceDestroyIOProcID関数です。こちらもLeopardから変更になって、以前のAudioDeviceRemoveIOProcはDeprecatedになっています。

extern OSStatus
AudioDeviceDestroyIOProcID( AudioDeviceID           inDevice,
                            AudioDeviceIOProcID     inIOProcID)

AudioDeviceCreateIOProcIDで関数が登録できたら、動作を開始させます。それが、AudioDeviceStart関数になります。動作を停止させるにはAudioDeviceStopを使います。

extern OSStatus
AudioDeviceStart(   AudioDeviceID       inDevice,
                    AudioDeviceIOProcID inProcID)

extern OSStatus
AudioDeviceStop(    AudioDeviceID       inDevice,
                    AudioDeviceIOProcID inProcID)

では、これらを使ってオーディオデバイスを動かしてみたいと思います。Cocoaアプリケーションを新規プロジェクトで作成して、CoreAudio.Frameworkを追加しておき、以下のクラスをInterface Builderでインスタンス化して、実行します。

#import <Cocoa/Cocoa.h>
#import <CoreAudio/CoreAudio.h>

@interface IOProcTest : NSObject {

    AudioDeviceID devID;
    AudioDeviceIOProcID procID;
}

- (void)destroyIOProc;

@end


@implementation IOProcTest

static OSStatus IOProc(AudioDeviceID           inDevice,
                       const AudioTimeStamp*   inNow,
                       const AudioBufferList*  inInputData,
                       const AudioTimeStamp*   inInputTime,
                       AudioBufferList*        outOutputData,
                       const AudioTimeStamp*   inOutputTime,
                       void*                   inClientData)
{
    printf("Output Time = %f\n", inOutputTime->mSampleTime);

    //念のためアウトをゼロでクリアする
	
    NSUInteger i;
    UInt32 buffers = outOutputData->mNumberBuffers;
	
    for (i = 0; i < buffers; i++) {
		
        float *ptr = outOutputData->mBuffers[i].mData;
        UInt32 byteSize = outOutputData->mBuffers[i].mDataByteSize;
		
        memset(ptr, 0, byteSize);
    }
	
    return noErr;
}


- (void)awakeFromNib
{
    OSStatus err = noErr;
    UInt32 size;
	
    //デフォルトのアウトプットに設定されているオーディオデバイスを取得する
    size = sizeof(devID);
    err = AudioHardwareGetProperty(
        kAudioHardwarePropertyDefaultOutputDevice, &size, &devID);
    if (err != noErr) {
        NSLog(@"Get DefaultOutputDevice Err");
        goto end;
    }
	
    //オーディオの入出力を行う関数を設定する
    err = AudioDeviceCreateIOProcID(devID, IOProc, NULL, &procID);
    if (err != noErr) {
        NSLog(@"Create IOProc Err");
        goto end;
    }

    //オーディオデバイスを開始させる
    err = AudioDeviceStart(devID, procID);
    if (err != noErr) {
        NSLog(@"Start Err");
        goto end;
    }
	
end:
	
    return;
}

- (void) dealloc
{
    [self destroyIOProc];
    [super dealloc];
}

- (void)destroyIOProc
{
    OSStatus err;

    //オーディオデバイスを停止する
    err = AudioDeviceStop(devID, procID);
    if (err != noErr) {
        NSLog(@"Stop Err");
        goto end;
    }

    //入出力を行う関数を取り除く
    err = AudioDeviceDestroyIOProcID(devID, procID);
    if (err != noErr) {
        NSLog(@"Destroy IOProc Err");
        goto end;
    }
	
end:
	
    return;
}

@end

実行してみるとログにダダダーッと文字が表示されてIOProcが(デフォルトだと512フレーム単位で)呼ばれている事が分かるとおもいます。

ちなみにCore AudioのIOProcは大丈夫だと思うのですが、AudioUnitのOutputUnitだと、アウトのデータを何も書き込まないままnoErrを返したら大音量でノイズが再生されてしまったというトラウマがあるので、いちおうアウトをゼロでクリアしています。何か音を出すにはここでデータを渡してください。それと、PropertyListenerと同じくIOProcの関数はCore Audio用の別スレッドで呼ばれますので、マルチスレッディングなコードを心がけてください。

といった感じで、7回ほど続けてきたCoreAudio.Frameworkはこんなところでしょうか。まあでも、普通にオーディオデバイス使うならAudioUnitのOutputUnitでやった方がいろいろと面倒見てくれて良いと思いますが、こうやって裸にして調べてみるとAudioUnitの中で何やってるのかもだいたい分かりますし、サンプリングレートの変換とか必要なければこっちでやった方がパフォーマンスを稼げると思いますので。

次はAudioToolboxに移ってオーディオファイル関係あたりを見ていこうと思います。CoreAudio.Frameworkに関しては気が向けばまた何か追加するかもしれません。

Core Audio その6 プロパティリスナー

オーディオデバイスの設定が外部から変更された時や、前回のようにオーディオデバイスの設定を変更し、その変更がちゃんと適用された後に処理を行いたい場合、プロパティが変更されたという通知を受け取るように設定します。

変更された事を知りたいプロパティを、〜AddPropertyListenerと名前のついた関数で登録します。必要がなくなったら〜RemovePropertyListerで登録を解除します。以下が<CoreAudio/AudioHardware.h>で宣言されている関数です。

//AudioHardware用

extern OSStatus
AudioHardwareAddPropertyListener(AudioHardwarePropertyID inPropertyID,
                                 AudioHardwarePropertyListenerProc inProc,
                                 void* inClientData)

extern OSStatus
AudioHardwareRemovePropertyListener(AudioHardwarePropertyID inPropertyID,
                                    AudioHardwarePropertyListenerProc inProc) 


//AudioDevice用

extern OSStatus
AudioDeviceAddPropertyListener(AudioDeviceID                   inDevice,
                               UInt32                          inChannel,
                               Boolean                         isInput,
                               AudioDevicePropertyID           inPropertyID,
                               AudioDevicePropertyListenerProc inProc,
                               void*                           inClientData)

extern OSStatus
AudioDeviceRemovePropertyListener(AudioDeviceID                   inDevice,
                                  UInt32                          inChannel,
                                  Boolean                         isInput,
                                  AudioDevicePropertyID           inPropertyID,
                                  AudioDevicePropertyListenerProc inProc)


//AudioStream用

extern OSStatus
AudioStreamAddPropertyListener(AudioStreamID                   inStream,
                               UInt32                          inChannel,
                               AudioDevicePropertyID           inPropertyID,
                               AudioStreamPropertyListenerProc inProc,
                               void*                           inClientData)

extern OSStatus
AudioStreamRemovePropertyListener(AudioStreamID                   inStream,
                                  UInt32                          inChannel,
                                  AudioDevicePropertyID           inPropertyID,
                                  AudioStreamPropertyListenerProc inProc)

だいたいの引数は〜GetPropertyや〜SetPropertyと一緒ですが、inProcというところにプロパティの変更を受け取る関数名を指定します。その関数は引数の構成がすでに決められていて、以下のように宣言されています。

typedef OSStatus
(*AudioHardwarePropertyListenerProc)( AudioHardwarePropertyID inPropertyID,
                                      void*                   inClientData);

typedef OSStatus
(*AudioDevicePropertyListenerProc)( AudioDeviceID         inDevice,
                                    UInt32                inChannel,
                                    Boolean               isInput,
                                    AudioDevicePropertyID inPropertyID,
                                    void*                 inClientData);

typedef OSStatus
(*AudioStreamPropertyListenerProc)( AudioStreamID         inStream,
                                    UInt32                inChannel,
                                    AudioDevicePropertyID inPropertyID,
                                    void*                 inClientData);

一つの関数に対して一つのプロパティを登録した場合は気にしなくても良いですが、いくつかのプロパティを同じ関数に登録したときなどは、これらの引数で渡ってくる情報をもとに条件分岐して処理を行うという感じになると思います。また、この関数には変更されたプロパティの種類が来るだけなので、変更された値自体は別途〜GetPropertyで取得しなければいけません。

では例として、オーディオデバイスのサンプリング周波数の変更を受け取ってみたいと思います。今回はアプリケーションを起動した状態にしておかないといけないので、Cocoaアプリケーションを新規プロジェクトで作成して、CoreAudio.Frameworkをインポートし、以下のクラスをnibファイル(最近ではxib?)でインスタンス化しておきます。

#import <Cocoa/Cocoa.h>
#import <CoreAudio/CoreAudio.h>

@interface SampleRateListner : NSObject {

}

@end


@implementation SampleRateListner

static OSStatus PropertyListener(AudioDeviceID inDevice,
                                 UInt32 inChannel,
                                 Boolean isInput,
                                 AudioDevicePropertyID inPropertyID,
                                 void *inClientData)
{
    OSStatus err = noErr;
    UInt32 size;
    
    //サンプリング周波数を取得してログに表示
    Float64 sampleRate;
    size = sizeof(sampleRate);
    err = AudioDeviceGetProperty(
        inDevice, 0, false, kAudioDevicePropertyNominalSampleRate, 
        &size, &sampleRate);
    if (err == noErr) {
        printf("SampleRate = %f\n", sampleRate);
    }

    return noErr;
}

- (void)awakeFromNib
{
    OSStatus err = noErr;
	
    //デフォルトのアウトプットに設定されているオーディオデバイスを取得する
    AudioDeviceID devID;
    UInt32 size = sizeof(devID);
    err = AudioHardwareGetProperty(
        kAudioHardwarePropertyDefaultOutputDevice, &size, &devID);
    if (err != noErr) goto catchErr;
	
    //プロパティリスナーを設定する
    err = AudioDeviceAddPropertyListener(
        devID, 0, false, kAudioDevicePropertyNominalSampleRate, 
        PropertyListener, self);
    if (err != noErr) goto catchErr;
	
    return;
	
catchErr:
	
    NSLog(@"catch err");
    exit(0);
}

@end

実行して起動したら、Audio MIDI 設定でデフォルトの出力に割り当てられているデバイスのサンプリング周波数を変更すると、ログにその周波数が表示されると思います。

このサンプルでは周波数を表示するだけですが、実際はプロパティの変更を受けてあれこれと処理をしなくてはいけないと思うので、独自のデータなどをinClientDataで渡して、それをもとに何か処理を行うという感じでしょうか。

ちなみに、今回のプロパティリスナーのようなCore Audioでのコールバック系の関数は基本的にメインではないスレッドで呼ばれるので、インスタンス変数へのアクセスなどするならマルチスレッドを意識してコードを記述していかなくてはいけないと思います。

といったところで、次回はオーディオデバイスから音を出したり、取り込んだりするところを見ていきたいと思います。

再生スライダー

iPodとかQuickTimePlayerのような、再生する位置を表示・変更できるようなスライダーを使いたいと思って、NSSliderにジャカジャカ位置を送っていたら、うまいことクリックして変更できなくて、あれこれ調べたらNSSliderからNSSliderCellを取得してmouseDownFlagsってのをチェックすれば良い事が分かりました。

そこらへんを検証してみたコードが以下のような感じです。タイマーはNSEventTrackingRunLoopModeでスライダーを変更中も関係なく動かしていますが、マウスで操作中のときは値を送らないようにしています。

#import <Cocoa/Cocoa.h>

@interface Controller : NSObject {
	
    IBOutlet NSSlider *slider;
    float sliderValue;
}

- (IBAction)setValue:(id)sender;

@end

@implementation Controller

- (void)awakeFromNib
{
    [slider setMinValue:0];
    [slider setMaxValue:100];
    [slider setContinuous:NO];
	
    NSTimer *timer = 
        [NSTimer scheduledTimerWithTimeInterval:0.05
                                         target:self 
                                       selector:@selector(sendSliderValue) 
                                       userInfo:nil 
                                        repeats:YES];
	
    [[NSRunLoop currentRunLoop] addTimer:timer
        forMode:NSEventTrackingRunLoopMode];
}

- (void)sendSliderValue
{
    NSSliderCell *cell = [slider cell];

    if (![cell mouseDownFlags]) {
		
        sliderValue++;
		
        if (sliderValue > 100) {
            sliderValue = 0;
        }
		
        [slider setFloatValue:sliderValue];
    }
}

- (IBAction)setValue:(id)sender
{
    sliderValue = [sender floatValue];
}

@end

Core Audio その5 プロパティの設定

※2009/10/2追記《このエントリーはMac OS X 10.5での場合について書かれています。10.6以降の場合はこちらをご覧ください。》

今回はプロパティに値を設定してみたいと思います。

取得のときの〜GetPropertyの関数と同じような感じで、設定は〜SetPropertyと名前のついた関数を使います。<CoreAudio/AudioHardware.h>に以下のような関数があります。

extern OSStatus
AudioHardwareSetProperty(   AudioHardwarePropertyID inPropertyID,
                            UInt32                  inPropertyDataSize,
                            const void*             inPropertyData)

extern OSStatus
AudioDeviceSetProperty( AudioDeviceID           inDevice,
                        const AudioTimeStamp*   inWhen,
                        UInt32                  inChannel,
                        Boolean                 isInput,
                        AudioDevicePropertyID   inPropertyID,
                        UInt32                  inPropertyDataSize,
                        const void*             inPropertyData)

extern OSStatus
AudioStreamSetProperty( AudioStreamID           inStream,
                        const AudioTimeStamp*   inWhen,
                        UInt32                  inChannel,
                        AudioDevicePropertyID   inPropertyID,
                        UInt32                  inPropertyDataSize,
                        const void*             inPropertyData)

取得のときと違い、AudioDeviceとAudioStreamにAudioTimeStampを渡すinWhenという引数があります。これは設定するタイミングを指定できるものですが、NULLを入れておけばすぐに変更されます。また、プロパティのサイズはポインタではなく値渡しになります。

それではAudioDeviceSetPropertyを使ってサンプリング周波数をアプリケーション側から変更してみたいと思います。今回もFoundation Toolでプロジェクトを作って、main関数に処理を書いてみます。オーディオデバイスのサンプリング周波数のプロパティはkAudioDevicePropertyNominalSampleRateです。

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>


int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    OSStatus err = noErr;
    UInt32 size;
	
    //デフォルトのアウトプットに設定されているオーディオデバイスを取得する
    AudioDeviceID devID;
    size = sizeof(devID);
    err = AudioHardwareGetProperty(
        kAudioHardwarePropertyDefaultOutputDevice, &size, &devID);
    if (err != noErr) {
        NSLog(@"Get DefaultOutputDevice Err");
        goto catchErr;
    }
	
    //デバイスからサンプリング周波数を取得する
    Float64 sampleRate;
    size = sizeof(sampleRate);
    err = AudioDeviceGetProperty(
        devID, 0, false, kAudioDevicePropertyNominalSampleRate, 
        &size, &sampleRate);
    if (err != noErr) {
        NSLog(@"Get NominalSampleRate err");
        goto catchErr;
    }
	
    //プロパティが書き換え可能か調べる
    Boolean isWritable;
    err = AudioDeviceGetPropertyInfo(
        devID, 0, false, kAudioDevicePropertyNominalSampleRate, 
        NULL, &isWritable);
    if (err != noErr) {
        NSLog(@"Get isWritable err");
        goto catchErr;
    }
	
    if (isWritable == false) {
        NSLog(@"isWritable false");
        goto catchErr;
    }
	
    //サンプリング周波数を変える
    if (sampleRate == 44100) {
        sampleRate = 48000;
    } else {
        sampleRate = 44100;
    }
	
    //デバイスにサンプリング周波数を設定する
    size = sizeof(sampleRate);
    err = AudioDeviceSetProperty(
        devID, NULL, 0, false, kAudioDevicePropertyNominalSampleRate, 
        size, &sampleRate);
    if (err != noErr) {
        NSLog(@"Set NominalSampleRate err");
        goto catchErr;
    }

catchErr:
	
    [pool drain];
    return 0;
}
<foundation><coreaudio></coreaudio></foundation>

これを実行すると、オーディオデバイスのサンプリング周波数を44.1kHzであれば48kHzに切り替え、その他であれば44.1kHzに切り替えます。Audio MIDI 設定を立ち上げた状態で実行すると切り替わっているのが分かると思います。(本来ならAudio MIDI 設定のようにサポートされているサンプリング周波数を取得して、その中から選ぶのが正しいやり方だと思います。とりあえずサンプルですので。)

いちおうこれで設定はできるのですが、inWhenにNULLを設定してすぐに設定が行われるようにしても、デバイス本体はプログラムとは関係なく動作しますので、設定直後にサンプリング周波数が変更されたと想定して処理を進めてしまうと、実は設定が変更されていなくて問題が出てくる場合があります。今回のサンプルでも、main関数内のAudioDeviceSetPropertyの後に再びGetPropertyでサンプリング周波数を取得しても、設定直後だと変更前の値が返ってくるときがあります。

このようにデバイスに値が設定された事を確認してから続きの処理を開始したい場合や、Audio MIDI 設定などで外部から設定が変更されたときに何か処理を行いたい場合の方法は、次回にやってみたいと思います。

Core Audio その4 プロパティの取得

※2009/10/2追記《このエントリーはMac OS X 10.5での場合について書かれています。10.6以降の場合はこちらをご覧ください。》

今回は、Core Audioのデバイスなどから情報(※プロパティ)を実際にやりとりする方法を見ていきたいと思います。(※使用する関数名や引数でpropertyと名前が付けられているので、この情報の事を以後プロパティと表記する事にします。)

まず、プロパティを取得するにはGetPropertyと名前のついた関数を使います。それぞれ以下の関数があります。

//オーディオシステム全体からの情報を取得する
extern OSStatus
AudioHardwareGetProperty(   AudioHardwarePropertyID inPropertyID,
                            UInt32*                 ioPropertyDataSize,
                            void*                   outPropertyData)

//オーディオデバイスの情報を取得する
extern OSStatus
AudioDeviceGetProperty( AudioDeviceID           inDevice,
                        UInt32                  inChannel,
                        Boolean                 isInput,
                        AudioDevicePropertyID   inPropertyID,
                        UInt32*                 ioPropertyDataSize,
                        void*                   outPropertyData)

//オーディオデバイス内のストリーム(音声の入出力部分)の情報を取得する
extern OSStatus
AudioStreamGetProperty( AudioStreamID           inStream,
                        UInt32                  inChannel,
                        AudioDevicePropertyID   inPropertyID,
                        UInt32*                 ioPropertyDataSize,
                        void*                   outPropertyData)

それぞれの関数の、〜PropertyIDというところにはプロパティIDという、取得したいプロパティの種類を表す定数を指定します(<CoreAudio/AudioHardware.h>で定義されています。)。ioPropertyDataSizeには取得するプロパティのサイズの変数へのポインタを、outPropertyDataには取得するプロパティへのポインタを渡して、プロパティを取得します。

AudioDeviceGetPropertyのisInputは音声の入力側の情報を取得するのであればtrueを、出力側であればfalseを渡します。inChannelは、kAudioDevicePropertyChannel〜あたりを使うのでなければ、0を入れておけば良いようです。

ではためしに、「Audio MIDI 設定」でデフォルトに設定されている出力デバイスを取得して、そのデバイスの名前を取得してみます。

New ProjectでFoundation Toolを選択して新規プロジェクトを作成し、CoreAudio.Frameworkを追加して、mainメソッドを以下のように書き換えてみます。

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    OSStatus err = noErr; //エラーコードを受け取る変数
    UInt32 size; //プロパティのサイズを指定する変数

    //デフォルトアウトプットデバイスを取得する

    AudioDeviceID devID;
    size = sizeof(devID);
    err = AudioHardwareGetProperty(
        kAudioHardwarePropertyDefaultOutputDevice, &size, &devID);
    if (err != noErr) {
        NSLog(@"AudioHardwareGetProperty err");
        goto catchErr;
    }
	
    NSLog(@"deviceID = %u", devID);

    //オーディオデバイスの名前を取得する

    CFStringRef strRef;
    size = sizeof(strRef);
    err = AudioDeviceGetProperty(
        devID, 0, false, kAudioDevicePropertyDeviceNameCFString, &size, &strRef);
    if (err != noErr) {
        NSLog(@"AudioDeviceGetProperty err");
        goto catchErr;
    }

    NSLog(@"deviceName = %@", (NSString *)strRef);

catchErr:
	
    [pool drain];
    return 0;
}

Core Audioの関数はOSStatusという型のエラーコードを返してきますので、noErrでなければエラー処理をします。とりあえず今回は関数の最後にgotoで飛ばしています。

MacBook Proで実行してみると以下のような感じでログに表示されて、情報を取得できている事が分かります。

[Session started at 2008-04-09 23:54:39 +0900.]
2008-04-09 23:54:39.625 PropertyTest01[5027:10b] deviceID = 264
2008-04-09 23:54:39.631 PropertyTest01[5027:10b] deviceName = Built-in Output

The Debugger has exited with status 0.

プロパティはものによってスカラ値であったり構造体であったり配列であったりしますので、サイズをioPropertyDataSizeに渡す必要があります。今回のように前もってサイズが分かるものは良いのですが、すべてのデバイスを取得する場合などは、接続されているデバイスの数によってサイズが変動しますので、事前にそのサイズを取得しておかなければなりません。

取得しようとしているプロパティのサイズと、プロパティが書き込み可能か、という情報を取得するのが以下のGetPropertyInfoの関数です。

extern OSStatus
AudioHardwareGetPropertyInfo(   AudioHardwarePropertyID inPropertyID,
                                UInt32*                 outSize,
                                Boolean*                outWritable)

extern OSStatus
AudioDeviceGetPropertyInfo( AudioDeviceID           inDevice,
                            UInt32                  inChannel,
                            Boolean                 isInput,
                            AudioDevicePropertyID   inPropertyID,
                            UInt32*                 outSize,
                            Boolean*                outWritable)

extern OSStatus
AudioStreamGetPropertyInfo( AudioStreamID           inStream,
                            UInt32                  inChannel,
                            AudioDevicePropertyID   inPropertyID,
                            UInt32*                 outSize,
                            Boolean*                outWritable)

これらを使って情報を取得してみます。マックにつながっているオーディオデバイスを全て取得して、アウトプットがあればフォーマットを取得してみる、というコードが以下のような感じになります。

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>


int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    OSStatus err = noErr;
    UInt32 size;
    NSInteger i, j;

    AudioStreamID *streamIDs = NULL;
    AudioDeviceID *devIDs = NULL;

    //全てのデバイスを取得する

    err = AudioHardwareGetPropertyInfo(
        kAudioHardwarePropertyDevices, &size, NULL);
    if (err != noErr) {
        NSLog(@"AudioHardwareGetPropertyInfo err");
        goto catchErr;
    }

    devIDs = malloc(size);
    err = AudioHardwareGetProperty(
        kAudioHardwarePropertyDevices, &size, devIDs);
    if (err != noErr) {
        NSLog(@"AudioHardwareGetProperty err");
        goto catchErr;
    }


    //デバイスの数を計算して繰り返す

    UInt32 count = size / sizeof(AudioDeviceID);

    for (i = 0; i < count ; i++) {

        //デバイスの名前を取得する(isInputがfalseだがアウトが無くても取得できる)
        CFStringRef strRef;
        size = sizeof(strRef);
        err = AudioDeviceGetProperty(
            devIDs[i], 0, false, kAudioDevicePropertyDeviceNameCFString,
            &size, &strRef);
        if (err != noErr) {
            NSLog(@"AudioDeviceGetProperty deviceNameCFString err");
            goto catchErr;
        }

        //デバイス内のストリームIDを取得する(ここではアウト側のみが取得できる)
        err = AudioDeviceGetPropertyInfo(
            devIDs[i], 0, false, kAudioDevicePropertyStreams, &size, NULL);
        if (err != noErr) {
            NSLog(@"AudioDeviceGetPropertyInfo err");
            goto catchErr;
        }

        if (size) NSLog(@"%@ / deviceID = %u", (NSString *)strRef, devIDs[i]);

        streamIDs = malloc(size);
        UInt32 nStream = size / sizeof(AudioStreamID);
        err = AudioDeviceGetProperty(
            devIDs[i], 0, false, kAudioDevicePropertyStreams, &size, streamIDs);
        if (err != noErr) {
            NSLog(@"AudioDeviceGetProperty streams err");
            goto catchErr;
        }

        //ストリームIDの数だけ繰り返す

        for (j = 0; j < nStream; j++) {

            //フォーマットを取得する
            AudioStreamBasicDescription format;
            size = sizeof(format);
            err = AudioStreamGetProperty(
                streamIDs[j], 0, kAudioStreamPropertyVirtualFormat, &size, &format);
            if (err != noErr) {
                NSLog(@"AudioStreamGetProperty virtualFormat err");
                goto catchErr;
            }

            NSLog(@"streamID = %u / channels = %u",
                streamIDs[j], format.mChannelsPerFrame);
        }

        free(streamIDs);
        streamIDs = NULL;
    }

catchErr:

    if (devIDs) free(devIDs);
    if (streamIDs) free(streamIDs);
	
    [pool drain];
    return 0;
}<foundation><coreaudio> </coreaudio></foundation>

FIREBOXというインターフェースをつなげて、Bluetoothも生かしたMacBook Proで実行してみると、以下のようにログに表示されます。

[Session started at 2008-04-13 21:53:31 +0900.]
2008-04-13 21:53:31.970 PropertyTest[779:10b] 
    PreSonus FIREBOX (2343) / deviceID = 258
2008-04-13 21:53:31.974 PropertyTest[779:10b] streamID = 265 / channels = 1
2008-04-13 21:53:31.974 PropertyTest[779:10b] streamID = 266 / channels = 1
2008-04-13 21:53:31.975 PropertyTest[779:10b] streamID = 267 / channels = 1
2008-04-13 21:53:31.975 PropertyTest[779:10b] streamID = 268 / channels = 1
2008-04-13 21:53:31.976 PropertyTest[779:10b] streamID = 269 / channels = 1
2008-04-13 21:53:31.976 PropertyTest[779:10b] streamID = 270 / channels = 1
2008-04-13 21:53:31.977 PropertyTest[779:10b] streamID = 271 / channels = 1
2008-04-13 21:53:31.977 PropertyTest[779:10b] streamID = 272 / channels = 1
2008-04-13 21:53:31.982 PropertyTest[779:10b] Built-in Output / deviceID = 279
2008-04-13 21:53:31.982 PropertyTest[779:10b] streamID = 280 / channels = 2
2008-04-13 21:53:31.983 PropertyTest[779:10b] receiver01 / deviceID = 273
2008-04-13 21:53:31.983 PropertyTest[779:10b] streamID = 274 / channels = 2

The Debugger has exited with status 0.

こうしてみると、DeviceIDとStreamIDがかぶっていないという事も分かります。

こんな感じで、プロパティを取得するときは、AudioHardwareGetPropertyInfo → AudioHardwareGetProperty → AudioDeviceGetPropertyInfo → AudioDeviceGetProperty → AudioStreamGetPropertyInfo → AudioStreamGetPropertyという順番で必要な関数を選択して取得していく事になります。

Core Audio その3 AudioObjectID

※2009/10/2追記《このエントリーはMac OS X 10.5での場合について書かれています。10.6以降の場合はこちらをご覧ください。》

前回の最後に、情報のやり取りをすると書いていましたが、その前にもう一つ説明しておきたいものがあります。

マックにつなげているオーディオインターフェースのチャンネル数だとか、サンプリング周波数だとかを知りたいとしても、そのオーディオインターフェースを特定できなければ知りようもありません。

Core Audioでは、マックのオーディオシステム全体や、オーディオデバイスや、オーディオデバイス内の入出力に、それぞれIDナンバーが振られており、AudioObjectIDという型の変数で表されています。<CoreAudio/AudioHardware.h>にAudioObject〜という名前で始まる関数が用意されていますので、それらにAudioObjectIDを渡してオーディオデバイスなどの情報を取得したり設定したり動かしたりできます。

ですが、内部的にそうなっているというだけで、実際にAudioObjectというものを使う事はないと思います。

その代わりに、AudioDeviceIDとAudioStreamIDとAudioSystemObjectというものが別に用意されています。AudioDeviceIDはオーディオデバイス(オーディオインターフェースそのもの)を表し、AudioStreamIDはオーディオデバイス内の入出力を表します。これらのIDはオーディオインターフェースが接続された時点で自動的に割り振られます。ちなみにヘッダをたどってみると、これらはAudioObjectIDをtypedefしただけのものです。

typedef UInt32      AudioObjectID;

typedef AudioObjectID   AudioDeviceID;

typedef AudioObjectID   AudioStreamID;

AudioSystemObjectは、マックのオーディオシステム全体を表します。実際にコード上にAudioSystemObjectなどと書く事は無く、<CoreAudio/AudioHardware.h>にあるAudioHardware〜という名前で始まる関数を使えば、AudioSystemObjectと情報をやり取りするという事になります。

AudioDeviceIDとAudioStreamIDは、それぞれAudioDevice〜やAudioStream〜という名前で始まる関数がありますので、それらを使用して情報をやり取りします。

といったところで、次回はそれらの関数を使ってみたいと思います。

Core Audio その2 AudioStreamBasicDescription

Core Audioにおいて、オーディオデータの状態(リニアPCM等のフォーマットとか、ビットやサンプリングレートとか、チャンネル数とか)を表す構造体がAudioStreamBasicDescriptionです。

struct AudioStreamBasicDescription
{
    Float64 mSampleRate;       //サンプリング周波数(1秒間のフレーム数)
    UInt32  mFormatID;         //フォーマットID(リニアPCM、MP3、AACなど)
    UInt32  mFormatFlags;      //フォーマットフラグ(エンディアン、整数or浮動小数点数)
    UInt32  mBytesPerPacket;   //1パケット(データを読み書きする単位)のバイト数
    UInt32  mFramesPerPacket;  //1パケットのフレーム数
    UInt32  mBytesPerFrame;    //1フレームのバイト数
    UInt32  mChannelsPerFrame; //1フレームのチャンネル数
    UInt32  mBitsPerChannel;   //1チャンネルのビット数
    UInt32  mReserved;         //意味なし。アラインメントを揃えるためのもの?
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

フォーマットIDは以下のような定数が定義されています。

enum
{
    kAudioFormatLinearPCM               = 'lpcm',
    kAudioFormatAC3                     = 'ac-3',
    kAudioFormat60958AC3                = 'cac3',
    kAudioFormatAppleIMA4               = 'ima4',
    kAudioFormatMPEG4AAC                = 'aac ',
    kAudioFormatMPEG4CELP               = 'celp',
    kAudioFormatMPEG4HVXC               = 'hvxc',
    kAudioFormatMPEG4TwinVQ             = 'twvq',
    kAudioFormatMACE3                   = 'MAC3',
    kAudioFormatMACE6                   = 'MAC6',
    kAudioFormatULaw                    = 'ulaw',
    kAudioFormatALaw                    = 'alaw',
    kAudioFormatQDesign                 = 'QDMC',
    kAudioFormatQDesign2                = 'QDM2',
    kAudioFormatQUALCOMM                = 'Qclp',
    kAudioFormatMPEGLayer1              = '.mp1',
    kAudioFormatMPEGLayer2              = '.mp2',
    kAudioFormatMPEGLayer3              = '.mp3',
    kAudioFormatTimeCode                = 'time',
    kAudioFormatMIDIStream              = 'midi',
    kAudioFormatParameterValueStream    = 'apvs',
    kAudioFormatAppleLossless           = 'alac',
    kAudioFormatMPEG4AAC_HE		= 'aach',
    kAudioFormatMPEG4AAC_LD		= 'aacl',
    kAudioFormatMPEG4AAC_HE_V2		= 'aacp',
    kAudioFormatMPEG4AAC_Spatial	= 'aacs',
    kAudioFormatAMR			= 'samr'
};

普通にオーディオデータを扱うときは、WAVやAIFFでおなじみの非圧縮フォーマットであるリニアPCM(kAudioFormatLinearPCM)になります。その他は、それぞれの圧縮フォーマットのファイルの読み書きをするときに使用します。オーディオファイルのフォーマット以外にもタイムコードやMIDIやParameterValueStreamなんてのがあるのがちょっと面白そうなところです。

フォーマットフラグは以下の定数が定義されています。

enum
{
    kAudioFormatFlagIsFloat                     = (1L << 0),
    kAudioFormatFlagIsBigEndian                 = (1L << 1),
    kAudioFormatFlagIsSignedInteger             = (1L << 2),
    kAudioFormatFlagIsPacked                    = (1L << 3),
    kAudioFormatFlagIsAlignedHigh               = (1L << 4),
    kAudioFormatFlagIsNonInterleaved            = (1L << 5),
    kAudioFormatFlagIsNonMixable                = (1L << 6),
    kAudioFormatFlagsAreAllClear                = (1L << 31),
    
    kLinearPCMFormatFlagIsFloat                 = kAudioFormatFlagIsFloat,
    kLinearPCMFormatFlagIsBigEndian             = kAudioFormatFlagIsBigEndian,
    kLinearPCMFormatFlagIsSignedInteger         = kAudioFormatFlagIsSignedInteger,
    kLinearPCMFormatFlagIsPacked                = kAudioFormatFlagIsPacked,
    kLinearPCMFormatFlagIsAlignedHigh           = kAudioFormatFlagIsAlignedHigh,
    kLinearPCMFormatFlagIsNonInterleaved        = kAudioFormatFlagIsNonInterleaved,
    kLinearPCMFormatFlagIsNonMixable            = kAudioFormatFlagIsNonMixable,
    kLinearPCMFormatFlagsAreAllClear            = kAudioFormatFlagsAreAllClear,
    
    kAppleLosslessFormatFlag_16BitSourceData    = 1,
    kAppleLosslessFormatFlag_20BitSourceData    = 2,
    kAppleLosslessFormatFlag_24BitSourceData    = 3,
    kAppleLosslessFormatFlag_32BitSourceData    = 4
};

enum
{
#if TARGET_RT_BIG_ENDIAN
    kAudioFormatFlagsNativeEndian       = kAudioFormatFlagIsBigEndian,
#else
    kAudioFormatFlagsNativeEndian       = 0,
#endif
    kAudioFormatFlagsCanonical =
        kAudioFormatFlagIsFloat |
        kAudioFormatFlagsNativeEndian |
        kAudioFormatFlagIsPacked,
    kAudioFormatFlagsNativeFloatPacked =
        kAudioFormatFlagIsFloat |
        kAudioFormatFlagsNativeEndian |
        kAudioFormatFlagIsPacked
};

フラグですから、設定したい定数をビット演算で組み合わせて指定します。例としてAIFFの16bit整数のオーディオファイルを作成する場合には、ビッグエンディアンで符号付き整数でPackedになりますから、

AudioStreamBasicDescription desc;
desc.mFormatFlags =
    kAudioFormatFlagIsBigEndian |
    kLinearPCMFormatFlagIsSignedInteger |
    kAudioFormatFlagIsPacked;

といった感じになります。

Core Audioではデフォルトだとオーディオデータは32bitのFloatのPackedのネイティブなエンディアンで扱われますから、それらが既に組み合わせられた、kAudioFormatFlagsNativeFloatPackedなんていう便利な定数も用意されています。

ちなみにPackedとは何かというと、オーディオデータの1サンプルに割り当てられたデータ領域の全てのビットを使った状態です。例えば、オーディオデータの1サンプルに32bitのメモリ領域が割り当てられているときに、ぴったり32bitのデータが入っている状態がPackedという事になります。32bitのメモリ領域に20bitのデータが入っているような場合はPackedはセットせず、その20bitのデータが上位ビットに寄せられていればAlignedHigh、下位ビットに寄せられていればAlignedLow(AlignedHighをセットしない)になります。

Core Audioでオーディオデータを扱うときには基本的にPackedなので、Packedでない状態が実際に使われているところがないかと探したら、自分の使っているMacBook Proのオーディオデバイスがそうでした。デバイスのビットを20bitや24bitに設定したときにはpackedではなく、32bitの領域が割り当てられていてAlignedLowになっています。とはいってもデバイス側のフォーマットなので、CoreAudio経由で使うときは基本的に32bitFloatに変換された状態で渡ってきますから、実際に意識する事はないと思います。

mBytesPerPacketからmBitsPerChannelまでの5つのメンバは、オーディオのデータがどんな状態で並んでいるかが表されます。

リニアPCMでのそれぞれの関係性を見ていくと、mBitsPerChannelで指定されたビット数の1サンプルをチャンネル数分まとめたものがフレームで、そのフレームをまとめて一回分の読み書きの単位としているのがパケットです。

mChannelsPerFrameで1フレーム内のチャンネル数、mBytesPerFrameで1フレームの容量、mFramesPerPacketで1パケット内のフレーム数、mBytesPerPacketで1パケットの容量が表される事になります。

リニアPCMならフレーム単位で一つのデータが成立するのでmFramesPerPacketが1となり、他の全てのメンバにもフォーマットに応じた値が設定されますが、圧縮フォーマットであれば、いくらかのフレームがまとめられて1パケットに圧縮されているので、mFramesPerPacketとmChannelsPerFrameのみが設定され、他の値は0という場合もあります。

リニアPCMでPackedの場合、5つのうち3つ決まれば残り2つは自然と値が計算で求められます。たとえば32bitのステレオのInterleavedだと、

desc.mBitsPerChannel = 32;
desc.mFramesPerPacket = 1;
desc.mChannelsPerFrame = 2;
desc.mBytesPerFrame = desc.mBitsPerChannel / 8 * desc.mChannelsPerFrame;
desc.mBytesPerPacket = desc.mBytesPerFrame * desc.mFramesPerPacket;

という感じです。

例として、オーディオファイルでよく使われそうなフォーマットの設定値を載せておきます。

// AIFF 16bit 44.1kHz STEREOの場合

AudioStreamBasicDescription aiffFormat;
aiffFormat.mSampleRate = 44100.0;
aiffFormat.mFormatID = kAudioFormatLinearPCM;
aiffFormat.mFormatFlags = 
    kAudioFormatFlagIsBigEndian |
    kAudioFormatFlagIsSignedInteger |
    kAudioFormatFlagIsPacked;
aiffFormat.mBitsPerChannel = 16;
aiffFormat.mChannelsPerFrame = 2;
aiffFormat.mFramesPerPacket = 1;
aiffFormat.mBytesPerFrame = 4;
aiffFormat.mBytesPerPacket = 4;
aiffFormat.mReserved = 0;


// WAVE 8bit 48kHz MONOの場合

AudioStreamBasicDescription wavFormat;
wavFormat.mSampleRate = 48000.0;
wavFormat.mFormatID = kAudioFormatLinearPCM;
wavFormat.mFormatFlags = kAudioFormatFlagIsPacked; //WAVの8bitはunsigned
wavFormat.mBitsPerChannel = 8;
wavFormat.mChannelsPerFrame = 1;
wavFormat.mFramesPerPacket = 1;
wavFormat.mBytesPerFrame = 1;
wavFormat.mBytesPerPacket = 1;
wavFormat.mReserved = 0;


// AAC 44.1kHz STEREOの場合

AudioStreamBasicDescription m4aFormat;
m4aFormat.mSampleRate = 44100.0;
m4aFormat.mFormatID = kAudioFormatMPEG4AAC;
m4aFormat.mFormatFlags = kAudioFormatFlagIsBigEndian;
m4aFormat.mBytesPerPacket = 0;
m4aFormat.mFramesPerPacket = 1024;
m4aFormat.mBytesPerFrame = 0;
m4aFormat.mChannelsPerFrame = 2;
m4aFormat.mBitsPerChannel = 0;
m4aFormat.mReserved = 0;

前回から見てきたAudioBufferListやAudioStreamBasicDescriptionが扱えれば、オーディオデバイスやオーディオファイル等と、オーディオデータやフォーマット情報のやり取りが出来るようになります。その方法については次回やってみたいと思います。

Core Audio その1 AudioBufferとAudioBufferList

いままでvDSPやら何やらと、オーディオプログラミングでも補助的なものをネタにしていたので、ちょっとここらへんで基本に立ち返ってCore Audioの基本的な部分を書いていこうと思います。自分的にもちゃんと書いておかないと、オーディオ系のプログラミングからしばらく離れたりしたときに忘れてしまいそうになるので。

Core Audioというと、広い意味ではAudioUnitプラグインやCore MIDIまで含まれていると思いますが、ここではCoreAudio Frameworkという、まさにCore Audioな部分を見ていこうと思います。

どこから始めようかとと考えましたが、なにはなくともオーディオのデータが扱えなくては始まりません。Core Audioにはオーディオデータを表す構造体として、AudioBufferとAudioBufferListというものが定義されていますので、とりあえず、そこを見ていきます。

AudioBufferはひとつのオーディオデータを表していて、そのAudioBufferを配列でまとめて持っているのがAudioBufferListになります。

struct AudioBuffer
{
    UInt32  mNumberChannels;
    UInt32  mDataByteSize;
    void*   mData;
};
struct AudioBufferList
{
    UInt32      mNumberBuffers;
    AudioBuffer mBuffers[kVariableLengthArray];
};
typedef struct AudioBufferList  AudioBufferList;

AudioBufferから見ていくと、mDataはオーディオデータのあるメモリ領域へのポインタです。mDataByteSizeはmDataの領域のサイズで、mNumberChannelsは含まれるチャンネル数になります。

AudioBufferListに移りまして、mBuffers[kVariableLengthArray]がAudioBufferの配列で、mNumberBuffersが配列の要素数になります。

なぜこんな風に2重構造になっているのかというと、オーディオデータが複数チャンネルある場合に、データの状態がインターリーブドになっているかいないかで2通りの扱い方があるからです。オーディオファイルなどではインターリーブドで渡す場合が多いですし、AudioUnitなどではインターリーブドでない形で渡す事になります。

例として、それぞれの場合でAudioBufferListを作成してみます。

長さが256フレームの32bitのステレオのデータをAudioBufferListを作ってみると、Interleavedならこんな感じです。

UInt32 frames = 256;
UInt32 channels = 2;
AudioBufferList list;
list.mNumberBuffers = 1;
list.mBuffers[0].mNumberChannels = channels;
list.mBuffers[0].mDataByteSize = frames * sizeof(float) * channels;
list.mBuffers[0].mData = calloc(1, frames * sizeof(float) * channels);

NonInterleavedだと、ちょっと工夫しなくてはいけません。

AudioBufferListのmBuffersに「kVariableLengthArray」という定数が記述されていますが、コマンド+ダブルクリックでたどってみると、

enum {
  kVariableLengthArray          = 1
};

となっています。AudioBufferListを作成した時点で要素が一つのAudioBufferの配列が確保されているという事ですので、InterleavedならAudioBufferListだけを作っておけば良かったのですが、NonInterleavedで配列の要素が2以上の場合はその分メモリを確保しておかなくてはいけません。mBuffersは構造体の最後のメンバですので、その後ろにそのまま確保します。

アップルのサンプルにあったAudioBufferListの生成と解放をするコードを参考にして、Objective-Cのメソッドにしてみると、

- (AudioBufferList *)allocateAudioBufferList:(UInt32)numChannels size:(UInt32)size
{
    AudioBufferList *list;
    UInt32 i;
	
    list = (AudioBufferList*)calloc(1, sizeof(AudioBufferList)
        + numChannels * sizeof(AudioBuffer));
    if (list == NULL) return NULL;
	
    list->mNumberBuffers = numChannels;
	
    for(i = 0; i < numChannels; ++i) {
        list->mBuffers[i].mNumberChannels = 1;
        list->mBuffers[i].mDataByteSize = size;
        list->mBuffers[i].mData = malloc(size);
        if(list->mBuffers[i].mData == NULL) {
            [self removeAudioBufferList:list];
            return NULL;
        }
    }
	
    return list;
}

- (void)removeAudioBufferList:(AudioBufferList *)list
{
    UInt32 i;
	
    if(list) {
        for(i = 0; i < list->mNumberBuffers; i++) {
            if (list->mBuffers[i].mData) free(list->mBuffers[i].mData);
        }
        free(list);
    }
}

といった感じになります。ちなみに、メモリを確保するところで、

list = (AudioBufferList*)calloc(1, sizeof(AudioBufferList)
    + numChannels * sizeof(AudioBuffer));

となっていて、もともとAudioBufferListでAudioBufferが1チャンネル分確保されているのに、さらにチャンネル数分のAudioBufferを確保しているのが無駄なような気がしますが、余分に確保しておく分には動作に問題はなさそうなのと、MTCoreAudioでも同じなので、とりあえずそのままコピペしてきてます。(ヘッダのコメントを見てみると、以前はkVariableLengthArrayを0にしてたけど、ANSI Cだと駄目だから1にしてるんだ、と書いてあります。)

このメソッドを使ってAudioBufferListを作成してみると、こんな感じになります。

UInt32 frames = 256;
UInt32 channels = 2;
AudioBufferList *list =
    [self allocateAudioBufferList:channels size:frames * sizeof(float)];

と、今回はAudioBufferListを見てきましたが、AudioBufferListの情報だけでは、チャンネル数が分かっても、ビットとかサンプリング周波数とかオーディオデータのフォーマットは分かりません。それに関しては、また次回。

AudioUnitをオフラインで使う

オーディオファイルにエフェクトをバッチ処理でかけるような時に、AudioUnitを使って出来ないかと思ったのですが、そういう事ができそうなkAudioUnitType_OfflineEffectを取得しても、VariSpeedやTimePitchしか出てきません。しかし、Logicとかで出来てる事が出来ないはずはないと思い、普通のkAudioUnitType_Effectをオフラインで使える方法はないかと調べてみました。

リアルタイムでの処理をシミュレートするという形で調べた結果、ポイントのみをとりあえず書いておきます。

・AudioUnitRender関数を実行すると別のスレッドでエフェクト処理をしてしまうので、AudioUnitAddRenderNotifyで、エフェクト処理した後のタイミングでデータを受け取る関数を設定する。
・その関数内ではActionFlagsがkAudioUnitRenderAction_PostRenderの時が処理後のデータとなる。
・AudioUnitRender関数に渡すAudioBufferListを自前で用意する。NotInterleaved。1024フレーム分くらいがベスト(小さいと処理が遅く、大きいと受け付けてくれない)。
・AudioUnitRender関数に渡すAudioUnitRenderActionFlagsは0。
・AudioUnitRender関数に渡すAudioTimeStampはmSampleTimeだけ必要だが、同じ数字を連続で渡してはいけない(一つでもずらしていればOK)。

ここらへんを守って、一回の処理が終わるごとに次のAudioUnitRenderを呼び出す、といったところでしょうか。ちなみにこの方法だと再生しながらバックグラウンドで処理させるみたいな事は出来ないと思います。

なんか、オフラインモードに切り替えてAudioUnitRender呼ぶだけでいいなんていうスマートな方法ってないですかねぇ。

高速フーリエ変換

vDSPで高速フーリエ変換を行う関数の使い方です。

vDSP One-Dimensional Fast Fourier Transforms Referenceというリファレンスを見ると、一次元のフーリエ変換だけで30個ほどの関数が用意されています。

大きく分けてRealとComplexがあり、そのそれぞれがさらに、In-Place(変換前のバッファにそのまま上書き)かOut-of-Place(変換前と後で別々のバッファ)かで分かれています。さらに細かく見るとfloat用とdouble用とか、一度にたくさん処理するものとか、いろいろあります。

今回はIn-PlaceでComplexのfft_zip()を使ってみます。

ところで、このfft_zip()関数などは、リファレンスには関数名にvDSP_がついていますが、コード補完では引数付きの状態では出てこなくて、vDSP_がつかないfft_zipで出てきたりします。

//fft_zip()関数

void vDSP_fft_zip (FFTSetup setup,	//FFTセットアップ
   DSPSplitComplex * ioData,		//複素数配列の構造体
   vDSP_Stride stride,			//ストライド
   vDSP_Length log2n,			//FFTサイズ
   FFTDirection direction);		//正変換か逆変換か

だいたいのvDSPの関数は、その関数のみで完結しますが、FFTを使うにはvDSP Libraryというリファレンスでもわざわざ使い方が解説されているように、ちょっとした準備と後始末が必要です。

まず、Accelerateフレームワークをインポートしておきます。

#import <Accelerate/Accelerate.h>

create_fftsetup()という関数で、FFTSetupというインスタンスのようなものを作成します。第一引数で指定するFFTのサイズは2の何乗か、になります。9なら512サンプル、10なら1024サンプルといった感じです。使う関数によって指定できるサイズの範囲が決まっています。

第二引数ではradixを指定します。今回はFFT_RADIX2を指定します。ほかにFFT_RADIX3とFFT_RADIX5もあり、FFTするサンプル数が合えば(2の累乗の3倍とか5倍とか)、こちらを使う事もできるようです。

//FFTするサイズを2の何乗かで指定する。この場合1024サンプル。
vDSP_Length log2n = 10;

//FFTセットアップを作成する
FFTSetup fftSetup = create_fftsetup(log2n, FFT_RADIX2);

複素数を実数と虚数を別々の配列でメンバに持つDSPSplitComplexという構造体を宣言し、realp(実数配列)とimagp(虚数配列)に、それぞれ変換するデータを入れたメモリ領域を割り当てます。

//FFTを行うデータを作成する
vDSP_Length fftSize = 1 << log2n;
DSPSplitComplex splitComplex;
splitComplex.realp = calloc(fftSize, sizeof(float));
splitComplex.imagp = calloc(fftSize, sizeof(float));
	
/*
ここでsplitComplex.realpとimagpに、変換するデータをコピーする
 */

変換する方向を指定します。正変換ならFFT_FORWARD、逆変換ならFFT_INVERSEです。ストライドも指定します。ここでは連続したデータと想定して1にしておきます。

//FFTか逆FFTを指定する。逆ならFFT_INVERSE
FFTDirection direction = FFT_FORWARD;
//ストライドを指定する
vDSP_Stride signalStride = 1;

fft_zip()関数で変換を行います。

//FFTを行う
fft_zip(fftSetup, &splitComplex, signalStride, log2n, direction);

なお、fft_zip()関数において逆変換をした場合には、FFTサイズ倍の大きさのレベルそのままでデータが返ってきていますので、FFTサイズで割ります。

//逆FFTの場合FFTサイズでシグナルを割る
if (direction == FFT_INVERSE) {
    float scale = 1.0 / fftSize;
    vsmul(splitComplex.realp, 1, &scale, splitComplex.realp, 1, fftSize);
    vsmul(splitComplex.imagp, 1, &scale, splitComplex.imagp, 1, fftSize);
}
	
/*
ここでsplitComplexからデータを取り出す
 */

このvsmul()という関数は、float配列の全要素に対して、同じ数をかけ算する関数です。このようにIn-Placeでも使えますし、アウト側に別の配列を渡してOut-of-Placeで使う事もできます。

最後にdestroy_fftsetup()でFFTセットアップを解放します。

//FFTセットアップを解放する
destroy_fftsetup(fftSetup);

free(splitComplex.realp);
free(splitComplex.imagp);

続けて変換を行うなら、fft_zipを繰り返して終わった後で解放すれば良いと思います。FFTをクラスとして作ってしまうなら、initでcreateして、deallocでdestroyという感じでしょうか。

と、単純にフーリエ変換はこんなところですが、フーリエ変換時によく使われる窓関数をかける関数もvDSPに用意されています。

vDSP_blkman_window (ブラックマン窓)
vDSP_hamm_window (ハミング窓)
vDSP_hann_window (ハニング窓)

という3種類(double用も含めると6種類)です。