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

文字列のイメージを作成する

OpenGL ESで文字列を表示しようと思ったのですが、OpenGL自体にそんな機能は無さそうなので、テクスチャに文字列を描画する方法を調べてみました。

テクスチャをCGContextから作って貼付けるところはDev Centerのサンプルソースを参考にしていただくとして、その前段階でCGImageの文字列を作成するコードです。

CGRect imageRect; //文字列用の画像の大きさ
CGContextRef bitmapContext; //文字列用のコンテキスト
Byte *bitmapBuffer; //コンテキストのバッファ

というのが用意されているとして、以下のような感じです。

memset(bitmapBuffer, 0, imageRect.size.width * imageRect.size.height * 4);
    
CGContextSetRGBFillColor (bitmapContext, 0.0, 0.0, 0.0, 1.0);
CGContextFillRect (bitmapContext, imageRect);
    
UIGraphicsPushContext(bitmapContext);
    
UIFont *tFont = [UIFont systemFontOfSize:40];
[[UIColor whiteColor] set];
[@"Test String 1234567890" drawAtPoint:CGPointMake(0, 0) withFont:tFont];
[@"日本語もOK!" drawAtPoint:CGPointMake(0, 44) withFont:tFont];
    
UIGraphicsPopContext();
    
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
//ここでテクスチャに描画する
CGImageRelease(image);

NSStringの描画メソッドはコンテキストを指定する事が出来ないので、UIGraphicPushContext()でbitmapContextをカレントのコンテキストにしています。実際にテクスチャに貼付けて表示してみるとこんな感じ。

drawtext.jpg

ちなみに、ランドスケープで左上原点にしてみるってこともやっていたので、こんな位置に表示されています。

これでパフォーマンス的にどうなのかは実際にアプリに組み込んでみないとまだ分かりませんけど。

修正とiPod 2Gでの不具合

Touch the Waveの波形表示が再生位置とちょっとずれていたので、修正したものをApp Storeに提出してあります。何も無ければ2〜3日後くらいには公開されるんじゃないでしょうか。

それはそれとして、音質向上のための修正をしていたらv0.5をiPod touch第2世代で使っているとノイズがプチプチと入っている事が判明してしまいました。v0.4ではCore Animationを使っている事で数秒間の動作停止が起きることがあって、それを修正するためにv0.5ではOpenGLに変更したんですけど、どうやらそっちも相性が悪かったようです。

別に特別な処理をしているからという訳ではなく、何も描画していないOpenGLのビューを一枚表示しておいて、RemoteIOでシンプルにサイン波だけを鳴らしてみると、それだけでプチプチいっちゃいます。なのでTouch the Waveではプレイリストとダウンロードの画面では大丈夫なんですけど、肝心のプレイヤーでノイズが入ってしまうという困った感じです。

まさか僕のデバイスだけの故障とかじゃないですよねぇ。第1世代では何事も無く再生できちゃうんですけど。

まあ、iPod touch第2世代はもともとロスレスの再生がプツプツとぎれるって不具合がありますから。全体的にパフォーマンスはアップしているけどオーディオは逆に弱くなっているんでしょうか。

回避策としてあと残されているのは、Quartzですかね。試してみてパフォーマンス的に問題無さそうならそっちに切り替えます。せっかくのiPhoneでのプログラミングなんだからCore AnimationとかOpenGLとか使ってみたかったのに、結局Quartzに落ち着くとしたらもったいないなぁと思いますけど。それでも駄目ならOS側の修正アップデートを待つしかないって感じです。

さっそくAVAudioPlayerを使ってみた

iPhone OSが2.2にアップデートされまして、Frameworkに新しくAVFoundationなんてものが追加されていました。今のところAVAudioPlayerというAPIしか無いようですが、AVと名がついているだけに今後映像がらみでも機能が増えそうな気がします。

そのAVAudioPlayerを早速試してみたところ、お手軽BGM再生APIという感じのようです。オーディオファイルのパスを渡してやるだけでWAV、AIFF、mp3、AAC、ロスレスあたりはいとも簡単に再生できます。これがあればやたらめんどくさいAudio Queue Serviceなんていりません。

