[先頭の項目] [前の項目] [次の項目] [最後の項目]

第 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 メッセージをアプリケーションプログラムからデバイスに直接送信するという、より単純な場合を考えてみます。この単純なシナリオを学ぶことで、2 つのデバイス間の MIDI メッセージのやり取りを Java Sound API がどのように調整するのか理解しやすくなります。

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

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 がスローされます。

トランスミッタを使用しないでメッセージを送信する簡単な具体例として、デフォルトのレシーバに Note On メッセージを送信する場合を考えます。このメッセージは、一般に 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 メソッドへの引数として指定された明示的な数値です。ゼロは、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
    }
	

実装によっては、単一のオブジェクトがデフォルトシーケンサとデフォルトシンセサイザの両方の機能を果たす場合もあります。言いかえると、実装が使用する 1 つのクラスが、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() を呼び出すことにより、そのデバイスを他のアプリケーションプログラムに解放できます。デバイスをクローズすると、そのデバイスが所有するトランスミッタおよびレシーバがすべて自動的にクローズされます。



[先頭の項目] [前の項目] [次の項目] [最後の項目]

Copyright © 2000, Sun Microsystems Inc. All rights reserved.