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.