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

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 MIDI その3 MIDIPacketListの送信

MIDIデータを送信する方法です。まず、MIDIPacketListの作り方を見てみます。

ひとつの普通のメッセージのMIDIPacketを含んだMIDIPacketListを作るだけであればMIDIPacketList構造体を作ってしまえばいいだけですが、複数のMIDIPacketを含んだMIDIPacketListを作りこんでいくには以下の関数を使います。

extern MIDIPacket *
MIDIPacketListInit(MIDIPacketList *pktlist)

extern MIDIPacket *
MIDIPacketListAdd(MIDIPacketList *  pktlist,
                  ByteCount         listSize,
                  MIDIPacket *      curPacket,
                  MIDITimeStamp     time,
                  ByteCount         nData,
                  const Byte *      data)

作り方の順番としては、

・MIDIPacketListとして作成するのに十分なメモリ領域を確保する
・MIDIPacketListInitでMIDIPacketListを初期化し、1つめのMIDIPacketのポインタを取得する
・MIDIPacketListAddでMIDIPacketを書き込む(必要な分だけMIDIPacketListAddを繰り返す)

という感じになります。この方法でMIDIPacketListを作るコードはこんな感じです。とりあえずMIDIPacketは1つだけですけど。

ByteCount bufferSize = 1024;
Byte packetListBuffer[bufferSize];
MIDIPacketList *packetListPtr = (MIDIPacketList *)packetListBuffer;
    
MIDITimeStamp time = AudioGetCurrentHostTime();
    
ByteCount dataSize = 3;
Byte data[dataSize];
data[0] = 0x90;
data[1] = 0x60;
data[2] = 0x10;
    
MIDIPacket *packet = MIDIPacketListInit(packetListPtr);
    
if (packet != NULL) {
    packet = MIDIPacketListAdd(
        packetListPtr, bufferSize, packet, time, dataSize, data);
}

実際に複数のMIDIPacketを追加する場合、MIDIPacketListAddでMIDIPacketの書き込みが成功すれば、次の書き込み位置となるMIDIPacketのポインタが返ってきますので、それをまたMIDIPacketListAddに渡してという風に繰り返してMIDIPacketを追加して行きます。ただ、いろいろ試した感じでは、MIDITimeStampを同じ時間に指定したMIDIPacketを追加しようとしても追加されないようです。他のソフトからは、同じ時間のMIDIPacketがいくつも入ったMIDIPacketListが来たりするんですが…。

MIDIPacketListが作成できたら、MIDISend関数で送信します。

extern OSStatus
MIDISend(MIDIPortRef            port, 
         MIDIEndpointRef        dest, 
         const MIDIPacketList *	pktlist )

ちなみに、MIDIReceivedという関数もあって、最初見たときに、受信なのにメッセージを送るとは何ぞや?と思ったのですが、これはソースとなっているMIDIエンドポイントにMIDIPacketListを送信して、アプリケーション内部で受信するためにというもののようです。

extern OSStatus 
MIDIReceived(MIDIEndpointRef        src, 
             const MIDIPacketList * pktlist )

これを使うと、受信したMIDIPacketListの中に先の時間のMIDIPacketがあったら、その時間に再び受信されるように再送信させられたり、あるいはアプリケーション内の音源をならすような場合、一度にたくさんのMIDIデータを時間を指定して送信しておけばあとは勝手にシーケンスしてくれる、という感じでしょうか。

Core MIDIを調べてると良く出てくるバーチャルソースってのが、そんなとき役立つのではないかと思います。アプリケーション内部にMIDIEndpointをバーチャルソースとして作るのがMIDISourceCreate関数です。

extern OSStatus
MIDISourceCreate(MIDIClientRef      client, 
                 CFStringRef        name, 
                 MIDIEndpointRef *  outSrc )

これでMIDIEndpointを作成してMIDIPortConnectSourceでMIDIPortとつなげたら、外部のソースと同じく受信する事が出来ます。

といったところで実際の送信の方法ですが、アプリケーション外部にMIDIデータを送信する順番としては、

