Core Audio – Mac」カテゴリーアーカイブ

AVAudioEngine

iOS8からAVAudioEngineという新たなオーディオの機能がAVFoundationに追加されました。AVAudioEngineというのはAUGraphのObjective-C版のような感じのものです。オーディオのプレイヤーやエフェクトやミキサーなどがNodeと呼ばれる部品で用意されていて、それらを繋いで音を鳴らす事が出来ます。

AUGraphよりもかなり簡単に使えるようになっているのですが、その反面、AUGraphを使うときのように細かい設定が出来るわけではなかったり、AUGraphで出来た事が出来なかったりする部分もあるので、いままでAUGraphを使っていた人にとっては物足りないかもしれません。でも、そんなに複雑な事をやらないのであれば、簡単にエフェクトをかけられたりするので便利だと思います。

AVAudioEngineに関してはWWDC2014でも解説されていますので、ちゃんとしたApple公式の解説を聞きたい・読みたいという方はそちらをご覧ください。

#501 – What’s New in Core Audio
#502 – AVAudioEngine in Practice

他だと、@shu223さんのiOS8-SamplerというgitリポジトリにもAVAudioEngineのサンプルコードが入っているようですので、参考にされると良いと思います。
https://github.com/shu223/iOS8-Sampler

サンプルコード

では簡単に、AVAudioEngineでオーディオファイルを鳴らして見たいと思います。Xcode 6でSingle View Applicationなどで適当なiOSのプロジェクトを作ったら、ViewController.mを以下のコードに差し替えてください。

//
//  ViewController.m
//
#import "ViewController.h"
#import <avfoundation/AVFoundation.h>
@interface ViewController ()
@property (nonatomic) AVAudioEngine *audioEngine;
@property (nonatomic) AVAudioUnitReverb *reverbNode;
@property (nonatomic) AVAudioPlayerNode *playerNode;
@property (nonatomic) AVAudioFile *audioFile;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    if (![self setup]) {
        NSLog(@"AudioEngine Setup Failed.");
    }
    [self play:nil];
}
- (BOOL)setup
{
    NSError *error = nil;
    // AudioEngineの生成
    self.audioEngine = [[AVAudioEngine alloc] init];
    // オーディオファイルの生成
    NSURL *fileURL = [[NSBundle mainBundle] URLForAuxiliaryExecutable:@"Rhythm.wav"];
    self.audioFile = [[AVAudioFile alloc] initForReading:fileURL error:&error];
    if (error) {
        return NO;
    }
    // オーディオプレイヤーノードの生成
    self.playerNode = [[AVAudioPlayerNode alloc] init];
    // リバーブエフェクトノードの生成
    self.reverbNode = [[AVAudioUnitReverb alloc] init];
    [self.reverbNode loadFactoryPreset:AVAudioUnitReverbPresetMediumHall];
    self.reverbNode.wetDryMix = 50.0f;
    // ノードをAudioEngineへ追加
    [self.audioEngine attachNode:self.playerNode];
    [self.audioEngine attachNode:self.reverbNode];
    // ノードを接続
    [self.audioEngine connect:self.playerNode
                           to:self.reverbNode
                       format:self.audioFile.processingFormat];
    [self.audioEngine connect:self.reverbNode
                           to:self.audioEngine.mainMixerNode
                       format:self.audioFile.processingFormat];
    // AudioEngineの開始
    if (![self.audioEngine startAndReturnError:&error]) {
        return NO;
    }
    return YES;
}
- (IBAction)play:(id)sender
{
    // オーディオプレイヤーノードの開始
    [self.playerNode play];
    // オーディオファイルを再生
    [self.playerNode scheduleFile:self.audioFile
                           atTime:nil
                completionHandler:nil];
}
@end

また、何かオーディオファイルをXcodeのプロジェクトに追加して”Rhythm.wav”と名前を付けてください。逆に、コード上の”Rhythm.wav”の部分を、用意したオーディオのファイルの名前に書き換えてもかまいません。拡張子はwavにしていますが、mp3でも何でも、iOSで普通にならせるオーディオのフォーマットであれば何でも構いません。

オーディオファイルを追加したら、ビルドして実行すると1回オーディオファイルが再生されると思います。UIButtonとかをplay:メソッドに接続すればタップして何度も鳴らす事が出来ます。

解説

上記のコードでは以下の図のような構成でAudioEngineをセットアップしています。ただ音を鳴らすだけではAVAudioEngineを使う意味が無いので、オーディオファイルの音にリバーブをかけて再生しています。

20140614AVAudioEngine.001

セットアップの全体の流れは、「AudioEngineを生成」→「Nodeを生成」→「必要があればNodeのプロパティなどを設定(だいたい後でも可)」→「Node同士を接続」→「AudioEngineをスタート」という感じです。Nodeの接続がうまくいってない場合はAudioEngineをスタートする(startAndReturnError:メソッドを呼ぶ)時点でエラーが返ってきます。

クラス

