第 10 章: MIDI メッセージの送信および受信


 

デバイス、トランスミッタ、およびレシーバの理解

JavaTM Sound API は、MIDI データ用のメッセージルーティングアーキテクチャーを指定します。その仕組みを理解すれば、柔軟性が高く、使いやすいアーキテクチャーであることがわかります。システムは、モジュール接続設計を採用しています。この設計は、特定のタスクを実行する個別のモジュールを相互に接続 (ネットワーク接続) することにより、モジュール間でのデータのやりとりを可能にします。

Java Sound API のメッセージ交換システム内の基本モジュールは、MidiDevice (Java 言語インタフェース) です。MidiDevices にはシーケンサ (タイムスタンプが付いた MIDI メッセージのシーケンスを記録、再生、読み込み、編集する)、シンセサイザ (MIDI メッセージによりトリガーされるとサウンドを生成する)、および MIDI の入力ポートおよび出力ポート (これらのポートを介して外部 MIDI デバイスとデータがやり取りされる) が含まれます。MIDI ポートに求められる一般的な機能については、基本インタフェース MidiDevice に記述されています。Sequencer および Synthesizer インタフェースは、MidiDevice インタフェースを継承して、それぞれ MIDI シーケンサおよび MIDI シンセサイザに特徴的な追加機能を記述します。シーケンサまたはシンセサイザとして機能する具象クラスは、これらのインタフェースを実装する必要があります。

MidiDevice は、通常、Receiver または Transmitter インタフェースを実装する 1 つまたは複数の補助オブジェクトを所有します。これらのインタフェースは、デバイスを相互に接続してデータのやり取りを可能にするための「プラグ」または「ポータル」を表します。ある MidiDeviceTransmitter を別の MidiDeviceReceiver に接続することにより、データをやり取りするモジュールネットワークを作成できます。

MidiDevice インタフェースには、デバイスが同時にサポートできるトランスミッタオブジェクトおよびレシーバオブジェクトの数を判断するためのメソッド、およびこれらのオブジェクトにアクセスするためのメソッドが含まれます。MIDI 出力ポートは、通常、発信メッセージを受け取る、少なくとも 1 つの Receiver を保持しています。同様に、シンセサイザは、通常、その Receiver または Receivers に送信されたメッセージに応答します。MIDI 入力ポートは、通常、着信メッセージを伝搬する少なくとも 1 つの Transmitter を保持しています。フル装備のシーケンサは、記録中にメッセージを受信する Receivers、および再生中にメッセージを送信する Transmitters の両方をサポートします。

Transmitter インタフェースには、トランスミッタが MidiMessages を送信するレシーバの設定および問い合わせを行うためのメソッドが含まれます。レシーバの設定により、2 者間の接続が確立されます。Receiver インタフェースには、MidiMessage をレシーバに送信するメソッドが含まれます。このメソッドは通常、Transmitter によって呼び出されます。Transmitter インタフェースと Receiver インタフェースの両方に、close メソッドが含まれます。このメソッドは、以前に接続されたトランスミッタまたはレシーバを解放して別の接続から利用できるようにします。

次に、トランスミッタとレシーバの使用法について考察します。2 つのデバイスを接続する一般的な事例 (シーケンサをシンセサイザに接続する場合など) について考える前に、MIDI メッセージをアプリケーションプログラムからデバイスに直接送信するという、より単純な場合を考えてみます。この単純なシナリオを学ぶことで、Java Sound API が 2 つのデバイス間の MIDI メッセージのやり取りを調整する方法が理解しやすくなります。

トランスミッタを使わずにメッセージをレシーバに送信する方法

MIDI メッセージをゼロから作成して、特定のレシーバに送信する場合を考えましょう。空白の ShortMessage を新たに作成し、次の ShortMessage メソッドを使って MIDI データをそこに書き込みます。

void setMessage(int command, int channel, int data1,
         int data2) 
		 

メッセージの送信準備ができたら、次の Receiver メソッドを使って Receiver オブジェクトに送信します。

void send(MidiMessage message, long timeStamp)

タイムスタンプ引数については、このあとすぐ説明します。ここでの説明は、特に正確な時間を指定する必要がなければ、この値を -1 に設定できるということだけにとどめます。この例では、メッセージを受信するデバイスは、可能なかぎり迅速にメッセージに応答しようとします。

アプリケーションプログラムは、デバイスの getReceiver メソッドを呼び出すことにより、MidiDevice 用のレシーバを取得できます。すべてのデバイスのレシーバが使用中であることなどが原因で、デバイスがプログラムにレシーバを提供できない場合は、MidiUnavailableException がスローされます。デバイスがレシーバを提供できる場合、プログラムは、このメソッドが返したレシーバをすぐに利用できます。プログラムがレシーバの使用を完了したら、レシーバの close メソッドを呼び出す必要があります。プログラムが close を呼び出したあと、レシーバに対してメソッドの呼び出しを試みた場合は、IllegalStateException がスローされます。

