Core MIDI – Mac」カテゴリーアーカイブ

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

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