ということでとりあえず再生してみるコードです。バンドルにmp3を入れてから実行すると延々とループして再生します。2つ以上あればミックスして再生されます。ちょっとお行儀の悪いコードですので、そのへんは適当に流してください。

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
    [window makeKeyAndVisible];
    
    NSBundle *bundle = [NSBundle mainBundle];
    NSArray *wavePaths = [NSBundle pathsForResourcesOfType:@"mp3" inDirectory:[bundle bundlePath]];
    
    for (NSString *path in wavePaths) {
        NSURL *url = [NSURL fileURLWithPath:path];
        NSError *error = nil;
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        if (!error) {
            audioPlayer.numberOfLoops = -1;
            [audioPlayer play];
        }
    }
}

RemoteIOでのオーディオ再生

iPhone用のアプリも公開できて一段落つきまして、さらにNDAも緩和されたという事で、iPhoneのオーディオプログラミングネタを書いていきたいと思います。

iPhoneで音を再生する方法はいくつかあります。短いサンプルを再生するならSystemSoundService。BGMで使うような長い曲を再生するならAudioQueueService。レイテンシーを低く再生するならAudioUnit。あと、OpenALを使うという手もあります。

「Touch the Wave」で使っているのはAudioUnitです。SystemSoundは短すぎて役に立ちませんし、AudioQueueではリアルタイムに音を変化させるような事や逆再生が出来ないですし(たぶん)、OpenALはよくわからないって感じでしたので。

何はなくともとりあえずAudioUnitを使うクラスのサンプルコードです。このクラスを生成すればAudioUnitが開始されて、解放すれば停止します。シンプルにするためにエラー処理はまったく記述していません。

※最終修正 2011/10/24

//
//  YKAudioOutput.h
//
#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>
@interface YKAudioOutput : NSObject {
AudioUnit outputUnit;
}
@end
//
//  YKAudioOutput.m
//
#import "YKAudioOutput.h"
@implementation YKAudioOutput
static OSStatus OutputCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData)
{
    OSStatus err = noErr;
    for (NSInteger i = 0; i < ioData->mNumberBuffers; i++) {
        //2009/6/28 OS3.0対応
        SInt16 *ptr = ioData->mBuffers[i].mData;
        for (NSInteger j = 0; j < inNumberFrames; j++) {
            UInt32 channels = ioData->mBuffers[i].mNumberChannels;
            for (NSInteger k = 0; k < channels; k++) {
                ptr[j * channels + k] = sin(M_PI / inNumberFrames * j * 50) * INT16_MAX;
            }
        }
    }
    return err;
}
- (void)setupOutputUnit
{
    AudioComponent component;
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    
    component = AudioComponentFindNext(NULL, &desc);
    AudioComponentInstanceNew(component, &outputUnit);
    AudioUnitInitialize(outputUnit);
    AURenderCallbackStruct callback;
    callback.inputProc = OutputCallback;
    callback.inputProcRefCon = self;
    
    AudioUnitSetProperty(outputUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Global,
                         0,
                         &callback,
                         sizeof(AURenderCallbackStruct));
    
    AudioStreamBasicDescription outputFormat;
    UInt32 size = sizeof(AudioStreamBasicDescription);
    
    outputFormat.mSampleRate = 44100;
    outputFormat.mFormatID = kAudioFormatLinearPCM;
    outputFormat.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
    outputFormat.mBitsPerChannel = 16;
    outputFormat.mChannelsPerFrame = 2;
    outputFormat.mFramesPerPacket = 1;
    outputFormat.mBytesPerFrame = outputFormat.mBitsPerChannel / 8 * outputFormat.mChannelsPerFrame;
    outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
    AudioUnitSetProperty(outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormat, size);
    AudioOutputUnitStart(outputUnit);
}
- (void)dispose
{
    AudioOutputUnitStop(outputUnit);
    AudioUnitUninitialize(outputUnit);
    AudioComponentInstanceDispose(outputUnit);
    outputUnit = NULL;
}
- (id) init
{
    self = [super init];
    if (self != nil) {
        [self setupOutputUnit];
    }
    return self;
}
- (void) dealloc
{
    [self dispose];
    [super dealloc];
}
@end

