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

コメントを残す

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