トランスミッタを使わずにメッセージを送信する簡単な具体例として、デフォルトのレシーバに ノートオンメッセージを送信する場合を考えます。このメッセージは、一般に MIDI 出力ポートまたはシンセサイザなどのデバイスに関連付けられています。具体的には、次のように、適切な ShortMessage を作成して、Receiversend メソッドに引数として渡します。

  ShortMessage myMsg = new ShortMessage();
  // Start playing the note Middle C (60), 
  // moderately loud (velocity = 93).
  myMsg.setMessage(ShortMessage.NOTE_ON, 0, 60, 93);
  long timeStamp = -1;
  Receiver	 rcvr = MidiSystem.getReceiver();
  rcvr.send(myMsg, timeStamp);
  

このコードは ShortMessage の static 整数フィールドである NOTE_ON を、MIDI メッセージのステータスバイトとして使用します。MIDI メッセージのほかの部分は、setMessage メソッドへの引数として指定された明示的な数値です。0 は、MIDI チャネル番号 1 を使って音を再生することを示します。60 は音が Middle C であることを示します。93 はキーを押す際の任意のベロシティー値を示します。これは、通常は、音符を最終的に演奏するシンセサイザが、大きめのボリュームで再生することを意味します。MIDI 仕様では、ベロシティーの厳密な解釈は、現在の楽器のシンセサイザ実装に任せています。その後、この MIDI メッセージは、タイムスタンプ -1 でレシーバに送信されます。ここで、タイムスタンプパラメータの正確な意味を考える必要があります。この点を次の節で取り上げます。

タイムスタンプの理解

第 8 章「MIDI パッケージの概要」では、MIDI 仕様は内容が分かれていることを説明しました。MIDI ワイヤプロトコル (デバイス間でリアルタイムに送信されるメッセージ) が記述されている部分と、標準 MIDI ファイル (イベントとして「シーケンス」に保存されるメッセージ) が記述されている部分があります。後者の仕様では、標準 MIDI ファイルに格納される各イベントは、再生する時を示すタイミング値のタグが付いています。対照的に、MIDI ワイヤプロトコル内のメッセージは、デバイスが受信すると即座に処理されることが前提になっています。このため、タイミング値は付いていません。

Java Sound API には新しい工夫もあります。順序に格納される MidiEvent オブジェクトに対して、標準 MIDI ファイル仕様と同様、タイミング値が提供されるだけではありません。しかし、Java Sound API では、デバイス間で送信されるメッセージ (つまり、MIDI ワイヤプロトコルに対応するメッセージ) にさえ、「タイムスタンプ」として知られるタイミング値を付けることが可能です。ここで問題にしているのは、MIDI のワイヤプロトコルに対応するメッセージタイミング値についてです。MidiEvent オブジェクトのタイミング値については、第 11 章「MIDI シーケンスの再生、記録、および編集」で説明します。

デバイスに送信されるメッセージのタイムスタンプ

Java Sound API のデバイス間で送信されるメッセージにオプションで付加されるタイムスタンプは、標準 MIDI ファイルのタイミング値とはまったく異なります。MIDI ファイルのタイミング値は、ビートやテンポなどの音楽上の概念に基づいており、各イベントのタイミングは、直前のイベントからの経過時間を測定します。対照的に、デバイスの Receiver オブジェクトに送信されるメッセージ上のタイムスタンプは、常にマイクロ秒単位の絶対時間から求められます。具体的に説明すると、レシーバを保持するデバイスのオープンを起点とする経過時間 (マイクロ秒数) が測定されます。

このようなタイムスタンプは、オペレーティングシステムまたはアプリケーションプログラムが有する待ち時間の問題補正を支援する目的で設計されました。これらのタイムスタンプは、タイミングに対する小さな修正に使用されるものであり、完全に任意の時点にイベントをスケジュールする複雑なキュー (MidiEvent タイミング値で行うものなど) を実装するために使用するものではないことに留意してください。

デバイスにReceiver を介して送信されるメッセージのタイムスタンプは、正確なタイミング情報をデバイスに提供します。メッセージを処理する際、デバイスはこの情報を使用します。たとえば、デバイスはイベントのタイミングを数ミリ秒単位で調整して、タイムスタンプ内の情報と突き合わせることができます。一方、すべてのデバイスがタイムスタンプをサポートしているわけではない場合、デバイスがメッセージのタイムスタンプを完全に無視することも可能です。

