iPad Core MIDIネットワーク通信アプリ開発始末記

色々あってCore MIDIを使ってネットワーク経由でMIDIを送信するiPadアプリを作ったので、メモを残しておく。

開発にはXcode 4.0.2を使用した。

MIDIの送信

MIDIを送信する方法は、Mac OS XでCore MIDIを使う場合と同じ。

  1. MIDIClientCreate()でMIDIクライアントを作成する。
  2. 作成したクライアントに対するMIDI出力ポートをMIDIOutputPortCreate()で作成する。
  3. 送信先となるMIDIエンドポイント(destination)を取得する。
  4. 送信したいMIDIデータからMIDIパケットリストを作成する。
  5. 作成したパケットリストをMIDISend()で送信する。

このあたりはhttp://objective-audio.jp/2008/06/core-midi-midipacketlist.htmlが参考になる。Mac OS X上での内容だが、iPadでも概ね同じ方法で大丈夫なようだ。

注意点として、MIDIパケットリストを作成する時に再生時刻を設定できるのだが、現時点ではiOS SDKのCore AudioにAudioGetCurrentHostTime()やAudioConvertNanosToHostTime()といった時刻情報をシステムティック単位で扱える便利なAPIが存在しない。

対策はApple DeveloperのQ&Aに書いてあったが、mach_absolute_time()やmach_timebase_info()あたりを使うことになるようだ。

ネットワークセッション用のMIDIエンドポイントの探し方

Mac OS Xでネットワーク経由でMIDIを送信する場合、Mac上での動作はこんな感じになる。

  1. Audio MIDI設定のMIDIネットワーク設定でセッションを生成する。この時「ローカル名」の項目に任意の名前を付けておく。
  2. MIDIアプリで出力先を選択する際に、上の「ローカル名」で設定した名前を選択する。

アプリケーションの実装においても、MIDIGetDestination()などのAPIでAudio MIDI設定で生成したセッションを(まだ通信を確立していない状態でも)取得できるので、それをMIDIエンドポイントとして使用することになる。

ところがiPadにはAudio MIDI設定のようなツールは存在しないようだ。なのでMac用のアプリと同じ手法ではネットワーク経由でMIDIを送信することはできない。

ではどうすればよいか? 実はiPadではMIDIクライアントを作成した時に裏で自動的にセッションが生成されているので、それをMIDIエンドポイントとして取得して使用することになる。

で、取得する方法だが、iOS SDKのCore MIDIにはMIDINetworkSession Classが追加されていて、その中のdestinationEndpointやsourceEndpointといったメソッドを使えばよい。

MIDINetworkSession *session = [MIDINetworkSession defaultSession];
MIDIEndpointRef source = [session sourceEndpoint];
MIDIEndpointRef destination = [session destinationEndpoint];

ちなみにMIDINetworkSessionのオブジェクトはクラスメソッドdefaultSessionで取得するのだが、シングルトンなオブジェクトだとAppleのリファレンスに書かれている。なので気をつけないと知らないうちに複数のスレッドから排他制御無しにオブジェクトにアクセスしてしまい、アプリが落ちたりすることがある。

MIDINetworkSessionを操作する処理は極力同じモジュール(同一クラス内とか、同一ファイル内とか)で行うように実装したほうが無難だろう。その方が排他制御を入れやすくなる。

セッションの接続を確立する方法

MacではAudio MIDI設定で接続を確立していたが、iPadではアプリ側で自力で接続を確立することになる。といっても便利なライブラリが用意されているので、それほど苦労しない。

MIDINetworkSessionクラスにaddConnectionというメソッドが用意されていて、これを使うことで自動的に他のiOSマシンやMacApple MIDI network driver互換の機能を有しているコンピュータとの接続が開始される。

この時MIDINetworkConnectionやMIDINetworkHostといったクラスを使用する。手順としてはこんな感じ。

  1. 接続先ホストを表すMIDINetworkHostクラスのオブジェクトを生成する。
  2. MIDINetworkHostオブジェクトを元に、ネットワーク接続を表すMIDINetworkConnectionクラスのオブジェクトを生成する。
  3. MIDINetworkSessionオブジェクトに、生成したMIDINetworkConnectionオブジェクトをaddConnectionメソッドで登録する。
MIDINetworkHost *host = [MIDINetworkHost hostWithName:@"MyMacBook1"
                                              address:@"192.0.2.11"
                                                 port:5004];
MIDINetworkConnection *connection = [MIDINetworkConnection connectionWithHost:host];
(void)[[MIDINetworkSession defaultSession] addConnection:connection];

MIDINetworkHostのクラスメソッドであるhostWithNameは3種類あるが、ここではIPアドレスとポート番号を自前で設定するものを使っている。

Bonjourを使って接続相手を自動的に探す

Audio MIDI設定を使っていると、接続可能な相手を自動的に探して表示してくれる。相手のIPアドレスやポート番号を指定しなくても接続できるので非常に便利だ。

iPadにはAudio MIDI設定が無いので、アプリ側で同様の機能を実装することになる。

接続可能な相手を探すにはBonjourを使うことになるが、SDKにNSNetServiceBrowserクラスやNSNetServiceクラスが用意されているので、これを使えばよい。このあたりはiPhone/iPadネットワークプログラミング(4)Bonjourを使ってデータを送る (2) iPhone/iPadからMacヘ送信 - Void 〜tomの雑記〜が参考になる。検索するサービス名についてはMIDINetworkBonjourServiceTypeという定数が既に存在するので、それを使う。

サービスとIPアドレスとの紐付けができたら、NSNetServiceのオブジェクトを元にMIDINetworkHostのオブジェクトを生成できる。前項にて「hostWithNameは3種類ある」と書いたが、その中にNSNetServiceのオブジェクトを引数としてとるものがあるのだ。

MIDINetworkHostのオブジェクトを生成したら、来るべき接続開始に備えてオブジェクトを何らかのコレクションに保持しておくことになる。NSArrayやNSDictionaryなどを使用してもよいが、個人的にはMIDINetworkSessionのaddContactメソッドにてセッションオブジェクト内のコンタクトリスト(=コレクション)に保持するようにするべきだと思う。

// 指定したサービスのアドレス解決等が成功したらコールバックされる、
// NSNetServiceのデリゲート。
- (void)netServiceDidResolveAddress:(NSNetService *)sender {
  MIDINetworkHost *host = [MIDINetworkHost hostWithName:[sender name]
                                             netService:sender];

  // 同じホストを二重に追加してないか等の
  // チェックをしていないので注意!
  @synchronized(session) {
    (void)[session addContact:host];
  }
}

というのも、addContactの役割はAudio MIDI設定のMIDIネットワーク設定の画面中にある「ディレクトリ」に接続先を追加することに非常に似ているからだ。

例えばMIDINetworkSessionのconnectionPolicyプロパティを使うと外部からの接続を制限できるのだが、その設定の中に「コンタクトリスト中のホストからの接続のみ許可する」というものがある。これはAudio MIDI設定における「自分のディレクトリ内のコンピュータのみ」接続を許可するという機能と同じだ。

何よりMIDINetworkSessionのNotificationにMIDINetworkNotificationContactsDidChangeがあって、コンタクトリストに変化があると通知を受け取ることができる。なのでGUI側で接続可能な相手の増減に応じてリアルタイムに接続先リストを更新したい場合に便利だろう。