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 MidiDevice
(a Java language
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 base
MidiDevice
interface. The Sequencer
and
Synthesizer
interfaces extend the
MidiDevice
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 the
Receiver
or Transmitter
interfaces. These
interfaces represent the "plugs" or "portals" that connect devices
together, permitting data to flow into and out of them. By
connecting a Transmitter
of one
MidiDevice
to a Receiver
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
one Receiver
through which the outgoing messages may
be received; similarly, a synthesizer normally responds to messages
sent to its Receiver
or Receivers
. A MIDI
input port normally has at least one Transmitter
,
which propagates the incoming messages. A full-featured sequencer
supports both Receivers
, which receive messages during
recording, and Transmitters
, which send messages
during playback.
The Transmitter
interface
includes methods for setting and querying the receivers to which
the transmitter sends its MidiMessages
. Setting the
receiver establishes the connection between the two. The
Receiver
interface contains a method that sends a
MidiMessage
to the receiver. Typically, this method is
invoked by a Transmitter
. Both the
Transmitter
and Receiver
interfaces
include a close
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.
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 following ShortMessage
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 this
Receiver
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's
getReceiver
method. If the device can't provide a
receiver to the program (typically because all the device's
receivers are already in use), a
MidiUnavailableException
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's close
method. If the
program attempts to invoke methods on a receiver after calling
close
, an IllegalStateException
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 to Receiver'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 the
setMessage
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.
Chapter 8, "Overview of the MIDI Package," explained that 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 devices—in other words, the messages that correspond
to MIDI wire protocol—can be given timing values, known as
time stamps. It is these time stamps that concern us here.
(The timing values in MidiEvent
objects are discussed
in detail in Chapter 11, "Playing,
Recording, and Editing MIDI Sequences.")
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 of Receiver
:
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 the timeStamp
argument. You aren't
required to time-stamp the messages you send to a receiver. You can
use -1 for the timeStamp
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.
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.
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 Chapter 11, "Playing, Recording, and Editing MIDI Sequences." Loading instruments into the synthesizer is discussed in Chapter 12, "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. (See Chapter 9, "Accessing MIDI System Resources," for more about default devices and how to access non-default devices.)
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
the Synthesizer
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.
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 a Sequencer
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 by
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 }
This code introduces a dual invocation of
the MidiDevice.getTransmitter
method, assigning the
results to inPortTrans1
and inPortTrans2
.
As mentioned earlier, a device can own multiple transmitters and
receivers. Each time MidiDevice.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()
int getMaxReceivers
()
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 existing
Receiver
, if any, is replaced by the newly specified
one. You can tell whether the transmitter currently has a receiver
by invoking Transmitter.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.
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. The
Transmitter
and Receiver
interfaces each
have a close
method. Note that invoking
Transmitter.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.