・MIDIClientを作成する
・MIDIOutputPortCreateでMIDIPortを作成する
・出力するMIDIEndpointを取得する
・MIDIPacketListを作成する
・MIDISendで送信する

と、なります。

以下がMIDIデータ送信のサンプルになります。DestinationのMIDIEndpointを1つだけ取得して送信していますので、そこから何かソフトなりシンセなりで受けるようにすれば音が鳴ると思います。ちなみに今回のサンプルくらい各MIDIPacketの間隔が開いていれば、MIDIPacketListのデータが勝手にバラされてMIDIEndpointに送信されるようです。間隔が短かったり、MIDIReceivedの場合は、そのまんまMIDIPacketListに複数のMIDIPacketが入った状態で送信されます。

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

@interface MIDIOutTest : NSObject {
    
    MIDIClientRef clientRef;
    MIDIPortRef outPortRef;
    MIDIEndpointRef destPointRef;
}

- (void)setup;
- (void)sendNoteOn;

@end

@implementation MIDIOutTest

- (void)awakeFromNib
{
    [self setup];
    [self sendNoteOn];
}

- (void)setup
{
    OSStatus err;
    
    //MIDIクライアントを作成する
    NSString *clientName = @"inputClient";
    err = MIDIClientCreate((CFStringRef)clientName, NULL, NULL, &clientRef);
    if (err != noErr) {
        NSLog(@"MIDIClientCreate err = %d", err);
        return;
    }
    
    //MIDIアウトプットポートを作成する
    NSString *outputPortName = @"outputPort";
    err = MIDIOutputPortCreate(
        clientRef, (CFStringRef)outputPortName, &outPortRef);
    if (err != noErr) {
        NSLog(@"MIDIOutputPortCreate err = %d", err);
        return;
    }
    
    //送信先のMIDIエンドポイントを取得する
    destPointRef = MIDIGetDestination(0);
    
    //送信先のMIDIエンドポイントの名前を取得してログに表示
    CFStringRef strRef;
    err = MIDIObjectGetStringProperty(
        destPointRef, kMIDIPropertyDisplayName, &strRef);
    if (err != noErr) {
        NSLog(@"MIDIObjectGetStringProperty err = %d", err);
        return;
    }
    NSLog(@"connect = %@", strRef);
    CFRelease(strRef);
}

- (void)sendNoteOn
{
    OSStatus err;
    
    //MIDIPacketListを作成する
    ByteCount bufferSize = 1024;
    Byte packetListBuffer[bufferSize];
    MIDIPacketList *packetListPtr = (MIDIPacketList *)packetListBuffer;
    
    //現在のHostTimeを取得する
    MIDITimeStamp time = AudioGetCurrentHostTime();
    
    //ノートオンのMIDIデータを作成する
    Byte noteOnData[3];
    noteOnData[0] = 0x90;
    noteOnData[1] = 0x3C;
    noteOnData[2] = 100;
    
    //ノートオフのMIDIデータを作成する
    Byte noteOffData[3];
    noteOffData[0] = 0x80;
    noteOffData[1] = 0x3C;
    noteOffData[2] = 0x00;
    
    //MIDIPacketListの初期化をする
    MIDIPacket *packet = MIDIPacketListInit(packetListPtr);
    
    for (NSInteger i = 0; i < 12; i++) {
        
        //MIDIPacketListにノートオンのMIDIPacketを追加する
        if (packet != NULL) {
            packet = MIDIPacketListAdd(
                packetListPtr, bufferSize, packet, time, 3, noteOnData);
        } else {
            break;
        }
        
        time = AudioConvertHostTimeToNanos(time) + kSecondScale * 0.25;
        time = AudioConvertNanosToHostTime(time);
        
        //MIDIPacketListにノートオフのMIDIPacketを追加する
        if (packet != NULL) {
            packet = MIDIPacketListAdd(
                packetListPtr, bufferSize, packet, time, 3, noteOffData);
        } else {
            break;
        }
        
        time = AudioConvertHostTimeToNanos(time) + kSecondScale * 0.75;
        time = AudioConvertNanosToHostTime(time);
        
        noteOnData[1] += 1;
        noteOffData[1] += 1;
    }
    
    err = MIDISend(outPortRef, destPointRef, packetListPtr);
    if (err != noErr) {
        NSLog(@"MIDISend err = %d", err);
    }
}

