前回の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