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 その7 IOProc」への4件のフィードバック

  1. kotan

    初めまして。とてもわかりやすい説明でいつも参考にさせていただいています。
    ところで、少し疑問なのですが、
    IOProcは引数で入力バッファと出力バッファを指定できますよね
    という事は、例えばASIOの様に、IO両方を備え付けたデバイスを利用する場合
    引数inInputDataでレコーディングバッファを取得しながら
    outOutputDataでプレイバックバッファの出力が行えそうだと思うのですが、
    なぜか実現できません。。。
    よって苦肉の策として同じデバイスを、わざわざ入力用と出力用の二つのデバイスIDで開き
    個別にStartさせ実行しているのですが、とても実用的とは思えません…
    何か良い手法をご存知ではないでしょうか?

    返信
  2. Yasoshima

    入力と出力が違うオーディオデバイスじゃありませんか?Macの本体についてる入力と出力は別のオーディオデバイスだったりしますので。外付けのオーディオインターフェースだと、入力も出力も同じIDなので大丈夫だったと思います。最近iPhoneばっかりでMacのCore Audioがごぶさたになっていまして、ちょっとうろ覚えです、すみません。ちなみに、違うデバイスの入力から出力へスルーさせるのとかはAppleのサンプルのComplexPlayThruあたりが参考になると思います。

    返信
  3. kotan

    回答ありがとうございます。
    それが、同じIDでもAudioHardwareGetPropertyで
    DefaultInputDeviceを指定するとinputバッファを受信し
    DefaultOutputDeviceを指定するとoutputバッファを受信できます…
    おそらくハードウェアを開く時に渡す引数でIO両方を兼ねて開けそうかと踏んだのですが…
    ともあれcomplexplaythruを調べてみます!

    返信
  4. Yasoshima

    kotanさんがまたここをチェックしていただいている事を祈りつつ…。
    あらためて僕が試した限りではひとつのIOProcでインもアウトも来ています。確認で書きますが、同じIDというのはDefaultInputDeviceもDefaultOutputDeviceもホントに同じAudioDeviceIDでしょうか?同じであれば、このエントリのコードを動作させた場合、inInputDataにはインプットのオーディオデータが来ていますので、そのままoutOutputDataに書き込んでやれば音声がスルーされます。AudioDeviceIDはインプットもアウトプットも関係ありませんので、AudioDeviceCreateIOProcIDは一回だけです。最初のコメントに「入力用と出力用の二つのデバイスIDで開き」とありますが、IDが同じなのにふたつ開くというのは矛盾している気がします。

    返信

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です