/*
 - (void)dispose
 {
     OSStatus err;
 
     err = MIDIPortDispose(outPortRef);
     if (err != noErr) NSLog(@"MIDIPortDispose err = %d", err);
 
     err = MIDIClientDispose(clientRef);
     if (err != noErr) NSLog(@"MIDIClientDispose err = %d", err);
 }
 */

@end

NSInvocationの作成

NSInvocationOperationとか使っていて、NSInvocationの作り方が良くわからなかったので、自分で作る方法を調べてみました。以下のコードがNSInvocationを使ってtest:というメソッドを呼び出してみたものです。

#import <Cocoa/Cocoa.h>

@interface InvocationTest : NSObject {
    
}

@end

@implementation InvocationTest

- (void)awakeFromNib
{
    SEL selector;
    NSMethodSignature *signature;
    NSInvocation *invocation;
    
    selector = @selector(test:);
    signature = [[self class] 
        instanceMethodSignatureForSelector:selector];
    
    if (signature) {
        invocation = [NSInvocation invocationWithMethodSignature:signature];
        [invocation setSelector:selector];
        [invocation setTarget:self];
        
        NSString *sendString = @"send";
        [invocation setArgument:&sendString atIndex:2];
        
        [invocation invoke];

        id returnValue;
        [invocation getReturnValue:&returnValue];
        
        NSLog(@"%@", returnValue);
    }
    
    [NSApp terminate:self];
}

- (id)test:(id)object
{
    NSLog(@"call test with %@", object);
    return @"return";
}

@end

まず、NSMethodSignatureというのがないとNSInvocationがインスタンス化できないので、instanceMethodSignatureForSelector:で作成します。これはNSObjectのクラスオブジェクトですが、呼び出すクラスにお目当てのメソッドが無いとNSMethodSignatureの作成に成功しません。

NSMethodSignatureのインスタンスが作成できたら、NSInvocationのクラスメソッドであるinvocationWithMethodSignature:でNSInvocationのインスタンスを作成します。

ターゲットとなるオブジェクトと、ここでもセレクタを設定します。さらに引数も渡せますが、setArgument:index:メソッドのインデックスに渡す数字は2からになります。リファレンスには0はselfで、1は_cmdがすでに使われているというような事が書いてあります。

invokeで登録したメソッドが呼び出されます。返り値がある場合はNSInvocationのインスタンスに保持されていますのでgetReturnValueで受け取ります。

Core MIDI その2 MIDIPacketの受信

Core MIDIではMIDIPacketというものがひとつのMIDIデータになります。そのMIDIPacketをMIDIPacketListに複数(あるいはひとつ)まとめて、MIDIエンドポイントを通してMIDIデータのやり取りをします。

MIDIPacketは構造体になっていて、以下のような感じで宣言されています。

struct MIDIPacket
{
    MIDITimeStamp   timeStamp;
    UInt16          length;
    Byte            data[256];
};
typedef struct MIDIPacket MIDIPacket;

timeStampは、0であれば受け取ったらすぐに処理をしろという事で、値が入ってれば処理すべきHosttimeという事のようです。lengthはdataのサイズです。dataはMIDIデータそのもので、すでに256バイトの配列が確保されているように見えますが、実際にいろいろ試してみた感じではlength分しかデータ領域が無いと思っていた方が良さそうです。

MIDIPacketListは以下のように宣言されています。

struct MIDIPacketList
{
    UInt32      numPackets;	
    MIDIPacket  packet[1];
};

packetは一つ分だけ確保されているように見えますが、MIDIPacketListには複数のMIDIPacketが含まれている事があり、その場合はnumPacketsにその数が入っていて、packetはその分の領域が確保されている状態になるようです。

そんな感じで、宣言されているのをそのまま当てにするとちょっと違っていたりするので、MIDIPacketListからのMIDIPacketの読み込みや、MIDPacketListの作成は、直接行うのではなく専用の関数が用意されています。