AVFoundationにAVAudioEngine関連で追加されたクラスには以下のものがあります。

  • AVAudioEngine(オーディオグラフ全体の管理)
  • AVAudioNodeとそのサブクラス(グラフ内のオーディオの部品)
  • AVAudioBuffer・AVAudioPCMBuffer(オーディオのバッファ)
  • AVAudioFile(オーディオファイル)
  • AVAudioFormat(オーディオのフォーマット。AudioStreamBasicDescription+αの情報)
  • AVAudioTime(オーディオ時間。再生するタイミングなど)
  • AVAudioChannelLayout、他
  • 以前からAVFoundationにあったクラスと結構名前が似通ったものがあるので注意してください(AVAudioPlayerとAVAudioPlayerNodeや、AVAudioMixとAVAudioMixingなど)。あとAudioUnitEffectとそのサブクラスはAVAudioNodeのサブクラスではあるのですが、クラス名にNodeと付いていないのでちょっと分かりにくいかもしれません。

    ノード同士の接続

    Node同士を接続するにはconnect:to:format:メソッドを使います。「接続元のNode」と「接続先のNode」と「接続間のフォーマット」を引数に渡します。フォーマットに関しては、今回みたいにオーディオファイルを鳴らすならAVAudioFileのprocessingFormatにいい感じのフォーマットが入っているのでそれを渡せばOKです。そんなのが無いという場合はAVAudioFormatのinitStandardFormat〜という初期化メソッドがあるのでそれで生成すれば、いい感じのやつを作ってくれます。AVAudioEngineではAudioStreamBasicDescriptionの全要素を自分で埋める場面というのはほとんど無いと思います。

    なお、セットアップ図にある「AVAudioMixerNode」と「AVAudioOutputNode」というのは最初から「mainMixerNode」と「outputNode」というプロパティでAVAudioEngineに用意されていて、なおかつmainMixerNodeとoutputNodeは最初から接続もされています。最初からAVAudioEngineに用意されているものはほかにも、inputNodeやmusicSequenceなどがあります。

    また、サンプルでは使っていませんがNodeのBusを指定して接続するメソッド(connect:to:fromBus:toBus:format:)もあって、ミキサーに複数接続する場合は別々のBusを指定して接続すれば同時に複数のNodeを繋げて鳴らす事が出来ます。

    オーディオプレイヤー

    オーディオファイルの再生には「AVAudioPlayerNode」というNodeを使っています。これはAVFoundationのAVAudioPlayerとは使い勝手が違って、AudioUnitのAudioFilePlayerがベースになっています。AVAudioPlayerのようにオーディオファイルのオブジェクトがあってそれを再生するというのではなく、プレイヤーがあって、そこにいろいろなオーディオファイルの鳴らしたいタイミングを登録していくという感じです。サンプルではatTimeのところの引数がnilなのですぐに鳴りますが、ここにAVAudioTimeで時間を指定すれば何秒後に鳴らすといったことが可能です。また、複数登録しておく事も出来ますが、ひとつのプレイヤーからは同時にひとつの音源しか鳴らせないので、複数のオーディオファイルを同時に鳴らしたいという場合はプレイヤーを複数作る必要があります。

    AVAudioPlayerNodeではオーディオファイルを直接ならす以外にも、AVAudioPCMBufferにオーディオデータを書き込んでおいて、そこから再生する事も出来ます。その場合にはループ再生をするオプションも指定できます。

    ボリューム

    AVAudioEngineの特徴であるのですが、ミキサーのインプット側のボリュームの設定はミキサーには無く、接続元の音源であるNode側にあります。ミキサーでボリューム調整ができるNodeはAVAudioMixingというプロトコルに準拠していてvolumeプロパティを持っています。ミキサーがインプットに接続されているNodeを辿っていって、AVAudioMixingに準拠しているNodeがあったらそのvolumeを見てレベル調整をしているようです。

    まとめ

    AVAudioEngineの利点は、サンプルのコードを見てもらうとわかるとおり、ちょっとエフェクトをかけたいというときにはすごく少ないコード量で実現できるというところです。Swiftからも特に制限無くAVAudioEngineの機能にアクセスできるので、Swift推進派な人には良いかもしれません。

    逆に欠点としては、ProcessingTap的なものが無くて独自の処理をオーディオグラフの途中に挟み込めないということと、SplitterやConverterのNodeがなくて接続の自由度が低いということです。この辺りの機能が追加されるなら、完全にAUGraphからAVAudioEngineに乗り換えられると思うので、今後に期待したいですね。

    CoreAudio その3〜5改 AudioObjectでのプロパティ取得・設定

    Mac OS X 10.6 Snow Leopardから、AudioHardware〜とかAudioDevice〜とかAudioStream〜とかのGet・Set系がことごとくDEPRECATEDになってしまいました。

    僕の認識では、もともとAudioObject〜があって、そのコンビニエンスメソッドとしてAudioDeviceとかの関数があると思っていたのですけど、どうやら歴史的には逆だったようです。ヘッダをよく見てみたら、AudioObject系はわりと最近の10.4から増えてたんですね。

    あと、AudioUnitがiPhoneと同じようにAudioComponentにいきなり変わっててびっくりしました。iPhoneで慣れ親しんでいたとはいえ、もちょっと緩やかに移行してくれてもいいんじゃないかなと思います。

    プロパティの定数系も結構Deprecatedしてます。これはXcodeで警告を出してくれないので注意が必要です。ヘッダのコメントに”Some Day Be Deprecated”なんてこっそりかかれてます。Device系だとkAudioDevicePropertyDeviceNameから始まるところですね。10.7になっていきなりエラー出まくりなんて事のないようにしっかりチェックしておきましょう。

    ということで、取得・設定系をAudioObjectを使ってやってみたいと思います。

    AudioObjectでオーディオ情報の取得・設定をするにはAudioObjectGetPropertyDataやAudioObjectSetPropertyDataという関数を使います。これはいままでAudioHardware〜やAudioDevice〜など別々の関数でやっていたオーディオ情報の取得や設定を、ひとつでまかなえるものです。プロパティのサイズを調べるのにも、AudioObjectGetPropertyDataSizeという関数で行う事になります。

    たとえば、プロパティの取得する関数はこんな感じで定義されています。

    extern OSStatus
    AudioObjectGetPropertyData(AudioObjectID inObjectID,
                               const AudioObjectPropertyAddress* inAddress,
                               UInt32 inQualifierDataSize,
                               const void* inQualifierData,
                               UInt32* ioDataSize,
                               void* outData)
    

    最初のAudioObjectIDというのはAudioDeviceIDやAudioStreamIDをそのまま渡します。前の記事のその3でも説明しましたが、AudioDeviceIDもAudioStreamIDもAudioObjectIDをtypedefしているだけのもので、つまり全てAudioObjectIDです(リファレンス的にAudioDeviceなどはAudioObjectのサブクラスってことらしい)。AudioHardware〜系の関数をつかっていたプロパティのときはkAudioObjectSystemObjectを渡します。

    ちょっとひとつ飛ばしまして、inQualifierDataSizeとinQualifierDataは、サンプルで使っているものが無かったのでよくわかりません。基本0とNULLで大丈夫なようです。もしかしたらまだ機能していないかもしれません。ioDataSizeとoutDataは取得するプロパティのサイズと受け取るメモリ領域で、前と同じです。

    戻ってAudioObjectPropertyAddressは、

    struct  AudioObjectPropertyAddress
    {
        AudioObjectPropertySelector mSelector;
        AudioObjectPropertyScope    mScope;
        AudioObjectPropertyElement  mElement;
    };
    typedef struct AudioObjectPropertyAddress   AudioObjectPropertyAddress;
    

    と定義されてます。デバイスとかストリームとか関係なくプロパティを特定するのに必要な情報ですね。

    Selectorは〜PropertyIDの事です。Scopeは、ほとんどの場合はkAudioObjectPropertyScopeGlobalで、Deviceなどの入出力部分のときはInputとかOutputで指定したりします。Elementは基本kAudioObjectPropertyElementMaster(= 0)でOKだと思います(これ以外指定するパターンがすぐに見つけられませんでした。Channel的なところで0以外を使うのではないかと思います)。

    最後に、サンプルソースです。Xcodeで新規プロジェクト→Command Line Tool→Foundationでプロジェクトを作成して、CoreAudio.Frameworkを追加して、main.mを以下のソースに差し替えてください。走らせると、デフォルトになっているオーディオデバイスの名前を表示して、サンプルレートを設定可能な中で変更します。

    #import <Foundation/Foundation.h>
    #import <CoreAudio/CoreAudio.h>
    int main (int argc, const char * argv[]) {
        
        AudioDeviceID devID;
        UInt32 size;
        AudioObjectPropertyAddress address;
        AudioValueRange *sampleRates;
        CFStringRef deviceName = NULL;
        //デフォルトのアウトプットに設定されているオーディオデバイスを取得する
        address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
        address.mScope = kAudioObjectPropertyScopeGlobal;
        address.mElement = kAudioObjectPropertyElementMaster;
        size = sizeof(devID);
        
        AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &devID);
        
        //アウトプットの名前を取得する
        address.mSelector = kAudioObjectPropertyName;
        size = sizeof(deviceName);
        
        AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &deviceName);
        
        //アウトプットのデバイスが対応しているサンプルレートを取得する
        address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
        AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size);
        
        UInt32 numOfSampleRates = size / sizeof(AudioValueRange);
        sampleRates = calloc(numOfSampleRates, sizeof(AudioValueRange));
        
        AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, sampleRates);
        
        //現在のサンプルレートを取得する
        Float64 currentSampleRate;
        address.mSelector = kAudioDevicePropertyNominalSampleRate;
        size = sizeof(Float64);
        
        AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, ¤tSampleRate);
        
        //サンプルレートを別のにする
        int currentIndex = 0;
        for (int i = 0; i < numOfSampleRates; i++) {
            if (sampleRates[i].mMinimum == currentSampleRate) {
                currentIndex = i;
                break;
            }
        }
        
        int newIndex = currentIndex + 1;
        if (newIndex >= numOfSampleRates) newIndex = 0;
        
        //サンプルレートを設定する
        Float64 newSampleRate = sampleRates[newIndex].mMinimum;
        size = sizeof(Float64);
        
        AudioObjectSetPropertyData(devID, &address, 0, NULL, size, &newSampleRate);
        
        NSLog(@"device name = %@", deviceName);
        NSLog(@"new samplerate = %f", newSampleRate);
        
        free(sampleRates);
        CFRelease(deviceName);
        
        return 0;
    }
    

    Core Audio Clock その4 スレーブ時の再生スピード調整

    Core Audio ClockをMTCのスレーブにした時、あるいはインターナルのソースをHostTimeにした時、Core Audio Clockの時間を基準にしてオーディオを再生しようとすると、オーディオデバイスとスピードのずれが出てきますので、調整しなければいけません。

    まず、オーディオの再生をしているときのIOProc内で、再生する頭のタイミングのHostTime(以下スタートホストタイム)と、HostTimeとオーディオデバイスのスピードの比率である「RateScalar」が取得できます。それと、MTCのスレーブにしているときはCoreAudioClockの「PlayRate」が、MTCとHostTimeのスピードの比率になっていますので、基本的にこれらを使って調整してみます。

    1回のIOProcごとのオーディオデータを用意するスタート時間としてスタートホストタイムを設定し、オーディオデータに( RateScalar × PlayRate )で求めたレートでVariSpeed等をかけてやれば良いはずなのですが、実際はスタートホストタイムがアバウトで微妙にデバイスとずれていたりして音を鳴らすとプチプチとノイズが入ってしまったりします。

    なので、スタートホストタイムは直接使わずに、( RateScalar × PlayRate )で調整したバッファフレームサイズ分の時間を毎度足していった値をスタート時間にして、スタートホストタイムとの差を少しずつ調整していくという感じでやってみました。

    ということで、インスタンス変数には以下のようなものがあるとしておいて、

    CAClockRef clockRef; //Core Audio Clock
    Float64 deviceSampleRate; //オーディオデバイスのサンプリング周波数
    UInt32 bufferFrames; //オーディオデバイスのバッファフレーム数
    double preSeconds; //スタート時間の差を求めるための時間
    

    IOProc内で以下のようなメソッドを呼ぶという感じで試してみました。ここでは、オーディオデータのスタート時間と1フレーム分の時間を求めています。

    - (void)ioProc:(AudioBufferList *)outOutputData 
        outputTime:(const AudioTimeStamp *)inOutputTime
    {
        OSStatus err;
        
        //アウトプットデバイスのRateScalarを取得する
        double rateScalar = inOutputTime->mRateScalar;
        //スピード調整のためのレート
        double ajustRate;
        //今回のスタート時間
        double startSeconds;
        
        //CAClockのプレイレートを取得する
        double playRate;
        err = CAClockGetPlayRate(clockRef, &playRate);
        if (err != noErr) NSLog(@"get PlayRate Err");
        
        //アウトプットデバイスのスタート時間をホストタイムから変換して取得する
        CAClockTime inHostTime;
        inHostTime.format = kCAClockTimeFormat_HostTime;
        inHostTime.time.hostTime = inOutputTime->mHostTime;
        
        CAClockTime outSecondsTime;
        outSecondsTime.format = kCAClockTimeFormat_Seconds;
        
        err = CAClockTranslateTime(clockRef, &inHostTime, 
            kCAClockTimeFormat_Seconds, &outSecondsTime);
        if (err != noErr) NSLog(@"translatetime Err");
        
        //前回コールバック時に求めたスタート時間との差
        double gap = outSecondsTime.time.seconds - preSeconds;
        
        //今回のスタート時間を設定する。前回との差が大きければリセット
        if (fabs(gap) > 0.1) {
            startSeconds = outSecondsTime.time.seconds;
            gap = 0.0;
        } else {
            startSeconds = preSeconds;
        }
        
        //差がきわめて小さければ調整はしない。
        //調整するなら差の大きさによって調整するレートを求める
        if (fabs(gap) > 0.0001) {
            ajustRate = pow(10, gap);
        } else {
            ajustRate = 1.0;
        }
        
        //1サンプル分の時間
        double secondsPerFrame = 
            playRate * rateScalar / deviceSampleRate * ajustRate;
        //次のコールバック時のスタート時間を算出する
        preSeconds = startSeconds + secondsPerFrame * bufferFrames;
    
    
        //
        // ここで、startSecondsとsecondsPerFrameを元に
        // オーディオデータを書き込む
        //
    }
    

    Core Audio Clock その3 MTCスレーブ改

    前回のCore Audio Clock その2で、MTCを受信してCore Audio Clockをスレーブで動かすというのをやりましたが、シンクソースにMTCを設定するだけではクォーターフレームメッセージしか受信してくれないので、フルタイムコードメッセージも受け取って再生時以外のタイムコードにも対応できるようにしてみたいと思います。

    あれこれ試してみたところ、フルタイムコードをCore Audio Clockが勝手に解析してくれるような機能はないようなので、一旦フルタイムコードメッセージからタイムコードを抜き出してCore Audio ClockのCAClockSetCurrentTimeで設定します。

    ただ、CAClockSetCurrentTimeは停止中でないと受け付けてくれないので、MTCを送信してきているシーケンサーが停止時にフルタイムコードを送ってきても、Core Audio ClockはMTCFreewheelTimeで設定されている時間の後に停止するので、そのタイミングでセットしないと受け付けてくれません。

    そんな感じで作り直してみたのが以下のコードです。前回プラスCoreMIDI.Frameworkになります。

    //
    //  CAClockMTCSlaveTest.h
    //
    
    #import <Cocoa/Cocoa.h>
    #import <CoreMIDI/CoreMIDI.h>
    #import <AudioToolbox/AudioToolbox.h>
    
    @interface CAClockMTCSlaveTest : NSObject {
        
        CAClockRef clockRef;
        MIDIEndpointRef srcPointRef;
        BOOL isStart;
        NSTimer *timer;
        IBOutlet NSTextField *textField;
        
        MIDIClientRef clientRef;
        MIDIPortRef inputPortRef;
        CAClockSeconds keepSeconds;
    }
    
    - (void)clockListener:(CAClockMessage)message
        parameter:(const void *)param;
    - (void)checkTime:(NSTimer *)timr;
    - (void)setCurrentTime:(NSNumber *)secondsNumber;
    - (void)setFullTimecode:(MIDIPacket *)packet;
    
    @end
    
    
    //
    //  CAClockMTCSlaveTest.m
    //
    
    #import "CAClockMTCSlaveTest.h"
    
    @implementation CAClockMTCSlaveTest
    
    #pragma mark -
    #pragma mark -- コールバック --
    
    static void 
    ClockListener(void *userData, 
        CAClockMessage message, const void *param)
    {
        [(id)userData clockListener:message parameter:param];
    }
    
    - (void)clockListener:(CAClockMessage)message 
        parameter:(const void *)param
    {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        
        switch (message) {
            case kCAClockMessage_Started:
                isStart = YES;
                NSLog(@"started");
                break;
            case kCAClockMessage_Stopped:
                isStart = NO;
                [self setCurrentTime:
                    [NSNumber numberWithDouble:keepSeconds]];
                NSLog(@"stoped");
                break;
            case kCAClockMessage_Armed:
                NSLog(@"armed");
                break;
            case kCAClockMessage_Disarmed:
                NSLog(@"disarmed");
                break;
            case kCAClockMessage_WrongSMPTEFormat:
                NSLog(@"wrongSMPTEFormat");
                break;
            default:
                break;
        }
        
        [pool drain];
    }
    
    static void 
    MIDIInputProc(const MIDIPacketList *pktlist, 
        void *readProcRefCon, void *srcConnRefCon)
    {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        
        //MIDIパケットリストの先頭のMIDIPacketのポインタを取得
        MIDIPacket *packet = (MIDIPacket *)&(pktlist->packet[0]);
        //パケットリストからパケットの数を取得
        UInt32 packetCount = pktlist->numPackets;
        
        for (NSInteger i = 0; i < packetCount; i++) {
            
            //フルタイムコードであれば処理をする
            if ((packet->data[0] == 0xF0) &&
                (packet->data[1] == 0x7F) && 
                (packet->data[2] == 0x7F) && 
                (packet->data[3] == 0x01) && 
                (packet->data[4] == 0x01)) {
                
                [(id)readProcRefCon setFullTimecode:packet];
            }
            
            //次のパケットへ進む
            packet = MIDIPacketNext(packet);
        }
        
        [pool drain];
    }
    
    - (void)setFullTimecode:(MIDIPacket *)packet
    {
        OSStatus err;
        
        SMPTETime smpteTime;
        smpteTime.mType = kSMPTETimeType30;
        smpteTime.mHours = packet->data[5] & 0x0F;
        smpteTime.mMinutes = packet->data[6];
        smpteTime.mSeconds = packet->data[7];
        smpteTime.mFrames = packet->data[8];
        smpteTime.mSubframeDivisor = 80;
        smpteTime.mSubframes = 0;
        
        CAClockSeconds seconds;
        err = CAClockSMPTETimeToSeconds(
            clockRef, &smpteTime, &seconds);
        if (err != noErr) {
            NSLog(@"SMPTETimeToSecond err = %d", err);
            return;
        }
        
        NSNumber *secondsNumber = [NSNumber numberWithDouble:seconds];
        [self performSelectorOnMainThread:@selector(setCurrentTime:) 
            withObject:secondsNumber 
            waitUntilDone:NO];
    }
    
    
    #pragma mark -
    #pragma mark -- タイムコードをセット --
    
    - (void)setCurrentTime:(NSNumber *)secondsNumber
    {
        CAClockSeconds seconds = [secondsNumber doubleValue];
        
        if (!isStart) {
            
            CAClockTime time;
            time.format = kCAClockTimeFormat_Seconds;
            time.time.seconds = seconds;
            
            OSStatus err = CAClockSetCurrentTime(clockRef, &time);
            if (err != noErr) {
                NSLog(@"set setCurrentTime err");
            }
            
        } else {
            
            keepSeconds = seconds;
        }
    }
    
    #pragma mark -
    #pragma mark -- 初期化など --
    
    - (void)awakeFromNib
    {
        OSStatus err = noErr;
        UInt32 size;
        
        //MIDIエンドポイントを取得する
        srcPointRef = MIDIGetSource(0);
        
        //MIDIエンドポイントから名前を取得して表示
        CFStringRef strSrcRef;
        err = MIDIObjectGetStringProperty(
            srcPointRef, kMIDIPropertyDisplayName, &strSrcRef);
        if (err != noErr) {
            NSLog(@"MIDI Get sourceName err = %d", err);
            goto end;
        }
        NSLog(@"connect = %@", strSrcRef);
        CFRelease(strSrcRef);
        
        
        //CAClockを作成する
        err = CAClockNew(0, &clockRef);
        if (err != noErr) {
            NSLog(@"CAClockNew err = %d", err);
            goto end;
        }
        <
    br />    //シンクモードをMTCにする
        UInt32 tSyncMode = kCAClockSyncMode_MTCTransport;
        size = sizeof(tSyncMode);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SyncMode, size, &tSyncMode);
        if (err != noErr) {
            NSLog(@"set syncmode Err = %d", err);
            goto end;
        }
        
        //CAClockの同期元にMIDIエンドポイントを設定する
        size = sizeof(srcPointRef);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SyncSource, size, &srcPointRef);
        if (err != noErr) {
            NSLog(@"caclock setSyncSourct err = %d", err);
            goto end;
        }
        
        //SMPTEを30fpsに設定する
        UInt32 tSMPTEType = kSMPTETimeType30;
        size = sizeof(tSMPTEType);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SMPTEFormat, size, &tSMPTEType);
        if (err != noErr) {
            NSLog(@"set smptetype Err = %d", err);
            goto end;
        }
        
        //MTCが停止しても動き続ける時間を設定する
        CAClockSeconds freeWheelTime = 0.2;
        size = sizeof(freeWheelTime);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_MTCFreewheelTime, 
            size, &freeWheelTime);
        if (err != noErr) {
            NSLog(@"set MTCFreewheelTime Err = %d", err);
            goto end;
        }
        
        //CAClockからの通知を受け取る関数を設定する
        err = CAClockAddListener(clockRef, ClockListener, self);
        if (err != noErr) {
            NSLog(@"caclock addListener err = %d", err);
            goto end;
        }
        
        //シンクソースとの同期を開始する
        err = CAClockArm(clockRef);
        if (err != noErr) {
            NSLog(@"CAClock arm err = %d", err);
            goto end;
        }
        
        
        //
        // フルタイムコードを受信するための設定
        //
        
        //MIDIクライアントを作成する
        NSString *clientName = @"inputClient";
        err = MIDIClientCreate(
            (CFStringRef)clientName, NULL, NULL, &clientRef);
        if (err != noErr) {
            NSLog(@"MIDIClientCreate err = %d", err);
            goto end;
        }
        
        //MIDIポートを作成する
        NSString *inputPortName = @"inputPort";
        err = MIDIInputPortCreate(
            clientRef, (CFStringRef)inputPortName, 
            MIDIInputProc, self, &inputPortRef);
        if (err != noErr) {
            NSLog(@"MIDIInputPortCreate err = %d", err);
            goto end;
        }
        
        //MIDIエンドポイントをポートに接続する
        err = MIDIPortConnectSource(inputPortRef, srcPointRef, NULL);
        if (err != noErr) {
            NSLog(@"MIDIPortConnectSource err = %d", err);
            goto end;
        }
        
        
        //タイマーを開始する
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 
            target:self selector:@selector(checkTime:) 
            userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] 
            addTimer:timer forMode:NSEventTrackingRunLoopMode];
        
    	return;
        
    end:
        
    	[NSApp terminate:self];
        return;
    }
    
    - (void) dealloc
    {
        [timer invalidate];
        
        OSStatus err;
        
        err = MIDIPortDisconnectSource(inputPortRef, srcPointRef);
        if (err != noErr) NSLog(@"MIDIPortDisconnectSource Err"); 
        err = MIDIPortDispose(inputPortRef);
        if (err != noErr) NSLog(@"MIDIPortDispose Err");
        err = MIDIClientDispose(clientRef);
        if (err != noErr) NSLog(@"MIDIClientDispose Err");
        
        err = CAClockDisarm(clockRef);
        if (err != noErr) NSLog(@"clock disarm Err");
        err = CAClockDispose(clockRef);
        if (err != noErr) NSLog(@"CAClockDispose err");
        
        [super dealloc];
    }
    
    #pragma mark -
    #pragma mark -- タイムの表示 --
    
    //現在のタイムを表示する
    - (void)checkTime:(NSTimer *)timr
    {
        OSStatus err;
        CAClockTime secondTime;
        
        //再生中か停止中かで取得するタイムを変える
        if (isStart) {
            //カレントタイムを取得する
            err = CAClockGetCurrentTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        } else {
            //スタートタイムを取得する
            err = CAClockGetStartTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        }
        
        CAClockSeconds seconds = secondTime.time.seconds;
        
        //秒数からタイムコードに変換する
        SMPTETime tSMPTETime;
        err = CAClockSecondsToSMPTETime(clockRef, seconds, 80, &tSMPTETime);
        if (err != noErr) {
            NSLog(@"secondsToSMPTE err = %d", err);
            return;
        }
        
        SInt16 tHours = tSMPTETime.mHours;
        SInt16 tMinutes = tSMPTETime.mMinutes;
        SInt16 tSeconds = tSMPTETime.mSeconds;
        SInt16 tFrames = tSMPTETime.mFrames;
        
        Float64 tPlayRate;
        err = CAClockGetPlayRate(clockRef, &tPlayRate);
        if (err != noErr) {
            NSLog(@"getPlayRate err = %d", err);
            return;
        }
        
        //タイムを表示する
        NSString *tSMPTEString = 
        [NSString stringWithFormat:
          @"seconds = %f / SMPTE = %2.2hi.%2.2hi.%2.2hi.%2.2hi / PlayRate = %f", 
          seconds, tHours, tMinutes, tSecond
    s, tFrames, tPlayRate];
        
        [textField setStringValue:tSMPTEString];
    }
    
    @end
    

    Core Audio Clock その2 MTCスレーブ

    Core Audio ClockをMTCのスレーブにするサンプルです。

    あれこれMIDIの設定とかしなくちゃいけないのかと思っていたら、意外と簡単でした。MIDIClientとかPortとかつくらずに、MIDIEndPointを直接渡してしまえばいいだけみたいです。

    設定の順番としては、

    ・CAClockを作成する
    ・SyncModeをMTCTransportにする
    ・SyncSourceにMTCが送られてくるMIDIEndPointを設定する
    ・タイムコードのフォーマットを送信側と合わせておく
    ・CAClockArmでMTCを受信できる状態にする
    ・同期する必要がなくなったらCAClockDisarmで解除する

    といった感じです。

    Cocoaアプリケーションを作成して、AudioToolboxとCoreMIDIのフレームワークをインポートし、以下のクラスを作成し、タイムコード表示用のテキストフィールドをアウトレットに設定します。MIDIのインプットは一つしか無い事を想定していますので、Audio MIDI 設定でうまいこと設定してください。同じMac上のMIDIシーケンサから受け取る場合は、IACドライバの「装置はオンライン」にチェックして、シーケンサのMTCのアウトをIACのバスを通して送られてくるように設定します。

    //
    //  CAClockMTCSlaveTest.h
    //
    
    #import <Cocoa/Cocoa.h>
    #import <CoreMIDI/CoreMIDI.h>
    #import <AudioToolbox/CoreAudioClock.h>
    
    @interface CAClockMTCSlaveTest : NSObject {
        
        CAClockRef clockRef;
        MIDIEndpointRef srcPointRef;
        BOOL isStart;
        NSTimer *timer;
        IBOutlet NSTextField *textField;
    }
    
    - (void)clockListener:(CAClockMessage)message parameter:(const void *)param;
    - (void)checkTime:(NSTimer *)timr;
    
    @end
    
    
    //
    //  CAClockMTCSlaveTest.m
    //
    
    #import "CAClockMTCSlaveTest.h"
    
    
    @implementation CAClockMTCSlaveTest
    
    #pragma mark -
    #pragma mark -- コールバック --
    
    static void 
    ClockListener(void *userData, CAClockMessage message, const void *param)
    {
        [(id)userData clockListener:message parameter:param];
    }
    
    - (void)clockListener:(CAClockMessage)message parameter:(const void *)param
    {
        switch (message) {
            case kCAClockMessage_Started:
                isStart = YES;
                NSLog(@"started");
                break;
            case kCAClockMessage_Stopped:
                isStart = NO;
                NSLog(@"stoped");
                break;
            case kCAClockMessage_Armed:
                NSLog(@"armed");
                break;
            case kCAClockMessage_Disarmed:
                NSLog(@"disarmed");
                break;
            case kCAClockMessage_WrongSMPTEFormat:
                NSLog(@"wrongSMPTEFormat");
                break;
            default:
                break;
        }
    }
    
    #pragma mark -
    #pragma mark -- 初期化など --
    
    - (void)awakeFromNib
    {
        OSStatus err = noErr;
        UInt32 size;
        
        //MIDIエンドポイントを取得する
        srcPointRef = MIDIGetSource(0);
        
        //MIDIエンドポイントから名前を取得して表示
        CFStringRef strSrcRef;
        err = MIDIObjectGetStringProperty(
            srcPointRef, kMIDIPropertyDisplayName, &strSrcRef);
        if (err != noErr) {
            NSLog(@"MIDI Get sourceName err = %d", err);
            goto end;
        }
        NSLog(@"connect = %@", strSrcRef);
        CFRelease(strSrcRef);
        
        
        //CAClockを作成する
        err = CAClockNew(0, &clockRef);
        if (err != noErr) {
            NSLog(@"CAClockNew err = %d", err);
            goto end;
        }
        
        //シンクモードをMTCにする
        UInt32 tSyncMode = kCAClockSyncMode_MTCTransport;
        size = sizeof(tSyncMode);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SyncMode, size, &tSyncMode);
        if (err != noErr) {
            NSLog(@"set syncmode Err = %d", err);
            goto end;
        }
        
        //CAClockの同期元にMIDIエンドポイントを設定する
        size = sizeof(srcPointRef);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SyncSource, size, &srcPointRef);
        if (err != noErr) {
            NSLog(@"caclock setSyncSourct err = %d", err);
            goto end;
        }
        
        //SMPTEを30fpsに設定する
        UInt32 tSMPTEType = kSMPTETimeType30;
        size = sizeof(tSMPTEType);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SMPTEFormat, size, &tSMPTEType);
        if (err != noErr) {
            NSLog(@"set smptetype Err = %d", err);
            goto end;
        }
        
        //CAClockからの通知を受け取る関数を設定する
        err = CAClockAddListener(clockRef, ClockListener, self);
        if (err != noErr) {
            NSLog(@"caclock addListener err = %d", err);
            goto end;
        }
        
        err = CAClockArm(clockRef);
        if (err != noErr) {
            NSLog(@"CAClock arm err = %d", err);
            goto end;
        }
        
        //タイマーを開始する
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 
            target:self selector:@selector(checkTime:) 
            userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] 
            addTimer:timer forMode:NSEventTrackingRunLoopMode];
        
        return;
        
    end:
        
        [NSApp terminate:self];
        return;
    }
    
    - (void) dealloc
    {
        [timer invalidate];
    
        OSStatus err;
        
        err = CAClockDisarm(clockRef);
        if (err != noErr) NSLog(@"clock disarm Err");
        
        err = CAClockDispose(clockRef);
        if (err != noErr) NSLog(@"CAClockDispose err");
        
        [super dealloc];
    }
    
    #pragma mark -
    #pragma mark -- タイムの表示 --
    
    //現在のタイムを表示する
    - (void)checkTime:(NSTimer *)timr
    {
        OSStatus err;
        CAClockTime secondTime;
        
        //再生中か停止中かで取得するタイムを変える
        if (isStart) {
            //カレントタイムを取得する
            err = CAClockGetCurrentTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        } else {
            //スタートタイムを取得する
            err = CAClockGetStartTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        }
        
        CAClockSeconds seconds = secondTime.time.seconds
    ;
        
        //秒数からタイムコードに変換する
        SMPTETime tSMPTETime;
        err = CAClockSecondsToSMPTETime(clockRef, seconds, 80, &tSMPTETime);
        if (err != noErr) {
            NSLog(@"secondsToSMPTE err = %d", err);
            return;
        }
        
        SInt16 tHours = tSMPTETime.mHours;
        SInt16 tMinutes = tSMPTETime.mMinutes;
        SInt16 tSeconds = tSMPTETime.mSeconds;
        SInt16 tFrames = tSMPTETime.mFrames;
        
        Float64 tPlayRate;
        err = CAClockGetPlayRate(clockRef, &tPlayRate);
        if (err != noErr) {
            NSLog(@"getPlayRate err = %d", err);
            return;
        }
        
        //タイムを表示する
        NSString *tSMPTEString = 
        [NSString stringWithFormat:
            @"seconds = %f / SMPTE = %2.2hi.%2.2hi.%2.2hi.%2.2hi / PlayRate = %f"
            , seconds, tHours, tMinutes, tSeconds, tFrames, tPlayRate];
        
        [textField setStringValue:tSMPTEString];
    }
    
    @end
    

    実行して、シーケンサ等からMTCを送り込まれると、受信してタイムを表示するはずです。ただ、SMPTETimeTypeに29.97を設定しても受信出来ませんでした。29.97を受信する時は30にSMPTETimeTypeを設定しておいて、同期するとPlayRateが0.999付近になります。

    Core Audio Clock その1 インターナル

    ちょっと最近Core Audio Clockの使い方を調べていたので、分かったところまでを書いておきます。たぶんオーディオデバイスとMIDIデータとの同期などに本来の威力を発揮するのでしょうが、MIDI関係はまだちゃんと調べていないので、とりあえず内部のクロックをソースにした簡単な動かし方といったところです。

    最初からいろいろ書くのはめんどくさいので、とりあえずサンプルコードからです。単純なストップウォッチです。

    //
    //  CAClockTest.h
    //
    
    #import <Cocoa/Cocoa.h>
    #import <AudioToolbox/AudioToolbox.h>
    
    @interface CAClockTest : NSObject {
        
        CAClockRef clockRef;
        BOOL isRunning;
        NSTimer *timer;
        IBOutlet NSTextField *textField;
    }
    
    - (void)clockListener:(CAClockMessage)message parameter:(const void *)param;
    - (void)checkTime;
    - (IBAction)start:(id)sender;
    - (IBAction)stop:(id)sender;
    - (IBAction)reset:(id)sender;
    
    @end
    
    
    //
    //  CAClockTest.m
    //
    
    #import "CAClockTest.h"
    
    @implementation CAClockTest
    
    static void 
    ClockListener(void *userData, CAClockMessage message, const void *param)
    {
        [(id)userData clockListener:message parameter:param];
    }
    
    - (void)clockListener:(CAClockMessage)message parameter:(const void *)param
    {
        switch (message) {
            case kCAClockMessage_Started:
                isRunning = YES;
                break;
            case kCAClockMessage_Stopped:
                isRunning = NO;
                break;
            case kCAClockMessage_StartTimeSet:
                [self checkTime:nil];
                break;
            default:
                break;
        }
    }
    
    //初期化
    - (void)awakeFromNib
    {
        OSStatus err = noErr;
        UInt32 size;
        
        isRunning = NO;
    	
        //CAClockを作成する。
        err = CAClockNew(0, &clockRef);
        if (err != noErr) {
            NSLog(@"CAClockNew err = %d", err);
            goto catchErr;
        }
        
        //同期モードを設定。Internalはデフォルトだけど一応設定
        UInt32 syncMode = kCAClockSyncMode_Internal;
        size = sizeof(syncMode);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SyncMode, size, &syncMode);
        if (err != noErr) {
            NSLog(@"set syncmode Err = %d", err);
            goto catchErr;
        }
        
        //クロックのソースを設定する。HostTimeもデフォルトだけど一応設定
        CAClockTimebase tTimeBase;
        tTimeBase = kCAClockTimebase_HostTime;
        size = sizeof(tTimeBase);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_InternalTimebase, size, &tTimeBase);
        if (err != noErr) {
            NSLog(@"set internalTimebase Err = %d", err);
            goto catchErr;
        }
        
        //SMPTEのフォーマットを30フレームに設定
        UInt32 tSMPTEType = kSMPTETimeType30;
        size = sizeof(tSMPTEType);
        err = CAClockSetProperty(
            clockRef, kCAClockProperty_SMPTEFormat, size, &tSMPTEType);
        if (err != noErr) {
            NSLog(@"set smptetype Err = %d", err);
            goto catchErr;
        }
        
        //CAClockからの通知を受信する
        err = CAClockAddListener(clockRef, ClockListener, self);
        if (err != noErr) {
            NSLog(@"caclock addListener err = %d", err);
            goto catchErr;
        }
        
        //タイマーを開始する
        timer = 
            [NSTimer scheduledTimerWithTimeInterval:0.01 
                target:self selector:@selector(checkTime:) 
                userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer 
            forMode:NSEventTrackingRunLoopMode];
        
        return;
    	
    catchErr:
    	
        [NSApp terminate:self];
        return;
    }
    
    //現在のタイムを表示する
    - (void)checkTime:(NSTimer *)timr
    {
        OSStatus err;
        CAClockTime secondTime;
        
        //再生中か停止中かで取得するタイムを変える
        if (isRunning) {
            //カレントタイムを取得する
            err = CAClockGetCurrentTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        } else {
            //スタートタイムを取得する
            err = CAClockGetStartTime(
                clockRef, kCAClockTimeFormat_Seconds, &secondTime);
            if (err != noErr) {
                NSLog(@"CAClock GetCurrenttime err = %d", err);
                return;
            }
        }
        
        CAClockSeconds seconds = secondTime.time.seconds;
        
        //秒数からタイムコードに変換する
        SMPTETime tSMPTETime;
        err = CAClockSecondsToSMPTETime(clockRef, seconds, 80, &tSMPTETime);
        if (err != noErr) {
            NSLog(@"secondsToSMPTE err = %d", err);
            return;
        }
        SInt16 tHours = tSMPTETime.mHours;
        SInt16 tMinutes = tSMPTETime.mMinutes;
        SInt16 tSeconds = tSMPTETime.mSeconds;
        SInt16 tFrames = tSMPTETime.mFrames;
        
        //タイムを表示する
        NSString *tSMPTEString = 
        [NSString stringWithFormat:
            @"seconds = %f / SMPTE = %2.2hi.%2.2hi.%2.2hi.%2.2hi", 
            seconds, tHours, tMinutes, tSeconds, tFrames];
        
        [textField setStringValue:tSMPTEString];
    }
    
    //クロックをスタートさせる
    - (IBAction)start:(id)sender
    {
        OSStatus err = CAClockStart(clockRef);
        if (err != noErr) NSLog(@"CAClock start err");
    }
    
    //クロックをストップさせる
    - (IBAction)stop:(id)sender
    {
        OSStatus err = CAClockStop(clockRef);
        if (err != noErr) NSLog(@"CAClock stop err");
    }
    
    //クロックを0に戻す
    - (IBAction)reset:(id)sender
    {
        BOOL isKeepRunning = NO;
        
        //再生中ならストップする
        if (isRunning) {
            isKeepRunning = YES;
            [self stop:nil];
        }
        
        //タイムを設定する
        CAClockTime secondTime;
        secondTime.format = k
    CAClockTimeFormat_Seconds;
        secondTime.time.seconds = 0.0;
        OSStatus err = CAClockSetCurrentTime(clockRef, &secondTime);
        if (err != noErr) NSLog(@"CAClock setCurrentTime err = %d", err);
        
        //再生中であったなら再び再生する
        if (isKeepRunning) {
            [self start:nil];
        }
    }
    
    //解放
    - (void) dealloc
    {
        [timer invalidate];
    
        OSStatus err = CAClockDispose(clockRef);
        if (err != noErr) NSLog(@"CAClockDispose err = %d", err);
        
        [super dealloc];
    }
    
    @end
    

    Cocoaアプリケーションを新規作成して、AudioToolbox.Frameworkを追加し、このサンプルコードのCAClockTestクラスも作成してください。それからInterfaceBuilderでCAClockTestをインスタンス化して、ボタンを3つ作ってIBActionの「start」「stop」「reset」につなげ、テキストフィールドを作ってIBOutletの「textField」からつなげます。

    実行すると、秒数とタイムコードをテキストフィールドに表示するストップウォッチとして動作するはずです。startで再生、stopで停止、resetで0に戻ります。

    コードを見ていきますと、awakeFromNib内でCAClockの作成や設定を行っています。CAClockNew関数でCAClockRefを作成して、CAClockSetProperty関数でパラメータを設定しています。設定している値はみんなデフォルトですが一応サンプルってことであえて設定してみています。freeTimerメソッドではCAClockDispose関数でCAClockRefを解放しています。

    awakeFromNib内の設定でのSyncModeというのは、インターナル(マック内部のHostTimeやオーディオデバイス)のタイミングに同期して動かすか、MIDI Time CodeかMIDI beat clockのに同期して動かすかを選べるようです。InternalTimebaseは、SyncModeをインターナルにした場合のクロックのソースを、HostTimeかAudio Output UnitかAudioDeviceかを選べます(それぞれ試してみましたが、AudioDeviceはなぜかエラーが出て使えませんでした)。SMPTEFormatは、秒数単位のタイムをタイムコードに変換する際のフォーマットを選びます。awakeFromNib内最後のCAClockAddListenerは、CAClockの状態が変更された時にその情報を受け取る関数を設定します。今回はClockListenerという関数を設定しています。ちなみに、Core Audioのコールバックではめずらしくメインスレッドで呼ばれています。

    メソッドを移りまして、checkTimeがCAClockから現在のタイムを取得してテキストフィールドに表示するメソッドで、start:とstop:とreset:がCAClockを操作するメソッドになりますが、使われる関数名そのまんまに意味を受け取ると、CAClockはちょっとハマってしまいます。

    では、ひとつひとつ関数を見て行きます。

    CAClockSetCurrentTime関数でタイムを設定すると、「カレントタイム」というよりは、次回スタートさせる時の開始時間である「スタートタイム」が設定されます。reset:メソッドの中で、再生中のときは一旦停止させている事から分かると思いますが、この関数で設定できるのは停止中だけです。

    CAClockStart関数は、その「スタートタイム」から再生が開始されます。再生中に再びCAClockStart関数を呼ぶとまた「スタートタイム」から開始されます。

    CAClockGetCurrentTime関数は、最後にCAClockStart関数が呼びだされた時の「スタートタイム」から現在までの時間『カレントタイム」を返します。

    CAClockStop関数は、現在のタイムを「スタートタイム」に設定して、CAClockを停止状態にします。ただ、CAClockStop関数を呼び出して停止状態にした後にCAClockGetCurrentTime関数を呼び出すと、停止されていないかのように時間が進み続けた状態の値が取得されますので、checkTimeメソッドでは停止中の場合はCAClockGetStartTimeで「スタートタイム」を取得するようにしています。

    まとめると、CAClockの中には次回のスタート時間である「スタートタイム」と、「再生中か」と、最後にスタートさせたときの「スタート時のクロックソースのタイム」だけが保持されていて、「カレントタイム」は取得の都度「スタート時のクロックソースのタイム」と「スタートタイム」を基準に、現在のクロックソースのタイムとの差から算出されるといった感じになります。

    あと今回のように再生や停止などを自分で操作している場合は意味が無かったかもしれませんが、ClockListener関数内でCAClockの「スタート」と「ストップ」と「スタートタイム変更」を受け取って実際に処理を行うようにしてあります。

    オーディオファイル その2 AudioFile

    今回は、Audio File APIを使ったオーディオファイルの読み書きの方法を見て行きたいと思います。前回のExtendedAudioFileはフォーマットを指定すれば勝手に変換して読み書きしてくれましたが、Audio Fileではほぼそのまんまデータが読み込まれますので、変換が必要であれば自前で実装するか、AudioToolboxにあるAudio Converterを使うということになります。

    オーディオファイルの読み込み

    オーディオファイルの読み込みをする時は、

    1 オーディオファイルを開く
    2 オーディオファイルのフォーマットを取得する
    3 読み込むバッファを確保する
    4 バッファへデータを読み込む(必要な分だけ繰り返す)
    5 終わったらオーディオファイルを閉じる

    というのが基本的な流れになります。

    オーディオをファイルを開く関数は以下のものがあります。

    //CFURLでオーディオファイルを開く
    extern OSStatus	
    AudioFileOpenURL (CFURLRef inFileRef, 
                      SInt8 inPermissions, 
                      AudioFileTypeID inFileTypeHint,
                      AudioFileID *outAudioFile)
    
    //FSRefでオーディオファイルを開く
    extern OSStatus	
    AudioFileOpen (const struct FSRef *inFileRef, 
                    SInt8 inPermissions, 
                    AudioFileTypeID inFileTypeHint,
                    AudioFileID *outAudioFile)
    

    inFileRefには開きたいオーディオのファイルパスを渡します。inFileTypeHintには開こうとするファイルの種類(AIFFなど)が分かっていれば指定、何を読み込むか分かんなければ0を入れてもいいですし、間違っていても問題ありません。ヒントっていうくらいですから合ってれば効率的に開けるって位のものだと思います。outAudioFileは開いたオーディオファイルのIDが返ってきますので、開いたファイルに対して何か行う時にはこのAudioFileIDを使います。inPermissionsにはファイルを開く上でのアクセス権を設定します。以下のような定数が定義されています。

    enum {
      fsCurPerm                     = 0x00, /* open access permissions in ioPermssn */
      fsRdPerm                      = 0x01,
      fsWrPerm                      = 0x02,
      fsRdWrPerm                    = 0x03,
      fsRdWrShPerm                  = 0x04
    };
    

    読み込みで開くならfsRdPermというところでしょうか。

    オーディオファイルのプロパティ(情報)の取得・設定を行うのは、Core Audioでおなじみのパターンの以下の関数です。

    //オーディオファイルの情報を取得する
    extern OSStatus
    AudioFileGetProperty(AudioFileID inAudioFile,
                         AudioFilePropertyID inPropertyID,
                         UInt32 *ioDataSize,
                         void *outPropertyData)
    
    //オーディオファイルの情報を設定する
    extern OSStatus
    AudioFileSetProperty(AudioFileID inAudioFile,
                         AudioFilePropertyID inPropertyID,
                         UInt32 inDataSize,
                         const void *inPropertyData)
    

    inAudioFileにAudioFileIDを、プロパティの種類をinPropertyIDに、ioDataSizeには値を入れるプロパティのサイズを、outPropertyDataにはプロパティ取得・設定先を渡します。

    プロパティは以下のようなものがあります。

    enum
    {
        kAudioFilePropertyFileFormat      = 'ffmt', //ファイルタイプ
        kAudioFilePropertyDataFormat            = 'dfmt', //フォーマット
        kAudioFilePropertyIsOptimized           = 'optm',
        kAudioFilePropertyMagicCookieData       = 'mgic',
        kAudioFilePropertyAudioDataByteCount    = 'bcnt', //バイト単位での長さ
        kAudioFilePropertyAudioDataPacketCount  = 'pcnt', //パケット単位での長さ
        kAudioFilePropertyMaximumPacketSize     = 'psze',
        kAudioFilePropertyDataOffset            = 'doff',
        kAudioFilePropertyChannelLayout         = 'cmap',
        kAudioFilePropertyDeferSizeUpdates      = 'dszu',
        kAudioFilePropertyDataFormatName        = 'fnme',
        kAudioFilePropertyMarkerList            = 'mkls',
        kAudioFilePropertyRegionList            = 'rgls',
        kAudioFilePropertyPacketToFrame         = 'pkfr',
        kAudioFilePropertyFrameToPacket         = 'frpk',
        kAudioFilePropertyChunkIDs              = 'chid',
        kAudioFilePropertyInfoDictionary        = 'info',
        kAudioFilePropertyPacketTableInfo       = 'pnfo',
        kAudioFilePropertyFormatList            = 'flst',
        kAudioFilePropertyPacketSizeUpperBound  = 'pkub',
        kAudioFilePropertyReserveDuration       = 'rsrv',
        kAudioFilePropertyEstimatedDuration     = 'edur',
        kAudioFilePropertyBitRate               = 'brat'
    };
    

    フォーマットとかファイルの長さの取得・設定あたりが主な使い道だと思います。ちょっと名前のつけられ方がややこしいですが、FileFormatがAIFFなどのファイルタイプで、DataFormatがAudioStreamBasicDescriptionで表されるフォーマットになります。

    オーディオファイルからのデータの読み込みを行うのは、以下の関数です。

    //バイト単位でデータを読み込む
    extern OSStatus	
    AudioFileReadBytes (AudioFileID inAudioFile,
                        Boolean     inUseCache,
                        SInt64      inStartingByte, 
                        UInt32      *ioNumBytes, 
                        void        *outBuffer)
    
    //パケット単位でデータを読み込む
    extern OSStatus
    AudioFileReadPackets (AudioFileID                  inAudioFile, 
                          Boolean                      inUseCache,
                          UInt32                       *outNumBytes,
                          AudioStreamPacketDescription *outPacketDescriptions,
                          SInt64                       inStartingPacket, 
                          UInt32                       *ioNumPackets, 
                          void                         *outBuffer)
    

    AudioFileReadBytesがバイトデータでの読み込みで、AudioFileReadPacketsがパケット単位での読み込みになります。ExtendedAudioFileでは読み込んだ分だけ読み込み位置が進んでくれましたが、こちらは毎度位置を自分で進めてinStartingByteに指定します。また、オーディオデータの読み込み先もAudioBufferListではなく普通のメモリ領域になります。

    なお、読み込むオーディオデータのバイトオーダーは、オーディオファイルのフォーマットの種類に関わらず、ネイティブなエンディアンに変換された状態で読み込まれるようです。

    MP3とかAACとかの圧縮フォーマットを読み込んだり、サンプリング周波数を変換して読み込む場合は、パケット単位で読み込んでデコーダに渡したりする感じになりますが、Tiger以降ではExtendedAudioFileがあるので、特殊な事をするのでなければ、ExtendedAudioFileを使った方が良いと思います。

    ちなみに、ExtendedAudioFileの読み込み速度と、バイト単位での読み込み速度が10倍くらい差があると前回書いていましたが、改めて調べたらそんなには差がありませんでした。自分の計りそこないだったかも知れませんし、パケット単位での読み込みが改善されたのかもしれません。

    読み込みや書き込みが終わったオーディオファイルを閉じるのは、AudioFileClose関数です。

    extern OSStatus
    AudioFileClose	(AudioFileID inAudioFile)
    

    AudioFileIDを渡してファイルを閉じます。少なくとも書き込み時にはちゃんと閉じておかないとファイルが読めません。読み込みの時でも、開きっぱなしにしておけるファイルの数に上限があるようなので、必要なければ閉じておくようにしておいた方が良いと思います。

    オーディオファイルの書き込み

    オーディオファイルを書き込むには、

    1 オーディオファイルのフォーマットを作成する
    2 フォーマットやファイルパスを指定してオーディオファイルを作成(または上書き)する
    3 書き込むデータを用意する
    4 データをオーディオファイルに渡して書き込む(必要な分だけ繰り返す)
    5 書き込みが終わったらファイルを閉じる 

    といった流れになります。

    オーディオファイルを作成・上書きしてAudioFileIDを取得するには以下の関数を使います。

    //URLでファイルパスを指定してオーディオファイルを作成・上書きする
    extern OSStatus	
    AudioFileCreateWithURL (CFURLRef                          inFileRef,
                            AudioFileTypeID                   inFileType,
                            const AudioStreamBasicDescription *inFormat,
                            UInt32                            inFlags,
                            AudioFileID                       *outAudioFile)
    
    //FSRefでファイルパスを指定してオーディオファイルを作成する
    extern OSStatus	
    AudioFileCreate (const struct FSRef                *inParentRef, 
                     CFStringRef                       inFileName,
                     AudioFileTypeID                   inFileType,
                     const AudioStreamBasicDescription *inFormat,
                     UInt32                            inFlags,
                     struct FSRef                      *outNewFileRef,
                     AudioFileID                       *outAudioFile)
    
    //FSRefでファイルパスを指定してオーディオファイルを上書きする
    extern OSStatus	
    AudioFileInitialize (const struct FSRef                *inFileRef,
                         AudioFileTypeID                   inFileType,
                         const AudioStreamBasicDescription *inFormat,
                         UInt32                            inFlags,
                         AudioFileID                       *outAudioFile)
    

    FSRefでファイルパスを指定する方は、新規作成と上書きでAudioFileCreateとAudioFileInitializeという二つの関数に分かれていますが、URLで指定する方は新規作成も上書きもAudioFileCreateWithURL関数ひとつでまかなえます。

    当然ですが、読み込みと違ってinFileTypeにはちゃんとファイルタイプを指定します。また、それに合わせたフォーマットをinFormatに渡さないとエラーが返ってきてファイルを作成する事が出来ません。

    inFlagsに渡すフラグは以下のようなものが用意されています。

    enum {
    	kAudioFileFlags_EraseFile = 1,
    	kAudioFileFlags_DontPageAlignAudioData = 2
    };
    

    0を指定すれば、既にファイルが存在していた場合はエラーが返って上書きされません。EraseFileなら上書きされます。DontPageAlignAudioDataは、上書き可能に加えて、データに余計なスペースをつけないって感じだと思いますが、何がどう差が出るかは良くわかりません。

    オーディオファイルへ実際に書き込みを行うには以下の関数を使います。

    //バイト単位で書き込む
    extern OSStatus	
    AudioFileWriteBytes (AudioFileID inAudioFile,  
                         Boolean     inUseCache,
                         SInt64      inStartingByte, 
                         UInt32      *ioNumBytes, 
                         const void  *inBuffer)
    
    //パケット単位で書き込む
    extern OSStatus	
    AudioFileWritePackets (AudioFileID                        inAudioFile,  
                           Boolean                            inUseCache,
                           UInt32                             inNumBytes,
                           const AudioStreamPacketDescription *inPacketDescriptions,
                           SInt64                             inStartingPacket, 
                           UInt32                             *ioNumPackets, 
                           const void                         *inBuffer)
    

    読み込みと同じような感じで、バイト単位とパケット単位の二つの書き込み方法があります。inStartingByteで毎度位置を指定しなくてはいけなかったり、ioNumBytesに実際に書き込まれた分だけの値が返ってくるのも同じです。

    ただ、読み込み時のデータのバイトオーダーは勝手にネイティブに変換されていましたが、書き込み時にはちゃんとフォーマットに合わせたエンディアンに変換しておかなければいけません。

    また、inStartingByteで書き込み位置を、まだ何も書き込んでいない位置に飛ばして指定すると、その間は無音が勝手に入ります。

    書き込み用の関数はこれくらいで、プロパティの設定・取得や、オーディオファイルを閉じるのは読み込みと共通です。

    サンプルコード(オーディオファイルのコピー)

    オーディオファイルの読み込みと書き込みを行うサンプルとして、オーディオファイルのコピーを行ってみたいと思います。ファイルタイプやフォーマットはリニアPCMに限定して、全く変えずコピーするようにしています。

    FoundationToolを新規作成して、AudioToolbox.Frameworkをプロジェクトに追加し、以下のコードのようにmain関数を記述します。inPathは適当に書いてあるだけなので、何かコピー元となるリニアPCMのオーディオファイルを指定してみてください。

    (※2008/6/26変更 NSURLを取得するのにURLWithString:を使っていましたが、それだとスペースの含まれているファイルパスが開けないので、fileURLWithPath:に変更しました。)

    #import <Foundation/Foundation.h>
    #import <AudioToolbox/AudioToolbox.h>
    
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
        //コピー元になるファイルを指定する
        NSString *inPath = @"/ConvertFile/sample.aiff";
        
        //一度にコピーするフレーム数
        UInt32 copyFrames = 1024;
        
        //その他の変数の宣言
        OSStatus err = noErr;
        UInt32 size;
        AudioFileID inID;
        AudioFileID outID;
        AudioFileTypeID fileType;
        AudioStreamBasicDescription format;
        Byte *ioBuffer = NULL;
        
        //読み込み側のオーディオファイルを開く(2008/6/26修正)
        //NSURL *inUrl = [NSURL URLWithString:inPath];
        NSURL *inUrl = [NSURL fileURLWithPath:inPath];
        err = AudioFileOpenURL((CFURLRef)inUrl, fsRdPerm, 0, &inID);
        if (err != noErr) goto catchErr;
        
        //オーディオファイルのフォーマットを取得する
        size = sizeof(format);
        err = AudioFileGetProperty(
            inID, kAudioFilePropertyDataFormat, &size, &format);
        if (err != noErr) goto catchErr;
        
        //リニアPCMでなければ終了する
        if (format.mFormatID != kAudioFormatLinearPCM) {
            NSLog(@"Not LinearPCM");
            goto catchErr;
        }
        
        //オーディオファイルのファイルタイプを取得する
        size = sizeof(fileType);
        err = AudioFileGetProperty(
            inID, kAudioFilePropertyFileFormat, &size, &fileType);
        if (err != noErr) goto catchErr;
        
        //ファイルタイプによってエンディアンを設定する
        switch (fileType) {
            case kAudioFileAIFFType:
                format.mFormatFlags |= kAudioFormatFlagIsBigEndian;
                break;
            case kAudioFileAIFCType:
                format.mFormatFlags |= kAudioFormatFlagIsBigEndian;
                break;
            case kAudioFileWAVEType:
                format.mFormatFlags &= ~kAudioFormatFlagIsBigEndian;
                break;
            default:
                NSLog(@"This file is not supported");
                goto catchErr;
                break;
        }
    
        NSLog(@"FileType = %@", NSFileTypeForHFSTypeCode(fileType));
        NSLog(@"Samplerate = %f", format.mSampleRate);
        NSLog(@"FormatID = %@", 
            NSFileTypeForHFSTypeCode(format.mFormatID));
        NSLog(@"FormatFlags = %4.4x", format.mFormatFlags);
        NSLog(@"BitsPerChannels = %u", format.mBitsPerChannel);
        NSLog(@"ChannelsPerFrame = %u", format.mChannelsPerFrame);
        NSLog(@"FramesPerPacket = %u", format.mFramesPerPacket);
        NSLog(@"BytesPerFrame = %u", format.mBytesPerFrame);
        NSLog(@"BytesPerPacket = %u", format.mBytesPerPacket);
        
        //書き出し側のオーディオファイルのパスを作成する(2008/6/26修正)
        NSString *extension = [inPath pathExtension];
        NSString *outPathWithoutExtension = 
            [[inPath stringByDeletingPathExtension] 
                stringByAppendingString:@"-export"];
        NSString *outPath = 
            [outPathWithoutExtension stringByAppendingPathExtension:extension];
        //NSURL *outUrl = [NSURL URLWithString:outPath];
        NSURL *outUrl = [NSURL fileURLWithPath:outPath];
        
        //書き出し側のオーディオファイルを作成する。上書きしない
        err = AudioFileCreateWithURL(
            (CFURLRef)outUrl, fileType, &format, 0, &outID);
        if (err != noErr) goto catchErr;
        
        //データをコピーする
        SInt64 startingByte = 0;
        UInt32 ioBufferBytes = copyFrames * format.mBytesPerFrame;
        ioBuffer = calloc(1, ioBufferBytes);
        BOOL isEnd = NO;
        
        //バイトスワップが必要か判断する
        NSUInteger swapBytes = 
            format.mBytesPerFrame / format.mChannelsPerFrame;
        BOOL isSwap = 
            ((format.mFormatFlags & kAudioFormatFlagIsBigEndian) != 
            kAudioFormatFlagsNativeEndian) && (swapBytes > 1);
        
        //オーディオデータをコピーする
        while (!isEnd) {
            
            //読み込むバイト数を設定する
            UInt32 readBytes = ioBufferBytes;
            
            //オーディオファイルからメモリに読み込む
            err = AudioFileReadBytes(
                inID, false, startingByte, &readBytes, ioBuffer);
            
            //errにeofErrが返ればファイルの最後なので終了
            if (err == eofErr) {
                isEnd = YES;
            } else if (err != noErr) {
                NSLog(@"readbytes err");
                goto catchErr;
            }
            
            //オーディオファイルのフォーマットがネイティブ
            //エンディアンと違う場合はバイトスワップする
            if (isSwap) {
                NSUInteger i, j;
                for (i = 0; i < (readBytes / swapBytes); i++) {
                    for (j = 0; j < swapBytes / 2; j++) {
                        Byte *ptr = &(ioBuffer[i * swapBytes]);
                        Byte temp = ptr[j];
                        ptr[j] = ptr[swapBytes - j - 1];
                        ptr[swapBytes - j - 1] = temp;
                    }
                }
            }
            
            //オーディオファイルに書き込む
            err = AudioFileWriteBytes(
                outID, false, startingByte, &re
    adBytes, ioBuffer);
            if (err != noErr) {
                NSLog(@"writebytes err");
                goto catchErr;
            }
            
            //読み書きのスタート位置を進める
            startingByte += readBytes;
        }
        
        NSLog(@"complete");
        
    catchErr:
        
        if (err != noErr) NSLog(@"err = %d", err);
        
        //バッファを解放する
        if (ioBuffer != NULL) free(ioBuffer);
        
        //オーディオファイルを閉じる
        AudioFileClose(outID);
        AudioFileClose(inID);
        
        [pool drain];
        return 0;
    }
    

    プログラムを実行すると、微妙なファイル容量のずれはありますが、中身的には全く同じオーディオデータのコピーが出来るはずです。ファイルタイプによって読み込みと書き込みでエンディアンが変わってしまうので、スワップするようにしています。まあ、でもやっぱりめんどくさいので、普通に書き込むだけだったらExtendedAudioFile使った方が良いのではないでしょうか。

    オーディオファイル その1 ExtendedAudioFile

    Core AudioのAudioToolbox.Frameworkにはオーディオファイルの読み書きに2通り方法が用意されています。<AudioToolbox/AudioFile.h>での、ほぼ生のデータを直接扱う方法と、<AudioToolbox/ExtendedAudioFile.h>での、オーディオファイルの読み書きにコンバーターを組み合わせてあるものを使う方法です。

    圧縮ファイルを扱う場合や、リニアPCMでサンプリング周波数を変換して扱いたい場合には、ExtendedAudioFile.hを使ったほうが楽だと思います。逆にリニアPCMでフォーマットの変換が全く必要ないときには、AudioFile.hのバイトデータでの読み書きを使うとパフォーマンス的に有利かもしれません。

    今回は、ExtendedAudioFileの使い方を見ていきたいと思います。まず、読み込みを行うときにはオーディオファイルを開いてExtAudioFileRefというオブジェクトを取得します。それを行うのが以下の関数です。できるだけObjective-Cを使いたい自分としては、URLで開く方がおすすめです。

    //FSRefでパスを指定して開く
    extern OSStatus
    ExtAudioFileOpen(const FSRef *inFSRef, ExtAudioFileRef *outExtAudioFile)
    
    //CFURLRefでパスを指定して開く
    extern OSStatus
    ExtAudioFileOpenURL(CFURLRef inURL, ExtAudioFileRef *outExtAudioFile)
    

    取得したExtAudioFileRefを使ってオーディオデータ読み込みを行うのが以下のExtAudioFileRead関数です。

    extern OSStatus
    ExtAudioFileRead(ExtAudioFileRef inExtAudioFile, 
                     UInt32 *ioNumberFrames,
                     AudioBufferList *ioData)
    

    読み込みたいフレーム数とAudioBufferListを渡すと読み込まれます。ファイルの最後の方の読み込み時などで、渡したフレーム数より読み込まれたデータが少なければ、ioNumberFramesとioData内のmDataByteSizeが書き換えられます。

    また、読み込み位置は読み込んだ分だけ勝手に進んでくれるので、頭からシーケンシャルに読み込む場合はただ繰り返し関数を呼ぶだけで大丈夫です。ファイルの任意の位置から読み込み始めたいときは、ExtAudioFileSeek関数を使って、読み込み位置を移動させます。

    extern OSStatus
    ExtAudioFileSeek(ExtAudioFileRef inExtAudioFile,
                     SInt64 inFrameOffset)
    

    inFrameOffsetにはオーディオファイル側のフレーム数を指定します。サンプリング周波数を変換して読み込んでいると変換後のレートで指定してしまいがちなので注意が必要です。

    オーディオファイルの書き込みを行うには以下の関数でオーディオファイルを作成し、ExtAudioFileRefを取得します。

    //FSRefとファイルネームでオーディオファイルを作成する
    extern OSStatus
    ExtAudioFileCreateNew(const FSRef *inParentDir,
                          CFStringRef inFileName, 
                          AudioFileTypeID inFileType,
                          const AudioStreamBasicDescription *inStreamDesc,
                          const AudioChannelLayout *inChannelLayout,
                          ExtAudioFileRef *outExtAudioFile)
    
    //CFURLRefでオーディオファイルを作成する
    extern OSStatus
    ExtAudioFileCreateWithURL(CFURLRef inURL,
                              AudioFileTypeID inFileType,
                              const AudioStreamBasicDescription *inStreamDesc,
                              const AudioChannelLayout *inChannelLayout,
                              UInt32 inFlags,
                              ExtAudioFileRef *outExtAudioFile)
    

    inFileTypeIDにはWAVやAIFFなどのオーディオフォーマットの種類を、inStreamDescにはオーディオファイルのフォーマットを
    渡します。ChannelLayoutは必要なければNULLでかまいません。URLの方にあるinFlagsでは上書きするかなどの設定が出来ます。

    オーディオファイルに書き込みを行うのは以下の関数です。

    extern OSStatus
    ExtAudioFileWrite(ExtAudioFileRef inExtAudioFile,
                      UInt32 inNumberFrames,
                      const AudioBufferList *ioData)
    
    extern OSStatus
    ExtAudioFileWriteAsync(ExtAudioFileRef inExtAudioFile,
                           UInt32 inNumberFrames,
                           const AudioBufferList *ioData)
    

    上の方のExtAudioFileWriteは普通にこの関数が呼ばれたタイミングで書き込まれますが、下のExtAudioFileWriteAsyncはバッファにデータがためこまれて非同期に書き込みが行われます。バッチ処理など立て続けに書き込みを行うときはWriteで、オーディオデバイスからの録音などコールバックを邪魔したくないときなどはWriteAsyncという使い分けになると思います。

    オーディオファイルの読み書きが必要なくなったときにExtAudioFileRefを解放してファイルを閉じるのが以下のExtAudioFileDispose関数です。ExtAudioFileRefはそんなにたくさん同時に作っておけないようなので、使うものだけを残しておいて、いらないものはこまめに解放するようにしたほうが良いです。

    extern OSStatus
    ExtAudioFileDispose(ExtAudioFileRef inExtAudioFile)
    

    ExtendedAudioFileでオーディオファイルの情報の取得や設定をするときは以下の関数で行います。Core Audioのときのように〜SetPropertyで設定、〜GetPropertyで取得というおなじみのパターンです。

    extern OSStatus
    ExtAudioFileGetProperty(ExtAudioFileRef	inExtAudioFile,
                            ExtAudioFilePropertyID inPropertyID,
                            UInt32 *ioPropertyDataSize,
                            void *outPropertyData)
    
    extern OSStatus
    ExtAudioFileSetProperty(ExtAudioFileRef	inExtAudioFile,
                            ExtAudioFilePropertyID inPropertyID,
                            UInt32 inPropertyDataSize,
                            const void *inPropertyData)
    

    設定・取得出来るプロパティは以下のものが定義されています。

    enum { // ExtAudioFilePropertyID
        kExtAudioFileProperty_FileDataFormat        = 'ffmt',
        kExtAudioFileProperty_FileChannelLayout     = 'fclo',
        kExtAudioFileProperty_ClientDataFormat      = 'cfmt',
        kExtAudioFileProperty_ClientChannelLayout   = 'cclo',
    	
        // read-only:
        kExtAudioFileProperty_AudioConverter        = 'acnv',
        kExtAudioFileProperty_AudioFile             = 'afil',
        kExtAudioFileProperty_FileMaxPacketSize     = 'fmps',
        kExtAudioFileProperty_ClientMaxPacketSize   = 'cmps',
        kExtAudioFileProperty_FileLengthFrames      = '#frm',
    
        // writable:
        kExtAudioFileProperty_ConverterConfig       = 'accf',
        kExtAudioFileProperty_IOBufferSizeBytes     = 'iobs',
        kExtAudioFileProperty_IOBuffer              = 'iobf' 
    };
    typedef UInt32 ExtAudioFilePropertyID;
    

    よく使いそうなものを見ていくと、FileDataFormatはオーディオファイル自体のフォーマットで、ClientDataFormatはコンバーターで変換された後のフォーマット、FileLengthFramesがオーディオファイル自体のフレーム数です。

    ちなみに、読み込み時にFileDataFormatを取得しても、エンディアンはファイルのものではなく、すでにマック側のネイティブなエンディアンのようです。インテルマックでしか検証していないのですが、ビッグエンディアンにしたAIFFとかCAFとかのフォーマットを取得してもビッグエンディアンのフラグは立っていませんでした。

    読み込み時の全体の流れとしては、オーディオファイルを開く、オーディオファイルのフォーマットを取得する、クライアントフォーマットを設定する、オーディオデータを読み込む、閉じる、といった感じです。

    書き込みの場合は、オーディオファイルをフォーマットを指定して新規作成する、クライアントフォーマットを設定する、オーディオデータを書き込む、閉じる、という風になります。

    といったところで、オーディオファイルの読み書きのサンプルとして、フォーマットを変換してファイルのコピーを行ってみたいと思います。FoundationToolを作成して、<CoreAudio/AudioToolbox.h>をインポートし、main関数を以下のように記述します。変換元のファイルパスは、適当に入れてあるので必要に応じて変更してください。WAVでもAIFFでもmp3でもAACでも、マックが標準でデコードできるコーデックなら何でも読み込めるはずです。実行して”complete”とログに表示されたら、変換元のファイルと同じフォルダにWAVの16bitの22.050kHzのオーディオファイルが出来ていると思います。

    (※2008/6/26変更 NSURLを取得するのにURLWithString:を使っていましたが、それだとスペースの含まれているファイルパスが開けないので、fileURLWithPath:に変更しました。)

    #import <Foundation/Foundation.h>
    #import <AudioToolbox/AudioToolbox.h>
    
    
    int main (int argc, const char * argv[])
    {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
        //変換するファイル
        NSString *inPath = @"/ConvertFile/sample.aiff";
    
        //一度に変換するフレーム数
        UInt32 convertFrames = 1024;
    	
        //変数の宣言
        OSStatus err = noErr;
        UInt32 size;
        ExtAudioFileRef inAudioFileRef = NULL;
        ExtAudioFileRef outAudioFileRef = NULL;
        AudioStreamBasicDescription inFileFormat, ioClientFormat, outFileFormat;
        void *ioData = NULL;
    	
        //読み込み側のオーディオファイルを開く(2008/6/26修正)
        //NSURL *inUrl = [NSURL URLWithString:inPath];
        NSURL *inUrl = [NSURL fileURLWithPath:inPath];
        err = ExtAudioFileOpenURL((CFURLRef)inUrl, &inAudioFileRef);
        if (err != noErr) goto catchErr;
    	
        //読み込み側のオーディオファイルからフォーマットを取得する
        //size = sizeof(ioClientFormat);(2009/12/4修正)
        size = sizeof(inFileFormat);
        err = ExtAudioFileGetProperty(
            inAudioFileRef, kExtAudioFileProperty_FileDataFormat,
             &size, &inFileFormat);
        if (err != noErr) goto catchErr;
    	
        //書き出し側のオーディオファイルのパスを作成する(2008/6/26修正)
        NSString *outPath = 
            [[inPath stringByDeletingPathExtension] 
                stringByAppendingString:@"-export.wav"];
        //NSURL *outUrl = [NSURL URLWithString:outPath];
        NSURL *outUrl = [NSURL fileURLWithPath:outPath];
    	
        //書き出し側のオーディオファイルのフォーマットを作成する
        outFileFormat.mSampleRate = 22050;
        outFileFormat.mFormatID = kAudioFormatLinearPCM;
        outFileFormat.mFormatFlags = 
            kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
        outFileFormat.mBitsPerChannel = 16;
        outFileFormat.mChannelsPerFrame = inFileFormat.mChannelsPerFrame;
        outFileFormat.mFramesPerPacket = 1;
        outFileFormat.mBytesPerFrame = 
            outFileFormat.mBitsPerChannel / 8 * outFileFormat.mChannelsPerFrame;
        outFileFormat.mBytesPerPacket = 
            outFileFormat.mBytesPerFrame * outFileFormat.mFramesPerPacket;
    	
        //書き出し側のオーディオファイルを作成する
        err = ExtAudioFileCreateWithURL(
            (CFURLRef)outUrl, kAudioFileWAVEType, &outFileFormat, 
            NULL, 0, &outAudioFileRef);
        if (err != noErr) goto catchErr;
    	
        //読み書き両方のクライアントフォーマットを設定する
        ioClientFormat.mSampleRate = inFileFormat.mSampleRate;
        ioClientFormat.mFormatID = kAudioFormatLinearPCM;
        ioClientFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
        ioClientFormat.mBitsPerChannel = 32;
        ioClientFormat.mChannelsPerFrame = inFileFormat.mChannelsPerFrame;
        ioClientFormat.mFramesPerPacket = 1;
        ioClientFormat.mBytesPerFrame = 
            ioClientFormat.mBitsPerChannel / 8 * ioClientFormat.mChannelsPerFrame;
        ioClientFormat.mBytesPerPacket = 
            ioClientFormat.mBytesPerFrame * ioClientFormat.mFramesPerPacket;
    	
        size = sizeof(ioClientFormat);
        err = ExtAudioFileSetProperty(
            outAudioFileRef, kExtAudioFileProperty_ClientDataFormat, 
            size, &ioClientFormat);
        if (err != noErr) goto catchErr;
    	
        size = sizeof(ioClientFormat);
        err = ExtAudioFileSetProperty(
            inAudioFileRef, kExtAudioFileProperty_ClientDataFormat, 
            size, &ioClientFormat);
        if (err != noErr) goto catchErr;
    	
        //オーディオデータの読み書きに使用するメモリ領域を確保する
        UInt32 allocByteSize = convertFrames * ioClientFormat.mBytesPerFrame;
        ioData = malloc(allocByteSize);
        if (!ioData) {
            err = 1002;
            goto catchErr;
        }
    	
        //オーディオデータの読み書きに使用するAudioBufferListを作成する
        AudioBufferList ioList;
        ioList.mNumberBuffers = 1;
        ioList.mBuffers[0].mNumberChannels = ioClientFormat.mChannelsPerFrame;
        ioList.mBuffers[0].mDataByteSize = allocByteSize;
        ioList.mBuffers[0].mData = ioData;
    	
        //オーディオデータをコピーする
        while (1) {
            //フレーム数とデータサイズを設定する
            UInt32 frames = convertFrames;
            ioList.mBuffers[0].mDataByteSize = allocByteSize;
    		
            //読み込み側のオーディオファイルからオーディオデータを読み込む
            err = ExtAudioFileRead(inAudioFileRef, &frames, &ioList);
            if (err != noErr) goto catchErr;
    		
            //最後まで読み込んだら終了
            if (frames == 0) break;
    		
            //書き込み側のオーディオファイルへ書き込む
            err = ExtAudioFileWrite(outAudioFileRef, frames, &ioList);
            if (err != noErr) goto catchErr;
        }
    	
        NSLog(@"complete");
    	
    	
    catchErr:
    	
        if (err != noErr) NSLog(@"err = %ld", err);
    	
        //解放する
        if (ioData) free(ioData);
        if (inAudioFileRef) ExtAudioFileDispose(inAudioFileRef);
        if (outAudioFileRef) ExtAudioFileDispose(outAudioFileRef);
    	
        [pool drain];
        return 0;
    }
    

    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でのコールバック系の関数は基本的にメインではないスレッドで呼ばれるので、インスタンス変数へのアクセスなどするならマルチスレッドを意識してコードを記述していかなくてはいけないと思います。

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