まず、AudioUnitを使うからといってAudioUnitFrameworkをインポートしてもエラーが出ます。AudioUnitだけをつかうときでもAudioUnitFrameworkはインポートせず、AudioToolbox.Frameworkだけをインポートすれば良いようです。

コードを見ていきますと、setupOutputUnitメソッドがAudioUnitのセットアップしている部分になります。順番としては、

・AudioComponentDescriptionに呼び出すAudioUnitの情報を記述する
・AudioComponentNextでAudioComponentを取得する
・AudioComponentInstanceNewでAudioUnitを取得する
・AudioUnitInitializeでAudioUnitを初期化する
・AURenderCallbackStructでコールバックの情報を記述する
・AudioUnitSetPropertyでコールバックの設定をする
・AudioUnitStartでRemoteIOをスタートする

という流れになっています。Macでのときと違って関数の名前にAudioとかついていますが、使い方は変わりません。iPhoneでのアウトプットユニットはRemoteIOと名前がついてまして、サブタイプにkAudioUnitSubType_RemoteIOを指定します。RemoteIOがスタートしたらコールバックが定期的に呼び出されますので、そこでオーディオデータを渡します。

iPhone OS 2.2.1までのRemoteIOのデフォルトのフォーマットは8.24の固定小数点でしたが、OS 3.0からは16bit整数のインターリーブドに変更されたようです。ただ、あくまでデフォルトが変更されただけですので、OSのバージョンがなんだろうが、自分でフォーマットを設定しておけば互換性は問題なく保たれます。ミキサーのユニットなんかは変わらず8.24みたいですので注意が必要です。

iOS 5ではさらにデフォルトが32ビットfloatに変更されているようです。AudioUnitSetPropertyでフォーマットを設定するように変更しました。(2011/10/24)

ジョグホイール

iPodのクリックホイールのように、グルグルと円を描いて値を増減させる例です。NSViewのサブクラスで以下のような感じに。1周まわすと1.0増減します。

//
// YKJogWheelView.h
//

#import <Cocoa/Cocoa.h>

@interface YKJogWheelView : NSView {

    CGFloat gStartAngle;
    CGFloat gPreAngle;
    NSInteger gRotCount;
}

@end
//
// YKJogWheelView.m
//

#import "YKJogWheelView.h"

@implementation YKJogWheelView

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

- (void)drawRect:(NSRect)rect {
    // Drawing code here.
}

- (void)setValue:(CGFloat)value
{
    NSLog(@"value = %f", value);
}

- (CGFloat)getAngle:(NSEvent *)theEvent
{
    NSPoint point = [theEvent locationInWindow];
    NSRect frame = [self frame];
    return -atan2(point.y - frame.origin.y - frame.size.height / 2, point.x - frame.origin.x - frame.size.width / 2) / M_PI / 2.0;
}

- (void)setDragEvent:(NSEvent *)theEvent
{
    CGFloat newAngle = [self getAngle:theEvent];
    
    if ((newAngle - gPreAngle) > 0.5) {
        gRotCount--;
    } else if ((newAngle - gPreAngle) < -0.5) {
        gRotCount++;
    }
    
    [self setValue:newAngle - gStartAngle + gRotCount];
    
    gPreAngle = newAngle;
}

- (void)mouseDown:(NSEvent *)theEvent
{
    gStartAngle = [self getAngle:theEvent];
    gRotCount = 0;
    [self setValue:0];
}

- (void)mouseDragged:(NSEvent *)theEvent
{
    [self setDragEvent:theEvent];
}

@end

24ビット

24bitのオーディオデータを作ったり読みこもうとしてもObjective-C(というかC言語)では24bitの型がありませんので、符号付き32bit整数の上位24bit分を使うというのが簡単な方法ではないかと思います。

ただこの場合、32bitの下位8bitは単純に切り捨てられますので、音にこだわるのであればディザをかけたりしたほうが良いのかもしれませんが、とりあえず今回は無視します。

32bit領域のAlignedHighなデータとして渡したりするのであれば何もしなくてもいいですが、32bit領域でAlignedLowな24bitのデータにしなくちゃいけないなんて時があったりしたら、

SInt32 value;

という、あるオーディオのデータがあったとして、

value >>= 8;

としてやります。