extern MIDIPacket *
MIDIPacketNext(MIDIPacket *pkt);

extern MIDIPacket *
MIDIPacketListInit(MIDIPacketList *pktlist)

extern MIDIPacket *
MIDIPacketListAdd(MIDIPacketList *  pktlist,
                  ByteCount         listSize,
                  MIDIPacket *      curPacket,
                  MIDITimeStamp     time,
                  ByteCount         nData,
                  const Byte *      data)

MIDIPacketListからの読み込みですが、ヘッダを見ると以下のような感じにするよう書いてあります。このようにMIDIPacketNextを使えばMIDIPacketのサイズに応じてパケットの位置を進めてくるようです。

MIDIPacket *packet = &packetList->packet[0];
  for (int i = 0; i < packetList->numPackets; ++i) {
    //...
    packet = MIDIPacketNext(packet);
  }

MIDIPacketListを作成するときにはMIDIPacketInitとMIDIPacketListAddを使ってMIDIPacketを追加して行くという事になりますがそれは次回やります。

MIDIPacketListを受信するには、MIDIClientCreate関数でMIDIClientを作成し、そのMIDIClientにMIDIInputPortCreate関数で入力用のMIDIPortを作成してMIDIデータを受信するコールバック関数を登録し、MIDIPortConnectSource関数でMIDIPortをMIDIEndpointと接続します。それぞれの関数は以下のように宣言されています。

extern OSStatus
MIDIClientCreate(CFStringRef     name, 
                 MIDINotifyProc	 notifyProc, 
                 void *          notifyRefCon, 
                 MIDIClientRef * outClient )

extern OSStatus 
MIDIInputPortCreate(MIDIClientRef client, 
                    CFStringRef   portName, 
                    MIDIReadProc  readProc, 
                    void *        refCon, 
                    MIDIPortRef * outPort )

extern OSStatus
MIDIPortConnectSource(MIDIPortRef     port, 
                      MIDIEndpointRef source, 
                      void *          connRefCon )

コールバック関数の引数の構成は決まっていて以下のような感じで宣言されています。

typedef void
(*MIDIReadProc)(const MIDIPacketList *pktlist, 
    void *readProcRefCon, void *srcConnRefCon);

readProcRefConにはMIDIPortを作成したときに登録したrefCon、srcConnRefConにはMIDIPortにソースを接続したときに登録したconnRefConがそれぞれ渡ってきます。これらを使ってMIDIPacketListがやってくるポート等を識別したりして処理する事ができると思います。呼ばれるのはメインスレッドではないようです。

それと、MIDIClientやMIDIPortが必要なくなったらMIDIClientDispose関数やMIDIPortDisposeで解放します。

以下が、アプリケーションの外部からMIDIデータを受信するサンプルです。ちょっと今回は気分を変えて、Cocoaアプリケーションのmain.mを以下のような感じで書き換えてみます。CoreMIDI.Frameworkも追加しておいてください。

//
//  main.m
//

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

static void 
MIDIInputProc(const MIDIPacketList *pktlist, 
    void *readProcRefCon, void *srcConnRefCon)
{
    //MIDIパケットリストの先頭のMIDIPacketのポインタを取得
    MIDIPacket *packet = (MIDIPacket *)&(pktlist->packet[0]);
    //パケットリストからパケットの数を取得
    UInt32 packetCount = pktlist->numPackets;
    
    for (NSInteger i = 0; i < packetCount; i++) {
        
        //data[0]からメッセージの種類とチャンネルを分けて取得する
        Byte mes = packet->data[0] & 0xF0;
        Byte ch = packet->data[0] & 0x0F;
        
        //メッセージの種類に応じてログに表示
        if ((mes == 0x90) && (packet->data[2] != 0)) {
            NSLog(@"note on number = %2.2x / velocity = %2.2x / channel = %2.2x",
                  packet->data[1], packet->data[2], ch);
        } else if (mes == 0x80 || mes == 0x90) {
            NSLog(@"note off number = %2.2x / velocity = %2.2x / channel = %2.2x", 
                  packet->data[1], packet->data[2], ch);
        } else if (mes == 0xB0) {
            NSLog(@"cc number = %2.2x / data = %2.2x / channel = %2.2x", 
                  packet->data[1], packet->data[2], ch);
        } else {
            NSLog(@"etc");
        }
        
        //次のパケットへ進む
        packet = MIDIPacketNext(packet);
    }
}

