Java Sound APIは、MIDIデータ用のメッセージ・ルーティング・アーキテクチャを指定します。その仕組みを理解すれば、柔軟性が高く、使いやすいアーキテクチャであることがわかります。このシステムはモジュール接続設計に基づいています。つまり、特定のタスクを実行する個別のモジュールを相互に接続(ネットワーク接続)することにより、モジュール間でのデータのやりとりを可能にします。
Java Sound APIのメッセージ交換システム内の基本モジュールは、MidiDevice
(Java言語インタフェース)です。MidiDevices
にはシーケンサ(タイムスタンプが付いたMIDIメッセージのシーケンスを記録、再生、読み込み、編集する)、シンセサイザ(MIDIメッセージによりトリガーされるとサウンドを生成する)、およびMIDIの入力ポートおよび出力ポート(これらのポートを介して外部MIDIデバイスとデータがやり取りされる)が含まれます。MIDIポートに求められる一般的な機能については、基本インタフェースMidiDevice
に記述されています。Sequencer
およびSynthesizer
インタフェースは、MidiDevice
インタフェースを継承して、それぞれMIDIシーケンサおよびMIDIシンセサイザに特徴的な追加機能を記述します。シーケンサまたはシンセサイザとして機能する具象クラスは、これらのインタフェースを実装する必要があります。
MidiDevice
は、通常、Receiver
またはTransmitter
インタフェースを実装する1つまたは複数の補助オブジェクトを所有します。これらのインタフェースは、デバイスを相互に接続してデータのやり取りを可能にするための「プラグ」または「ポータル」を表します。あるMidiDevice
のTransmitter
を別のMidiDeviceのReceiver
に接続することにより、データをやり取りするモジュール・ネットワークを作成できます。
MidiDevice
インタフェースには、デバイスが同時にサポートできるトランスミッタ・オブジェクトおよびレシーバ・オブジェクトの数を判断するためのメソッド、およびこれらのオブジェクトにアクセスするためのメソッドが含まれています。MIDI出力ポートは通常、発信メッセージを受け取るReceiver
を少なくとも1つ保持しています。同様に、シンセサイザは通常、そのReceiver
(1つまたは複数)に送信されたメッセージに応答します。MIDI入力ポートは通常、着信メッセージを伝搬する
Transmitter
を少なくとも1つ保持しています。フル装備のシーケンサは、記録中にメッセージを受信するReceiver
、および再生中にメッセージを送信するTransmitter
の両方をサポートします。
Transmitter
インタフェースには、トランスミッタがそのMidiMessage
を送信するレシーバの設定および問合せを行うためのメソッドが含まれています。レシーバの設定により、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
を作成して、Receiverの
send
メソッドに引数として渡します。
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には新しい工夫もあります。標準MIDIファイル仕様と同様、シーケンスに格納されるMidiEvent
オブジェクトに対して(MIDIファイルから読み取れるように)タイミング値が提供されることは不思議ではありません。しかし、Java Sound APIでは、デバイス間で送信されるメッセージ(つまり、MIDIワイヤー・プロトコルに対応するメッセージ)にさえ、タイムスタンプとして知られるタイミング値を付けることが可能です。ここで問題にしているのは、これらのタイム・スタンプについてです。MidiEvent
オブジェクトのタイミング値については、第11章「MIDIシーケンスの再生、記録、および編集」で説明します。
Java Sound APIのデバイス間で送信されるメッセージにオプションで付加されるタイムスタンプは、標準MIDIファイルのタイミング値とはまったく異なります。MIDIファイルのタイミング値は、ビートやテンポなどの音楽上の概念に基づいており、各イベントのタイミングは、直前のイベントからの経過時間を測定します。対照的に、デバイスのReceiver
オブジェクトに送信されるメッセージ上のタイムスタンプは、常にマイクロ秒単位の絶対時間から求められます。具体的に説明すると、レシーバを保持するデバイスのオープンを起点とする経過時間(マイクロ秒数)が測定されます。
このようなタイムスタンプは、オペレーティング・システムまたはアプリケーション・プログラムが有する待ち時間の問題補正を支援するためにされました。これらのタイムスタンプは、タイミングに対する小さな修正に使用されるものであり、完全に任意の時点にイベントをスケジュールする複雑なキュー(MidiEvent
タイミング値で行うものなど)を実装するために使用するものではないことに留意してください。
デバイスにReceiver
を介して送信されるメッセージのタイムスタンプは、正確なタイミング情報をデバイスに提供します。メッセージを処理する際、デバイスはこの情報を使用します。たとえば、デバイスがイベントのタイミングを数ミリ秒単位で調整して、タイムスタンプ内の情報と合わせる場合があります。一方、すべてのデバイスがタイムスタンプをサポートしているわけではないため、デバイスがメッセージのタイムスタンプを完全に無視することもあります。
デバイスがタイムスタンプをサポートする場合であっても、要求した時間にイベントを正確にスケジュールできない場合もあります。メッセージのタイムスタンプがかなり遠い将来を示している場合は、それを送信したり、意図したとおりにデバイスに処理させることは期待できません。また、メッセージのタイムスタンプが過去のものである場合も、デバイスにメッセージを正確にスケジュールさせることはもちろん期待できません。遠い将来または過去のタイムスタンプを処理する方法は、デバイスに依存します。送信側には、遠すぎる将来であるとデバイスが判断する基準、またはデバイスでタイムスタンプの問題が発生したかどうかはわかりません。このように送信側が関知しない状態は、外部MIDIハードウェア・デバイスの動作に似ています。外部MIDIハードウェア・デバイスは、メッセージを送信しますが、それが正確に受信されたかどうかには関知しません。(MIDIワイヤー・プロトコルは、単一方向のプロトコルです。)
デバイスの中には、タイムスタンプ付きのメッセージをTransmitter
経由で送信するものもあります。たとえば、MIDI入力ポートから送信されるメッセージには、メッセージがそのポートに着信した時間が刻まれます。システムの中には、イベント処理メカニズムが原因で、その後のメッセージ処理中に一定の度合いでタイミングの精度が落ちることがあります。この場合でも、メッセージのタイムスタンプを利用して、元のタイミング情報を保存することができます。
デバイスがタイムスタンプをサポートするかどうかを確かめるには、次のMidiDevice
メソッドを呼び出します。
long getMicrosecondPosition()
デバイスがタイムスタンプを無視する場合、このメソッドは-1を返します。そうでない場合、デバイスが現在認識している時間を返します。送信者はこの値をオフセットとして使用して、その後に送信するメッセージのタイムスタンプを判定できます。たとえば、メッセージに5ミリ秒先のタイムスタンプを付けて送信する場合、デバイスの現在位置をマイクロ秒単位で取得し、その値に5000マイクロ秒を加えた値をタイムスタンプとして使用します。MidiDeviceの
時間の概念では、デバイスのオープン時が常に時間ゼロになることに留意してください。
ここで、タイムスタンプがどういうものかを踏まえた上で、Receiver
のsend
メソッドの説明に戻ります。
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
メソッドの二重呼出しを導入して、その結果をinPortTrans1
とinPortTrans2
に割り当てています。すでに説明したように、デバイスは複数のトランスミッタとレシーバを所有できます。指定されたデバイスに対してMidiDevice.getTransmitter()
が呼び出されるたびに、別のトランスミッタが返されます。この動作は利用可能なトランスミッタがなくなるまで続けられ、なくなった時点で例外がスローされます。
デバイスがサポートするトランスミッタおよびレシーバの数を確認するには、次のMidiDevice
メソッドを使用できます。
int getMaxTransmitters()
int getMaxReceivers
()
これらのメソッドは、現在利用可能な数ではなく、デバイスが所有する総数を返します。
トランスミッタがMIDIメッセージをレシーバに送信できるのは、一度に1つだけです。TransmitterのsetReceiver
メソッドを呼び出すたびに、既存のReceiver
(存在する場合)が新たに指定されたものに置き換えられます。トランスミッタが現在レシーバを保持しているかどうかはTransmitter.getReceiver
を呼び出すことで判断できます。ただし、デバイスが複数のトランスミッタを所有する場合、上記の入力ポートの例に示したように、各トランスミッタを異なるレシーバに接続することにより、データを一度に複数のデバイスに送信できます。
同様に、デバイスは複数のレシーバを使って、一度に複数のデバイスから受信できます。複数のレシーバで必要なコードも、上記の複数のトランスミッタを扱う場合のコードとほぼ同様で、簡単です。また、単一のレシーバが一度に複数のトランスミッタからメッセージを受信することも可能です。
接続が完了したら、取得した各トランスミッタおよびレシーバに対してclose
メソッドを呼び出して、リソースを解放できます。Transmitter
およびReceiver
インタフェースは、それぞれclose
メソッドを保持しています。Transmitter.setReceiver
を呼び出しても、トランスミッタの現在のレシーバはクローズされないことに留意してください。レシーバはオープンしたままの状態で、接続されているほかのすべてのトランスミッタからのメッセージを受信できます。
デバイスを完了した場合も、同様に、MidiDevice.close()
を呼び出すことにより、そのデバイスをほかのアプリケーション・プログラムに解放できます。デバイスをクローズすると、そのデバイスが所有するトランスミッタおよびレシーバがすべて自動的にクローズされます。