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の「スタート」と「ストップ」と「スタートタイム変更」を受け取って実際に処理を行うようにしてあります。

コメントを残す

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