int main(int argc, char *argv[])
{
    OSStatus err;
    MIDIClientRef clientRef;
    MIDIPortRef inputPortRef;
    
    //MIDIクライアントを作成する
    NSString *clientName = @"inputClient";
    err = MIDIClientCreate((CFStringRef)clientName, NULL, NULL, &clientRef);
    if (err != noErr) {
        NSLog(@"MIDIClientCreate err = %d", err);
        return 1;
    }
    
    //MIDIポートを作成する
    NSString *inputPortName = @"inputPort";
    err = MIDIInputPortCreate(
        clientRef, (CFStringRef)inputPortName, 
        MIDIInputProc, NULL, &inputPortRef);
    if (err != noErr) {
        NSLog(@"MIDIInputPortCreate err = %d", err);
        return 1;
    }
    
    //MIDIエンドポイントを取得し、MIDIポートに接続する
    ItemCount sourceCount = MIDIGetNumberOfSources();
    for (ItemCount i = 0; i < sourceCount; i++) {
        MIDIEndpointRef sourcePointRef = MIDIGetSource(i);
        err = MIDIPortConnectSource(inputPortRef, sourcePointRef, NULL);
        if (err != noErr) {
            NSLog(@"MIDIPortConnectSource err = %d", err);
            return 1;
        }
    }
    
    return NSApplicationMain(argc,  (const char **) argv);
}

実行すると、オンラインになっているMIDIインプット全てから受信してログに表示します。MIDIPortとかの解放は全然やってませんし、実際にはMIDITimeStampの時間にもちゃんと対応して処理する事も必要になると思います。

Core MIDI その1 MIDIObject

Core MIDIに関してリアルタイムで調査中ですが、ちょこちょこと分かった事を書いて行こうと思います。ここのページにもいろいろと書いてありますので参考に。

まず用語解説的なところから始めまして、Core MIDIを使うときに必要なMIDIオブジェクトというものを見て行きます。MIDIオブジェクトは、

・MIDIDevice
・MIDIEntity
・MIDIEndpoint

・MIDIClient
・MIDIPort

という5つがあるのですが、上の3つは基本的にアプリケーション外部からCoreMIDI経由で取得するもの(MIDIEndpointは例外あり)で、下の2つのMIDIClientとMIDIPortはアプリケーション側に作るものです。

まず、上の方から見て行きます。MIDIDeviceというのは名前の通りMIDIのデバイスの事ですが、Core MIDIではMIDIインターフェース以外にも、アプリケーション同士を直接つなげるIACドライバとか、ネットワーク経由での接続とかもありまして、それらも全て同じMIDIDeviceとして取得できます。

まあ、実際見た方が分かりやすいと思いますので、Audio MIDI 設定を開いてみますと…

AudioMIDISetup.jpg

このように表示されているとしたら、この表示されている4つがMIDIDeviceになります。右から2つ目の「Onyx Firewire(0134)」ように実際つながっていなくてもAudio MIDI 設定で登録されていれば、MIDIDeviceとして取得できます。

MIDIDeviceはMIDIEntityというのを持っていまして、たとえば今回見ているAudio MIDI 設定のUA-25でいえば…

UA25.jpg

左下の「ポート」の中に表示されている「EDIROL UA-25」というのがエンティティになります。これはデバイスによって複数持つ場合もあり、「IACドライバ」なんかでは自由に増やせますから、

IAC.jpg

こんな風に2つ作っておくと、「IACドライバ」というMIDIDeviceの中から「バス1」と「バス2」という2つのMIDIEntityが取得できます。

MIDIEndpointというのは、MIDIEntityの中にあるMIDI入力や出力の事(Audio MIDI 設定ではポートの右にあるコネクタのところのMIDI入力と出力)になります。MIDIインターフェースに装備されているMIDI INやMIDI OUTのコネクタそのものだと考えてよいと思います。Core MIDIではアプリケーション側から見て入力はSource、出力はDestinationと表現されます。

