Understanding Devices, Transmitters, and Receivers
The Java Sound API specifies a message-routing architecture for MIDI data that's flexible and easy to use, once you understand how it works. The system is based on a module-connection design: distinct modules, each of which performs a specific task, can be interconnected (networked), enabling data to flow from one module to another.The base module in the Java Sound API's messaging system is the
MidiDevice
interface.MidiDevices
include sequencers (which record, play, load, and edit sequences of time-stamped MIDI messages), synthesizers (which generate sounds when triggered by MIDI messages), and MIDI input and output ports, through which data comes from and goes to external MIDI devices. The functionality typically required of MIDI ports is described by the baseMidiDevice
interface. TheSequencer
andSynthesizer
interfaces extend theMidiDevice
interface to describe the additional functionality characteristic of MIDI sequencers and synthesizers, respectively. Concrete classes that function as sequencers or synthesizers should implement these interfaces.A
MidiDevice
typically owns one or more ancillary objects that implement theReceiver
orTransmitter
interfaces. These interfaces represent the "plugs" or "portals" that connect devices together, permitting data to flow into and out of them. By connecting aTransmitter
of oneMidiDevice
to aReceiver
of another, you can create a network of modules in which data flows from one to another.The
MidiDevice
interface includes methods for determining how many transmitter and receiver objects the device can support concurrently, and other methods for accessing those objects. A MIDI output port normally has at least oneReceiver
through which the outgoing messages may be received; similarly, a synthesizer normally responds to messages sent to itsReceiver
orReceivers
. A MIDI input port normally has at least oneTransmitter
, which propagates the incoming messages. A full-featured sequencer supports bothReceivers
, which receive messages during recording, andTransmitters
, which send messages during playback.The
Transmitter
interface includes methods for setting and querying the receivers to which the transmitter sends itsMidiMessages
. Setting the receiver establishes the connection between the two. TheReceiver
interface contains a method that sends aMidiMessage
to the receiver. Typically, this method is invoked by aTransmitter
. Both theTransmitter
andReceiver
interfaces include aclose
method that frees up a previously connected transmitter or receiver, making it available for a different connection.We'll now examine how to use transmitters and receivers. Before getting to the typical case of connecting two devices (such as hooking a sequencer to a synthesizer), we'll examine the simpler case where you send a MIDI message directly from your application program to a device. Studying this simple scenario should make it easier to understand how the Java Sound API arranges for sending MIDI messages between two devices.
Sending a Message to a Receiver without Using a Transmitter
Let's say you want to create a MIDI message from scratch and then send it to some receiver. You can create a new, blank
ShortMessage
and then fill it with MIDI data using the followingShortMessage
method:void setMessage(int command, int channel, int data1, int data2)Once you have a message ready to send, you can send it to a
Receiver
object, using thisReceiver
method:void send(MidiMessage message, long timeStamp)The time-stamp argument will be explained momentarily. For now, we'll just mention that its value can be set to -1 if you don't care about specifying a precise time. In this case, the device receiving the message will try to respond to the message as soon as possible.
An application program can obtain a receiver for a
MidiDevice
by invoking the device'sgetReceiver
method. If the device can't provide a receiver to the program (typically because all the device's receivers are already in use), aMidiUnavailableException
is thrown. Otherwise, the receiver returned from this method is available for immediate use by the program. When the program has finished using the receiver, it should call the receiver'sclose
method. If the program attempts to invoke methods on a receiver after callingclose
, anIllegalStateException
may be thrown.As a concrete simple example of sending a message without using a transmitter, let's send a Note On message to the default receiver, which is typically associated with a device such as the MIDI output port or a synthesizer. We do this by creating a suitable
ShortMessage
and passing it as an argument toReceiver's
send
method: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);This code uses a static integer field of
ShortMessage
, namely,NOTE_ON
, for use as the MIDI message's status byte. The other parts of the MIDI message are given explicit numeric values as arguments to thesetMessage
method. The zero indicates that the note is to be played using MIDI channel number 1; the 60 indicates the note Middle C; and the 93 is an arbitrary key-down velocity value, which typically indicates that the synthesizer that eventually plays the note should play it somewhat loudly. (The MIDI specification leaves the exact interpretation of velocity up to the synthesizer's implementation of its current instrument.) This MIDI message is then sent to the receiver with a time stamp of -1. We now need to examine exactly what the time stamp parameter means, which is the subject of the next section.Understanding Time Stamps
As you already know, the MIDI specification has different parts. One part describes MIDI "wire" protocol (messages sent between devices in real time), and another part describes Standard MIDI Files (messages stored as events in "sequences"). In the latter part of the specification, each event stored in a standard MIDI file is tagged with a timing value that indicates when that event should be played. By contrast, messages in MIDI wire protocol are always supposed to be processed immediately, as soon as they're received by a device, so they have no accompanying timing values.
The Java Sound API adds an additional twist. It comes as no surprise that timing values are present in the
MidiEvent
objects that are stored in sequences (as might be read from a MIDI file), just as in the Standard MIDI Files specification. But in the Java Sound API, even the messages sent between devicesin other words, the messages that correspond to MIDI wire protocolcan be given timing values, known as time stamps. It is these time stamps that concern us here.Time Stamps on Messages Sent to Devices
The time stamp that can optionally accompany messages sent between devices in the Java Sound API is quite different from the timing values in a standard MIDI file. The timing values in a MIDI file are often based on musical concepts such as beats and tempo, and each event's timing measures the time elapsed since the previous event. In contrast, the time stamp on a message sent to a device's
Receiver
object always measures absolute time in microseconds. Specifically, it measures the number of microseconds elapsed since the device that owns the receiver was opened.This kind of time stamp is designed to help compensate for latencies introduced by the operating system or by the application program. It's important to realize that these time stamps are used for minor adjustments to timing, not to implement complex queues that can schedule events at completely arbitrary times (as
MidiEvent
timing values do).The time stamp on a message sent to a device (through a
Receiver
) can provide precise timing information to the device. The device might use this information when it processes the message. For example, it might adjust the event's timing by a few milliseconds to match the information in the time stamp. On the other hand, not all devices support time stamps, so the device might completely ignore the message's time stamp.Even if a device supports time stamps, it might not schedule the event for exactly the time that you requested. You can't expect to send a message whose time stamp is very far in the future and have the device handle it as you intended, and you certainly can't expect a device to correctly schedule a message whose time stamp is in the past! It's up to the device to decide how to handle time stamps that are too far off in the future or are in the past. The sender doesn't know what the device considers to be too far off, or whether the device had any problem with the time stamp. This ignorance mimics the behavior of external MIDI hardware devices, which send messages without ever knowing whether they were received correctly. (MIDI wire protocol is unidirectional.)
Some devices send time-stamped messages (via a
Transmitter
). For example, the messages sent by a MIDI input port might be stamped with the time the incoming message arrived at the port. On some systems, the event-handling mechanisms cause a certain amount of timing precision to be lost during subsequent processing of the message. The message's time stamp allows the original timing information to be preserved.To learn whether a device supports time stamps, invoke the following method of
MidiDevice
:long getMicrosecondPosition()This method returns -1 if the device ignores time stamps. Otherwise, it returns the device's current notion of time, which you as the sender can use as an offset when determining the time stamps for messages you subsequently send. For example, if you want to send a message with a time stamp for five milliseconds in the future, you can get the device's current position in microseconds, add 5000 microseconds, and use that as the time stamp. Keep in mind that the
MidiDevice's
notion of time always places time zero at the time the device was opened.Now, with all that explanation of time stamps as a background, let's return to the
send
method ofReceiver
:void send(MidiMessage message, long timeStamp)The
timeStamp
argument is expressed in microseconds, according to the receiving device's notion of time. If the device doesn't support time stamps, it simply ignores thetimeStamp
argument. You aren't required to time-stamp the messages you send to a receiver. You can use -1 for thetimeStamp
argument to indicate that you don't care about adjusting the exact timing; you're just leaving it up to the receiving device to process the message as soon as it can. However, it's not advisable to send -1 with some messages and explicit time stamps with other messages sent to the same receiver. Doing so is likely to cause irregularities in the resultant timing.Connecting Transmitters to Receivers
We've seen how you can send a MIDI message directly to a receiver, without using a transmitter. Now let's look at the more common case, where you aren't creating MIDI messages from scratch, but are simply connecting devices together so that one of them can send MIDI messages to the other.
Connecting to a Single Device
The specific case we'll take as our first example is connecting a sequencer to a synthesizer. After this connection is made, starting the sequencer running will cause the synthesizer to generate audio from the events in the sequencer's current sequence. For now, we'll ignore the process of loading a sequence from a MIDI file into the sequencer. Also, we won't go into the mechanism of playing the sequence. Loading and playing sequences is discussed in detail in Playing, Recording, and Editing MIDI Sequences. Loading instruments into the synthesizer is discussed in Synthesizing Sound. For now, all we're interested in is how to make the connection between the sequencer and the synthesizer. This will serve as an illustration of the more general process of connecting one device's transmitter to another device's receiver.
For simplicity, we'll use the default sequencer and the default synthesizer.
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 }An implementation might actually have a single object that serves as both the default sequencer and the default synthesizer. In other words, the implementation might use a class that implements both the
Sequencer
interface and theSynthesizer
interface. In that case, it probably wouldn't be necessary to make the explicit connection that we did in the code above. For portability, though, it's safer not to assume such a configuration. If desired, you can test for this condition, of course:if (seq instanceof Synthesizer)although the explicit connection above should work in any case.
Connecting to More than One Device
The previous code example illustrated a one-to-one connection between a transmitter and a receiver. But, what if you need to send the same MIDI message to multiple receivers? For example, suppose you want to capture MIDI data from an external device to drive the internal synthesizer while simultaneously recording the data to a sequence. This form of connection, sometimes referred to as "fan out" or as a "splitter," is straightforward. The following statements show how to create a fan-out connection, through which the MIDI messages arriving at the MIDI input port are sent to both a
Synthesizer
object and aSequencer
object. We assume you've already obtained and opened the three devices: the input port, sequencer, and synthesizer. (To obtain the input port, you'll need to iterate over all the items returned byMidiSystem.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 }This code introduces a dual invocation of the
MidiDevice.getTransmitter
method, assigning the results toinPortTrans1
andinPortTrans2
. As mentioned earlier, a device can own multiple transmitters and receivers. Each timeMidiDevice.getTransmitter()
is invoked for a given device, another transmitter is returned, until no more are available, at which time an exception will be thrown.To learn how many transmitters and receivers a device supports, you can use the following
MidiDevice
method:int getMaxTransmitters() intgetMaxReceivers
()These methods return the total number owned by the device, not the number currently available.
A transmitter can transmit MIDI messages to only one receiver at a time. (Every time you call
Transmitter's setReceiver
method, the existingReceiver
, if any, is replaced by the newly specified one. You can tell whether the transmitter currently has a receiver by invokingTransmitter.getReceiver
.) However, if a device has multiple transmitters, it can send data to more than one device at a time, by connecting each transmitter to a different receiver, as we saw in the case of the input port above.Similarly, a device can use its multiple receivers to receive from more than one device at a time. The multiple-receiver code that's required is straightforward, being directly analogous to the multiple-transmitter code above. It's also possible for a single receiver to receive messages from more than one transmitter at a time.
Closing Connections
Once you're done with a connection, you can free up its resources by invoking the
close
method for each transmitter and receiver that you've obtained. TheTransmitter
andReceiver
interfaces each have aclose
method. Note that invokingTransmitter.setReceiver
doesn't close the transmitter's current receiver. The receiver is left open, and it can still receive messages from any other transmitter that's connected to it.If you're also done with the devices, you can similarly make them available to other application programs by invoking
MidiDevice.close()
. Closing a device automatically closes all its transmitters and receivers.