デバイスがタイムスタンプをサポートする場合であっても、要求した時間にイベントを正確にスケジュールできない場合もあります。メッセージのタイムスタンプがかなり遠い将来を示している場合は、それを送信したり、意図したとおりにデバイスに処理させることは期待できません。また、メッセージのタイムスタンプが過去のものである場合も、デバイスにメッセージを正確にスケジュールさせることは期待できません。遠い将来または過去のタイムスタンプを処理する方法は、デバイスに依存します。送信側には、遠すぎる将来であるとデバイスが判断する基準、またはタイムスタンプに問題が発生したことがあるかどうかはわかりません。このように送信側が関知しない状態は、外部 MIDI ハードウェアデバイスの動作に似ています。外部 MIDI ハードウェアデバイスは、メッセージを送信しますが、それが正確に受信されたかどうかには関知しません。MIDI ワイヤプロトコルは、単一方向のプロトコルです。

デバイスの中には、タイムスタンプ付きのメッセージを Transmitter 経由で送信するものもあります。たとえば、MIDI 入力ポートから送信されるメッセージには、メッセージがそのポートに着信した時間が刻まれます。システムの中には、イベント処理機構が原因で、その後のメッセージ処理中に一定の度合いでタイミングの精度が落ちることがあります。この場合でも、メッセージのタイムスタンプを利用して、元のタイミング情報を保存することができます。

デバイスがタイムスタンプをサポートするかどうかを確かめるには、次の MidiDevice メソッドを呼び出します。

    long getMicrosecondPosition()

デバイスがタイムスタンプを無視する場合、このメソッドは -1 を返します。タイムスタンプを無視しない場合、デバイスが現在認識している時間を返します。送信者はこの値をオフセットとして使用して、その後に送信するメッセージのタイムスタンプを判定できます。たとえば、メッセージに 5 ミリ秒先のタイムスタンプを付けて送信する場合、デバイスの現在位置をマイクロ秒単位で取得し、その値に 5000 マイクロ秒を加えた値をタイムスタンプとして使用します。MidiDevice の時間の概念は、デバイスのオープン時が常に時間ゼロになることに留意してください。

ここで、タイムスタンプがどういうものかを踏まえた上で、Receiversend メソッドの説明に戻ります。

void send(MidiMessage message, long timeStamp)

引数 timeStamp は、受信側デバイスの時間の概念に従って、マイクロ秒で表されます。デバイスがタイムスタンプをサポートしない場合、timeStamp 引数は単にデバイスに無視されます。この場合、受信側に送信するメッセージにタイムスタンプを付ける必要はありません。引数 timeStamp に -1 を指定すると、正確なタイミング調整を行う必要がないことを示すことができます。これは、メッセージの受信後できるだけ早く処理する条件で、受信側デバイスに処理を任せることを意味します。ただし、同一の受信者にメッセージを送信する際、-1 にメッセージを付けて送信したり、明示的なタイムスタンプにほかのメッセージを付けて送信することはお勧めできません。このようなことを行うと、結果のタイミングに狂いを生じる場合があります。

トランスミッタのレシーバへの接続

これまで、トランスミッタを使わずに MIDI メッセージを直接レシーバに送信する方法を説明してきました。ここで、より一般的なケースを考えてみます。それは、MIDI メッセージをゼロから作成するのではなく、複数のデバイスを単純に接続して、その中のデバイスからほかのデバイスへ MIDI メッセージを送信する場合です。

単一のデバイスの接続

最初の例として、シーケンサをシンセサイザに接続するケースを取り上げます。接続完了後にシーケンサの実行を開始すると、シンセサイザが、シーケンサの現在のイベントシーケンスを使ってオーディオを生成します。ここでは、MIDI ファイルからシーケンサにシーケンスを読み込むプロセスは説明しません。また、シーケンスを再生するメカニズムについてもここでは触れません。シーケンスの読み込みおよび再生については、第 11 章「MIDI シーケンスの再生、記録、および編集」で説明します。楽器をシンセサイザに読み込む方法については、第 12 章「サウンドの合成」で説明します。ここでは、シーケンサとシンセサイザ間で接続を確立する方法に焦点を絞って説明します。ここでの説明は、あるデバイスのトランスミッタと別のデバイスのレシーバとを接続する場合にも応用できます。

簡略を期すために、ここではデフォルトのシーケンサおよびデフォルトのシンセサイザを使用します。デフォルトのデバイスについて、およびデフォルト以外のデバイスへのアクセス方法については、第 9 章「MIDI システムリソースへのアクセス」を参照してください。

    Sequencer    	seq;
    Transmitter  	seqTrans;
    Synthesizer  	synth;
    Receiver	     synthRcvr;
    try {
          seq	  = MidiSystem.getSequencer();
          seqTrans = seq.getTransmitter();
          synth	  = MidiSystem.getSynthesizer();
          synthRcvr = synth.getReceiver(); 
          seqTrans.setReceiver(synthRcvr);	
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }
	