アプリケーション内部の方に移りまして、MIDIClientというのはアプリケーション内部に作るMIDIデバイスのようなものです。MIDIClientを作成するときにMacのMIDIシステムの変更を受け取るコールバック関数を登録する事もできます。

アプリケーション外部とMIDIデータをやり取りするにはMIDIClientにMIDIPortというものを作成します。入力に関しては、MIDIEndpointをMIDIPortに登録すると同時にコールバックを設定して、リアルタイムでMIDIデータを受け取れます。

ちなみにMusicPlayerの再生とかCore Audio Clockのように、MIDIClientとかMIDIPortとかを作らずにMIDIEndpointを直接渡すだけで良い場合もあります。MIDIClientやMIDIPortは自分でMIDIの生のデータを処理したいときに使うという感じでしょうか。

といったところでサンプルです、。MIDIデバイスからMIDIエンティティを取得してさらに入力側のMIDIエンドポイントを取得します。FoundationToolで新規作成して、CoreMIDI.Frameworkを追加し実行すると、使用可能なデバイスの、エンドポイントの名前とオフラインかどうかを表示します。

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

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    OSStatus err;
    CFStringRef strDeviceRef = NULL;
    CFStringRef strEndPointRef = NULL;
    
    //MIDIデバイスの数を取得する
    ItemCount count = MIDIGetNumberOfDevices();
    
    for (ItemCount i = 0; i < count; i++) {
        
        //MIDIデバイスを取得する
        MIDIDeviceRef devRef = MIDIGetDevice(i);
        
        //MIDIデバイスの名前を取得する
        err = MIDIObjectGetStringProperty(
            devRef, kMIDIPropertyName, &strDeviceRef);
        if (err != noErr) {
            NSLog(@"err = %d", err);
        }
        
        //MIDIエンティティの数を取得する
        ItemCount numEntities = MIDIDeviceGetNumberOfEntities(devRef);
        
        for (NSInteger j = 0; j < numEntities; j++) {
            
            //MIDIエンティティを取得する
            MIDIEntityRef entityRef = MIDIDeviceGetEntity(devRef, j);
            
            //MIDIエンティティからMIDIエンドポイントのソースの数を取得する
            ItemCount sourceCount = MIDIEntityGetNumberOfSources(entityRef);
            
            for (NSInteger k = 0; k < sourceCount; k++) {
                
                //MIDIエンドポイントを取得する
                MIDIEndpointRef endPointRef = 
                    MIDIEntityGetSource(entityRef, k);
                
                //MIDIエンドポイントの名前を取得する
                err = MIDIObjectGetStringProperty(
                    endPointRef, kMIDIPropertyName, &strEndPointRef);
                if (err != noErr) {
                    NSLog(@"err = %d", err);
                }
                
                SInt32 isOffline;
                err = MIDIObjectGetIntegerProperty(
                    endPointRef, kMIDIPropertyOffline, &isOffline);
                if (err != noErr) {
                    NSLog(@"err = %d", err);
                }
                
                //ログに表示
                NSLog(@"Device = %@ / EndPoint = %@ / Offline = %d", 
                    strDeviceRef, strEndPointRef, isOffline);
                
                if (strEndPointRef) {
                    CFRelease(strEndPointRef);
                    strEndPointRef = NULL;
                }
            }
        }
        
        if (strDeviceRef) {
            CFRelease(strDeviceRef);
            strDeviceRef = NULL;
        }
    }
    
    [pool drain];
    return 0;
}