処理系によって算術シフトになるか論理シフトが変わるらしいですが、Xcodeでgccを使っているのであればこの場合算術シフトで、valueがマイナスのときは上位8bitが1で埋め尽くされています。

ちなみに、IntelMac想定の検証コード。

#import <Foundation/Foundation.h>

void logBit (SInt32 value) {
    printf("MSB ");
    for (NSInteger i = 3; i >= 0; i--) {
        Byte *valuePtr = (Byte *)&value;
        for (NSInteger j = 7; j >= 0; j--) {
            SInt32 isBitOn = (valuePtr[i] & (1 << j)) ? YES : NO;
            printf("%d", isBitOn);
        }
        printf(" ");
    }
    printf(" LSB\n\n");
}

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

    SInt32 value = INT32_MIN;
    printf("value = %d\n", value);
    logBit(value);
    
    SInt32 sValue = value;
    sValue >>= 8;
    printf("signed shift value = %d\n", sValue);
    logBit(sValue);
    
    UInt32 uValue = (UInt32)value;
    uValue >>= 8;
    printf("unsigned shift value = %u\n", uValue);
    logBit(uValue);
    
    [pool drain];
    return 0;
}

実行すると…

value = -2147483648
MSB 10000000 00000000 00000000 00000000  LSB

signed shift value = -8388608
MSB 11111111 10000000 00000000 00000000  LSB

unsigned shift value = 8388608
MSB 00000000 10000000 00000000 00000000  LSB

という感じになります。

リサージュ波形を表示する

オシロスコープで表示できるようなリサージュ波形を描画するには、

x = sin(右チャンネルのオーディオデータ);
y = sin(左チャンネルのオーディオデータ);

という感じでサンプルごとに座標を求めて、線で繋ぐ。

NSViewのサブクラスでこのようなインスタンス変数があるとして、

NSUInteger length; //オーディオデータのサンプル数
float *lPtr; //左チャンネルのオーディオデータ
float *rPtr; //右チャンネルのオーディオデータ

以下のようにdrawRectメソッドを記述する。(※2008/7/8 描画する位置をrectではなくboundsから求めるように変更しました)

- (void)drawRect:(NSRect)rect {
    
    NSRect viewRect = [self bounds];
    double halfWidth = viewRect.size.width / 2.0;
    double halfHeight = viewRect.size.height / 2.0;
    
    [[NSColor blackColor] set];
    NSRectFill(
        NSMakeRect(0, 0, viewRect.size.width, viewRect.size.height));
    
    NSBezierPath *path = [NSBezierPath bezierPath];
    [[NSColor greenColor] set];
    [path setLineWidth:1.0];
    
    for (NSUInteger i = 0; i < length; i++) {
        
        double x = sin(rPtr[i]);
        double y = sin(lPtr[i]);
        
        NSPoint point = 
            NSMakePoint(x * halfWidth + halfWidth , 
                y * halfHeight + halfHeight);
        if ([path isEmpty]) {
            [path moveToPoint:point];
        }
        
        [path lineToPoint:point];
    }
    
    [path stroke];
}

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

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

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

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

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

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

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

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

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


    //
    // ここで、startSecondsとsecondsPerFrameを元に
    // オーディオデータを書き込む
    //
}

テンポラリファイル名を取得する

テンポラリのディレクトリの中のテンポラリのファイル名を取得するサンプル。

NSTemporaryDirectory()でテンポラリフォルダを取得。mkstempsでテンポラリファイル名が取得できるが、一旦書き込み可能な文字列の配列にコピーしないといけない。NSStringのlengthでは日本語とか含まれている場合、取得した長さが配列の長さと違うので、strlenで配列の長さを取得してコピーする。

#import <Foundation/Foundation.h>

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

    NSString *tempDir = NSTemporaryDirectory();
    NSString *filePath = 
        [tempDir stringByAppendingPathComponent:
            @"prefixXXXXXXsuffix"];
    
    size_t bufferSize = 
        strlen([filePath fileSystemRepresentation]) + 1;
    char buffer[bufferSize];
    if ([filePath getFileSystemRepresentation:buffer 
            maxLength:bufferSize]) {
        if (mkstemps(buffer, 6) != -1) {
            NSLog(@"TemporaryFile = '%s'", buffer);
        }
    }
    
    [pool drain];
    return 0;
}

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