実装によっては、単一のオブジェクトがデフォルトシーケンサとデフォルトシンセサイザの両方の機能を果たす場合もあります。つまり、実装で、Sequencer インタフェースと Synthesizer インタフェースの両方を実装するクラスを使用することがあります。その場合、上記のコードで示したような明示的な接続を確立することは、通常必要ありません。ただし、移植性の観点から、このような構成を前提にしないほうが安全です。必要に応じて、次の方法でこのような実装が存在するかどうかを確認することもできます。

if (seq instanceof Synthesizer)

ただし、上に示した明示的な接続は、すべての場合に有効です。

複数のデバイスへの接続

前のコード例では、トランスミッタとレシーバ間の 1 対 1 の接続を示しました。次に、同じ MIDI メッセージを複数のレシーバに送信する必要がある場合について考えます。たとえば、外部デバイスから MIDI データを取り込んで、内部シンセサイザを作動させ、同時にデータを決まった順序で記録する場合などがこれに該当します。この接続形態は、「ファンアウト」または「スプリッタ」とも呼ばれる簡単な接続です。次の文は、ファンアウト接続の作成方法を示しています。ファンアウト接続を介して、MIDI 入力ポートに着信する MIDI メッセージは、Synthesizer オブジェクトと Sequencer オブジェクトの両方に送信されます。入力ポート、シーケンサ、シンセサイザの 3 つのデバイスの取得およびオープンは完了しているものとします。入力ポートを取得するには、MidiSystem.getMidiDeviceInfo が返す項目すべてに対し、繰り返し実行する必要があります。

    Synthesizer  synth;
    Sequencer    seq;
    MidiDevice   inputPort;
    // [obtain and open the three devices...]
    Transmitter	  inPortTrans1, inPortTrans2;
    Receiver     	synthRcvr;
    Receiver     	seqRcvr;
    try {
          inPortTrans1 = inputPort.getTransmitter();
          synthRcvr = synth.getReceiver(); 
          inPortTrans1.setReceiver(synthRcvr);
          inPortTrans2 = inputPort.getTransmitter();
          seqRcvr = seq.getReceiver(); 
          inPortTrans2.setReceiver(seqRcvr);
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }
	

このコードは、MidiDevice.getTransmitter メソッドの二重呼び出しを導入して、その結果を inPortTrans1inPortTrans2 に割り当てています。すでに説明したように、デバイスは複数のトランスミッタとレシーバを所有できます。指定されたデバイスに対して MidiDevice.getTransmitter() が呼び出されるたびに、別のトランスミッタが返されます。この動作は利用可能なトランスミッタがなくなるまで続けられ、なくなった時点で例外がスローされます。

デバイスがサポートするトランスミッタおよびレシーバの数を確認するには、次の MidiDevice メソッドを使用できます。

    int getMaxTransmitters()
    int getMaxReceivers()
	

これらのメソッドは、現在利用可能な数ではなく、デバイスが所有する総数を返します。

トランスミッタが MIDI メッセージをレシーバに送信できるのは、一度に 1 つだけです。TransmittersetReceiver メソッドを呼び出すたびに、既存の Receiver (存在する場合) が新たに指定された Receiver に置き換えられます。トランスミッタが現在レシーバを保持しているかどうかは Transmitter.getReceiver を呼び出すことで判断できます。ただし、デバイスが複数のトランスミッタを所有する場合、上記の入力ポートの例に示したように、各トランスミッタを異なるレシーバに接続することにより、データを一度に複数のデバイスに送信できます。

同様に、デバイスは複数のレシーバを使って、一度に複数のデバイスから受信できます。複数のレシーバで必要なコードも、上記の複数のトランスミッタを扱う場合のコードとほぼ同様で、簡単です。また、単一のレシーバが一度に複数のトランスミッタからメッセージを受信することも可能です。

接続のクローズ

接続が完了したら、取得した各トランスミッタおよびレシーバに対して close メソッドを呼び出して、リソースを解放します。Transmitter および Receiver インタフェースは、それぞれ close メソッドを保持しています。Transmitter.setReceiver を呼び出すことにより、トランスミッタの最新のレシーバはクローズされないことに留意してください。最新のレシーバはオープンしたままの状態で、接続されているほかのすべてのトランスミッタからのメッセージを受信できます。

デバイスを完了した場合も、同様に、MidiDevice.close() を呼び出すことにより、そのデバイスをほかのアプリケーションプログラムに解放できます。デバイスをクローズすると、そのデバイスが所有するトランスミッタおよびレシーバがすべて自動的にクローズされます。