この方法で使用可能なMIDIエンドポイントを取得するときは、オフラインになっていない事もチェックしないといけなかったりしていろいろ面倒ですが、以下のようにMIDIGetNumberOfSourcesとMIDIGetSourceで、いきなりオンラインになっているMIDIエンドポイントを取得できます。

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

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

    OSStatus err;
    CFStringRef strEndPointRef = NULL;
    
    ItemCount sourceCount = MIDIGetNumberOfSources();
    
    for (NSInteger i = 0; i < sourceCount; i++) {
        
        //MIDIエンドポイントを取得する
        MIDIEndpointRef endPointRef = MIDIGetSource(i);
        
        //MIDIエンドポイントの名前を取得する
        err = MIDIObjectGetStringProperty(
            endPointRef, kMIDIPropertyName, &strEndPointRef);
        
        if (err == noErr) {
            
            //ログに表示
            NSLog(@"EndPoint = %@", strEndPointRef);
            
            if (strEndPointRef) {
                CFRelease(strEndPointRef);
                strEndPointRef = NULL;
            }
            
        } else {
            
            NSLog(@"err = %d", err);
        }
    }
    
    [pool drain];
    return 0;
}

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付近になります。

動き続けるCALayer

CoreAnimationでCALayerを動かし続けたい時の方法です。プロパティ変更での暗黙的なアニメーションだといろいろと不都合なので、CABasicAnimationを使ってやってみました。

以下がそのサンプルです。カスタムビューにアウトレットをつなげて実行してください。

//
//  EndlessCoreAnimationTest.h
//

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

@interface EndlessCoreAnimationTest : NSObject {

    IBOutlet NSView *view;
    CALayer *moveLayer;
    NSTimer *timer;
    NSDate *startDate;
}

- (void)changeAnimation;

@end


//
//  EndlessCoreAnimationTest.m
//

#import "EndlessCoreAnimationTest.h"


@implementation EndlessCoreAnimationTest

- (void)awakeFromNib
{
    NSRect viewRect = [view bounds];
    CGFloat viewWidth = viewRect.size.width;
    CGFloat viewHeight = viewRect.size.height;
    CGFloat minSize = (viewWidth > viewHeight) ? viewHeight : viewWidth;
    
    CALayer *baseLayer = [CALayer layer];
    CGColorRef baseColor = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 1.0);
    baseLayer.backgroundColor = baseColor;
    CGColorRelease(baseColor);
    
    moveLayer = [CALayer layer];
    moveLayer.anchorPoint = CGPointMake(0.5, - 9.0);
    moveLayer.bounds = CGRectMake(0, 0, minSize * 0.05, minSize * 0.05);
    moveLayer.position = CGPointMake(viewWidth * 0.5, viewHeight * 0.5);
    CGColorRef moveColor = CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0);
    moveLayer.backgroundColor = moveColor;
    CGColorRelease(moveColor);
    
    [view setLayer:baseLayer];
    [view setWantsLayer:YES];
    [baseLayer addSublayer:moveLayer];
    
    [self performSelector:@selector(startTimer) 
        withObject:nil afterDelay:1.0];
}

- (void)startTimer
{
    startDate = [[NSDate date] retain];
    [self changeAnimation];
    timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
        target:self selector:@selector(changeAnimation) 
        userInfo:nil repeats:YES];
}

- (void)changeAnimation
{
    CGFloat currentTime = [startDate timeIntervalSinceNow];
    CGFloat duration = 2.0;
    
    CABasicAnimation *anim = 
        [CABasicAnimation animationWithKeyPath:@"transform"];
    CATransform3D fromTrans = 
        CATransform3DMakeRotation(currentTime * M_PI / 4.0, 0.0, 0.0, 1.0);
    CATransform3D toTrans = 
        CATransform3DMakeRotation((currentTime - duration) * M_PI / 4.0, 
        0.0, 0.0, 1.0);
	
    anim.fromValue = [NSValue valueWithCATransform3D:fromTrans];
    anim.toValue = [NSValue valueWithCATransform3D:toTrans];
    anim.duration = duration;
    
    [moveLayer addAnimation:anim forKey:@"transformAnimation"];
}


- (void) dealloc
{
    [timer invalidate];
    [startDate release];
    
    [super dealloc];
}

@end

durationを2秒にしてtoValueを2秒後の値にしたCABasicAnimationを、NSTimerで1秒ごとに途切れる事無く追加しています。追加する時に動いているアニメーションの位置が新たなアニメーションのfromValueと一致するようになっていればスムーズにつながります。

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;
}