NoteWith the 5.0 release, there is a new real-time
|
In the world of MIDI, a
sequencer is any hardware or software device that can
precisely play or record a sequence of time-stamped MIDI
messages. Similarly, in the Java Sound API, the
Sequencer
abstract interface defines the properties of
an object that can play and record Sequences
of
MidiEvent
objects. A Sequencer
typically
loads these MidiEvent
sequences from a standard MIDI
file or saves them to such a file. Sequences can also be edited.
This chapter explains how to use Sequencer
objects,
along with related classes and interfaces, to accomplish such
tasks.
To develop an intuitive
understanding of what a Sequencer
is, think of it by
analogy with a tape recorder, which a sequencer resembles in many
respects. Whereas a tape recorder plays audio, a sequencer plays
MIDI data. A sequence is a multi-track, linear, time-ordered
recording of MIDI musical data, which a sequencer can play at
various speeds, rewind, shuttle to particular points, record into,
or copy to a file for storage.
Chapter 10, "Transmitting and Receiving MIDI Messages,"
explained that devices typically have Receiver
objects, Transmitter
objects, or both. To
play music, a device generally receives
MidiMessages
through a Receiver
, which in
turn has usually received them from a Transmitter
that
belongs to a Sequencer
. The device that owns this
Receiver
might be a Synthesizer
, which
will generate audio directly, or it might be a MIDI output port,
which transmits MIDI data through a physical cable to some external
piece of equipment. Similarly, to record music, a series
of time-stamped MidiMessages
are generally sent to a
Receiver
owned by a Sequencer
, which
places them in a Sequence
object. Typically the object
sending the messages is a Transmitter
associated with
a hardware input port, and the port relays MIDI data that it gets
from an external instrument. However, the device responsible for
sending the messages might instead be some other
Sequencer
, or any other device that has a
Transmitter
. Furthermore, as mentioned in Chapter 10,
a program can send messages without using any
Transmitter
at all.
A Sequencer
itself has both Receivers
and
Transmitters
. When it's recording, it actually obtains
MidiMessages
via its Receivers
. During
playback, it uses its Transmitters
to send
MidiMessages
that are stored in the
Sequence
that it has recorded (or loaded from a
file).
One way to think of the role
of a Sequencer
in the Java Sound API is as an
aggregator and "de-aggregator" of MidiMessages
. A
series of separate MidiMessages
, each of which is
independent, is sent to the Sequencer
along with its
own time stamp that marks the timing of a musical event. These
MidiMessages
are encapsulated in
MidiEvent
objects and collected in
Sequence
objects through the action of the
Sequencer.record
method. A Sequence
is a
data structure containing aggregates of MidiEvents
,
and it usually represents a series of musical notes, often an
entire song or composition. On playback, the Sequencer
again extracts the MidiMessages
from the
MidiEvent
objects in the Sequence
and
then transmits them to one or more devices that will either render
them into sound, save them, modify them, or pass them on to some
other device.
Some sequencers might have
neither transmitters nor receivers. For example, they might create
MidiEvents
from scratch as a result of keyboard or
mouse events, instead of receiving MidiMessages
through Receivers
. Similarly, they might play music by
communicating directly with an internal synthesizer (which could
actually be the same object as the sequencer) instead of sending
MidiMessages
to a Receiver
associated
with a separate object. However, the rest of this chapter assumes
the normal case of a sequencer that uses Receivers
and
Transmitters
.
It's possible for an
application program to send MIDI messages directly to a device,
without using a sequencer, as was described in Chapter 10,
"Transmitting and Receiving MIDI
Messages." The program simply invokes the
Receiver.send
method each time it wants to send a
message. This is a straightforward approach that's useful when the
program itself creates the messages in real time. For example,
consider a program that lets the user play notes by clicking on an
onscreen piano keyboard. When the program gets a mouse-down event,
it immediately sends the appropriate Note On message to the
synthesizer.
As mentioned in Chapter 10,
the program can include a time stamp with each MIDI message it
sends to the device's receiver. However, such time stamps are used
only for fine-tuning the timing, to correct for processing latency.
The caller can't generally set arbitrary time stamps; the time
value passed to Receiver.send
must be close to the
present time, or the receiving device might not be able to schedule
the message correctly. This means that if an application program
wanted to create a queue of MIDI messages for an entire piece of
music ahead of time (instead of creating each message in response
to a real-time event), it would have to be very careful to schedule
each invocation of Receiver.send
for nearly the right
time.
Fortunately, most
application programs don't have to be concerned with such
scheduling. Instead of invoking Receiver.send
itself,
a program can use a Sequencer
object to manage the
queue of MIDI messages for it. The sequencer takes care of
scheduling and sending the messages—in other words, playing
the music with the correct timing. Generally, it's advantageous to
use a sequencer whenever you need to convert a non-real-time series
of MIDI messages to a real-time series (as in playback), or vice
versa (as in recording). Sequencers are most commonly used for
playing data from MIDI files and for recording data from a MIDI
input port.
Before examining the
Sequencer
API, it helps to understand the kind of data
that's stored in a sequence.
In the Java Sound API,
sequencers closely follow the Standard MIDI Files specification in
the way that they organize recorded MIDI data. As mentioned above,
a Sequence
is an aggregation of
MidiEvents
, organized in time. But there is more
structure to a Sequence
than just a linear series of
MidiEvents
: a Sequence
actually contains
global timing information plus a collection of Tracks
,
and it is the Tracks
themselves that hold the
MidiEvent
data. So the data played by a sequencer
consists of a three-level hierarchy of objects:
Sequencer
, Track
, and
MidiEvent
.
In the conventional use of
these objects, the Sequence
represents a complete
musical composition or section of a composition, with each
Track
corresponding to a voice or player in the
ensemble. In this model, all the data on a particular
Track
would also therefore be encoded into a
particular MIDI channel reserved for that voice or player.
This way of organizing data
is convenient for purposes of editing sequences, but note that this
is just a conventional way to use Tracks
. There is
nothing in the definition of the Track
class per se
that keeps it from containing a mix of MidiEvents
on
different MIDI channels. For example, an entire multi-channel MIDI
composition can be mixed and recorded onto one Track
.
Also, standard MIDI files of Type 0 (as opposed to Type 1 and Type
2) contain by definition only one track; so a Sequence
that's read from such a file will necessarily have a single
Track
object.
As discussed in Chapter 8,
"Overview of the MIDI Package," the
Java Sound API includes MidiMessage
objects that
correspond to the raw two- or three-byte sequences that make up
most standard MIDI messages. A MidiEvent
is simply a
packaging of a MidiMessage
along with an accompanying
timing value that specifies when the event occurs. (We might then
say that a sequence really consists of a four- or five-level
hierarchy of data, rather than three-level, because the ostensible
lowest level, MidiEvent
, actually contains a
lower-level MidiMessage
, and likewise the
MidiMessage
object contains an array of bytes that
comprises a standard MIDI message.)
In the Java Sound API, there
are two different ways in which MidiMessages
can be
associated with timing values. One is the way mentioned above under
"When to Use a Sequencer." This
technique is described in detail under "Sending a Message to a Receiver without
Using a Transmitter" and "Understanding Time Stamps"
in Chapter 10, "Transmitting and Receiving
MIDI Messages." There, we saw that the send
method
of Receiver
takes a MidiMessage
argument
and a time-stamp argument. That kind of time stamp can only be
expressed in microseconds.
The other way in which a
MidiMessage
can have its timing specified is by being
encapsulated in a MidiEvent
. In this case, the timing
is expressed in slightly more abstract units called
ticks.
What is the duration of a tick? It can vary between sequences (but not within a sequence), and its value is stored in the header of a standard MIDI file. The size of a tick is given in one of two types of units:
On the other hand, in the case of SMPTE, the units measure absolute time, and the notion of tempo is inapplicable. There are actually four different SMPTE conventions available, which refer to the number of motion-picture frames per second. The number of frames per second can be 24, 25, 29.97, or 30. With SMPTE time code, the size of a tick is expressed as a fraction of a frame.
In the Java Sound API, you
can invoke Sequence.getDivisionType
to learn which
type of unit—namely, PPQ or one of the SMPTE units—is
used in a particular sequence. You can then calculate the size of a
tick after invoking Sequence.getResolution
. The latter
method returns the number of ticks per quarter note if the division
type is PPQ, or per SMPTE frame if the division type is one of the
SMPTE conventions. You can get the size of a tick using this
formula in the case of PPQ:
ticksPerSecond = resolution * (currentTempoInBeatsPerMinute / 60.0); tickSize = 1.0 / ticksPerSecond;
and this formula in the case of SMPTE:
framesPerSecond = (divisionType == Sequence.SMPTE_24 ? 24 : (divisionType == Sequence.SMPTE_25 ? 25 : (divisionType == Sequence.SMPTE_30 ? 30 : (divisionType == Sequence.SMPTE_30DROP ?
29.97)))); ticksPerSecond = resolution * framesPerSecond; tickSize = 1.0 / ticksPerSecond;
The Java Sound API's
definition of timing in a sequence mirrors that of the Standard
MIDI Files specification. However, there's one important
difference. The tick values contained in MidiEvents
measure cumulative time, rather than delta time.
In a standard MIDI file, each event's timing information measures
the amount of time elapsed since the onset of the previous event in
the sequence. This is called delta time. But in the Java Sound API,
the ticks aren't delta values; they're the previous event's time
value plus the delta value. In other words, in the Java
Sound API the timing value for each event is always greater than
that of the previous event in the sequence (or equal, if the events
are supposed to be simultaneous). Each event's timing value
measures the time elapsed since the beginning of the sequence.
To summarize, the Java Sound
API expresses timing information in either MIDI ticks or
microseconds. MidiEvents
store timing information in
terms of MIDI ticks. The duration of a tick can be calculated from
the Sequence's
global timing information and, if the
sequence uses tempo-based timing, the current musical tempo. The
time stamp associated with a MidiMessage
sent to a
Receiver
, on the other hand, is always expressed in
microseconds.
One goal of this design is
to avoid conflicting notions of time. It's the job of a
Sequencer
to interpret the time units in its
MidiEvents
, which might have PPQ units, and translate
these into absolute time in microseconds, taking the current tempo
into account. The sequencer must also express the microseconds
relative to the time when the device receiving the message was
opened. Note that a sequencer can have multiple transmitters, each
delivering messages to a different receiver that might be
associated with a completely different device. You can see, then,
that the sequencer has to be able to perform multiple translations
at the same time, making sure that each device receives time stamps
appropriate for its notion of time.
To make matters more complicated, different devices might update their notions of time based on different sources (such as the operating system's clock, or a clock maintained by a sound card). This means that their timings can drift relative to the sequencer's. To keep in synchronization with the sequencer, some devices permit themselves to be "slaves" to the sequencer's notion of time. Setting masters and slaves is discussed later under "Using Advanced Sequencer Features."
The Sequencer
interface provides methods in several categories:
Sequence
object, and to save the currently loaded
sequence data to a MIDI file. Sequence
. Sequencer
may
play at different tempos, with some Tracks
muted, and
in various synchronization states with other objects. Sequencer
processes certain kinds of
MIDI events.
Sequencer
methods you'll invoke,
the first step is to obtain a Sequencer
device from
the system and reserve it for your program's use.
An application program
doesn't instantiate a Sequencer
; after all,
Sequencer
is just an interface. Instead, like all
devices in the Java Sound API's MIDI package, a
Sequencer
is accessed through the static
MidiSystem
object. As mentioned in Chapter 9,
"Accessing MIDI System Resources," the
following MidiSystem
method can be used to obtain the
default Sequencer
:
static Sequencer getSequencer()
The following code fragment
obtains the default Sequencer
, acquires any system
resources it needs, and makes it operational:
Sequencer sequencer; // Get default sequencer. sequencer = MidiSystem.getSequencer(); if (sequencer == null) { // Error -- sequencer device is not supported. // Inform user and return... } else { // Acquire resources and make operational. sequencer.open(); }
The invocation of
open
reserves the sequencer device for your program's
use. It doesn't make much sense to imagine sharing a sequencer,
because it can play only one sequence at a time. When you're done
using the sequencer, you can make it available to other programs by
invoking close
.
Non-default sequencers can be obtained as described in Chapter 9, "Accessing MIDI System Resources."
Having obtained a sequencer from the system and reserved it, you then need load the data that the sequencer should play. There are three typical ways of accomplishing this:
MidiEvent
objects to
those tracks
InputStream
that you then read directly to the
sequencer by means of
Sequencer.setSequence(InputStream)
. With this
approach, you don't explicitly create a Sequence
object. In fact, the Sequencer
implementation might
not even create a Sequence
behind the scenes, because
some sequencers have a built-in mechanism for handling data
directly from a file.
The other approach is to
create a Sequence
explicitly. You'll need to use this
approach if you're going to edit the sequence data before playing
it. With this approach, you invoke MidiSystem's
overloaded method getSequence
. The method is able to
get the sequence from an InputStream
, a
File
, or a URL
. The method returns a
Sequence
object that can then be loaded into a
Sequencer
for playback. Expanding on the previous code
excerpt, here's an example of obtaining a Sequence
object from a File
and loading it into our
sequencer
:
try { File myMidiFile = new File("seq1.mid"); // Construct a Sequence object, and // load it into my sequencer. Sequence mySeq = MidiSystem.getSequence(myMidiFile); sequencer.setSequence(mySeq); } catch (Exception e) { // Handle error and/or return }
Like
MidiSystem's
getSequence
method,
setSequence
may throw an
InvalidMidiDataException
—and, in the case of the
InputStream
variant, an
IOException
—if it runs into any trouble.
Starting and stopping a
Sequencer
is accomplished using the following
methods:
void start()
void stop()
The
Sequencer.start
method begins playback of the
sequence. Note that playback starts at the current position in a
sequence. Loading an existing sequence using the
setSequence
method, described above, initializes the
sequencer's current position to the very beginning of the sequence.
The stop
method stops the sequencer, but it does not
automatically rewind the current Sequence
. Starting a
stopped Sequence
without resetting the position simply
resumes playback of the sequence from the current position. In this
case, the stop
method has served as a pause operation.
However, there are various Sequencer
methods for
setting the current sequence position to an arbitrary value before
playback is started. (We'll discuss these methods below.)
As mentioned earlier, a
Sequencer
typically has one or more
Transmitter
objects, through which it sends
MidiMessages
to a Receiver
. It is through
these Transmitters
that a Sequencer
plays
the Sequence
, by emitting appropriately timed
MidiMessages
that correspond to the
MidiEvents
contained in the current
Sequence
. Therefore, part of the setup procedure for
playing back a Sequence
is to invoke the
setReceiver
method on the Sequencer's
Transmitter
object, in effect wiring its output to the
device that will make use of the played-back data. For more details
on Transmitters
and Receivers
, see
Chapter 10, "Transmitting and Receiving
MIDI Messages."
To capture MIDI data to a
Sequence
, and subsequently to a file, you need to
perform some additional steps beyond those described above. The
following outline shows the steps necessary to set up for recording
to a Track
in a Sequence
:
MidiSystem.getSequencer
to get a new sequencer
to use for recording, as above. setReceiver
method, to send data to a
Receiver
associated with the recording
Sequencer
. Sequence
object, which will store the
recorded data. When you create the Sequence
object,
you must specify the global timing information for the sequence.
For example:
Sequence mySeq; try{ mySeq = new Sequence(Sequence.PPQ, 10); } catch (Exception ex) { ex.printStackTrace(); }The constructor for
Sequence
takes as arguments a
divisionType
and a timing resolution. The
divisionType
argument specifies the units of the
resolution argument. In this case, we've specified that the timing
resolution of the Sequence
we're creating will be 10
pulses per quarter note. An additional optional argument to the
Sequence
constructor is a number of tracks argument,
which would cause the initial sequence to begin with the specified
number of (initially empty) Tracks
. Otherwise the
Sequence
will be created with no initial
Tracks
; they can be added later as needed. Track
in the
Sequence
, with Sequence.createTrack
. This
step is unnecessary if the Sequence
was created with
initial Tracks
. Sequencer.setSequence
, select our new
Sequence
to receive the recording. The
setSequence
method ties together an existing
Sequence
with the Sequencer
, which is
somewhat analogous to loading a tape onto a tape recorder. Sequencer.recordEnable
for each
Track
to be recorded. If necessary, get a reference to
the available Tracks
in the Sequence
by
invoking Sequence.getTracks
. startRecording
on the
Sequencer
. Sequencer.stop
or
Sequencer.stopRecording
. Sequence
to a MIDI file with
MidiSystem.write
. The write
method of
MidiSystem
takes a Sequence
as one of its
arguments, and will write that Sequence
to a stream or
file.Many application programs allow a sequence to be created by loading it from a file, and quite a few also allow a sequence to be created by capturing it from live MIDI input (that is, recording). Some programs, however, will need to create MIDI sequences from scratch, whether programmatically or in response to user input. Full-featured sequencer programs permit the user to manually construct new sequences, as well as to edit existing ones.
These data-editing
operations are achieved in the Java Sound API not by
Sequencer
methods, but by methods of the data objects
themselves: Sequence
, Track
, and
MidiEvent
. You can create an empty sequence using one
of the Sequence
constructors, and then add tracks to
it by invoking the following Sequence
method:
Track createTrack()If your program allows the user to edit sequences, you'll need this
Sequence
method to remove tracks:
boolean deleteTrack(Track track)
Once the sequence contains
tracks, you can modify the contents of the tracks by invoking
methods of the Track
class. The
MidiEvents
contained in the Track
are
stored as a java.util.Vector
in the Track
object, and Track
provides a set of methods for
accessing, adding, and removing the events in the list. The methods
add
and remove
are fairly
self-explanatory, adding or removing a specified
MidiEvent
from a Track
. A
get
method is provided, which takes an index into the
Track's
event list and returns the
MidiEvent
stored there. In addition, there are
size
and tick
methods, which respectively
return the number of MidiEvents
in the track, and the
track's duration, expressed as a total number of
Ticks
.
To create a new event before
adding it to the track, you'll of course use the
MidiEvent
constructor. To specify or modify the MIDI
message embedded in the event, you can invoke the
setMessage
method of the appropriate
MidiMessage
subclass (ShortMessage
,
SysexMessage
, or MetaMessage
). To modify
the time that the event should occur, invoke
MidiEvent.setTick
.
In combination, these low-level methods provide the basis for the editing functionality needed by a full-featured sequencer program.
So far, this chapter has
focused on simple playback and recording of MIDI data. This section
will briefly describe some of the more advanced features available
through methods of the Sequencer
interface and the
Sequence
class.
There are two
Sequencer
methods that obtain the sequencer's current
position in the sequence. The first of these:
long getTickPosition()
returns the position measured in MIDI ticks from the beginning of the sequence. The second method:
long getMicrosecondPosition()
returns the current position
in microseconds. This method assumes that the sequence is being
played at the default rate as stored in the MIDI file or in the
Sequence
. It does not return a different
value if you've changed the playback speed as described below.
You can similarly set the sequencer's current position according to one unit or the other:
void setTickPosition(long tick)
void setMicrosecondPosition(long microsecond)
As indicated earlier, a
sequence's speed is indicated by its tempo, which can vary over the
course of the sequence. A sequence can contain events that
encapsulate standard MIDI tempo-change messages. When the sequencer
processes such an event, it changes the speed of playback to
reflect the indicated tempo. In addition, you can programmatically
change the tempo by invoking any of these Sequencer
methods:
public void setTempoInBPM(float bpm) public void setTempoInMPQ(float mpq) public void setTempoFactor(float factor)The first two of these methods set the tempo in beats per minute or microseconds per quarter note, respectively. The tempo will stay at the specified value until one of these methods is invoked again, or until a tempo-change event is encountered in the sequence, at which point the current tempo is overridden by the newly specified one.
The third method,
setTempoFactor
, is different in nature. It scales
whatever tempo is set for the sequencer (whether by tempo-change
events or by one of the first two methods above). The default
scalar is 1.0 (no change). Although this method causes the playback
or recording to be faster or slower than the nominal tempo (unless
the factor is 1.0), it doesn't alter the nominal tempo. In other
words, the tempo values returned by getTempoInBPM
and
getTempoInMPQ
are unaffected by the tempo factor, even
though the tempo factor does affect the actual rate of playback or
recording. Also, if the tempo is changed by a tempo-change event or
by one of the first two methods, it still gets scaled by whatever
tempo factor was last set. If you load a new sequence, however, the
tempo factor is reset to 1.0.
Note that all these tempo-change directives are ineffectual when the sequence's division type is one of the SMPTE types, instead of PPQ.
It's often convenient for users of sequencers to be able to turn off certain tracks, to hear more clearly exactly what is happening in the music. A full-featured sequencer program lets the user choose which tracks should sound during playback. (Speaking more precisely, since sequencers don't actually create sound themselves, the user chooses which tracks will contribute to the stream of MIDI messages that the sequencer produces.) Typically, there are two types of graphical controls on each track: a mute button and a solo button. If the mute button is activated, that track will not sound under any circumstances, until the mute button is deactivated. Soloing is a less well-known feature. It's roughly the opposite of muting. If the solo button on any track is activated, only tracks whose solo buttons are activated will sound. This feature lets the user quickly audition a small number of tracks without having to mute all the other tracks. The mute button typically takes priority over the solo button: if both are activated, the track doesn't sound.
Using Sequencer
methods, muting or soloing tracks (as well as querying a track's
current mute or solo state) is easily accomplished. Let's assume we
have obtained the default Sequencer
and that we've
loaded sequence data into it. Muting the fifth track in the
sequence would be accomplished as follows:
sequencer.setTrackMute(4, true); boolean muted = sequencer.getTrackMute(4); if (!muted) { return; // muting failed }There are a couple of things to note about the above code snippet. First, tracks of a sequence are numbered starting with 0 and ending with the total number of tracks minus 1. Also, the second argument to
setTrackMute
is a boolean. If it's true, the
request is to mute the track; otherwise the request is to unmute
the specified track. Lastly, in order to test that the muting took
effect, we invoke the Sequencer getTrackMute
method,
passing it the track number we're querying. If it returns
true
, as we'd expect in this case, then the mute
request worked. If it returns false
, then it failed.
Mute requests may fail for
various reasons. For example, the track number specified in the
setTrackMute
call might exceed the total number of
tracks, or the sequencer might not support muting. By calling
getTrackMute
, we can determine if our request
succeeded or failed.
As an aside, the boolean
that's returned by getTrackMute
can, indeed, tell us
if a failure occurred, but it can't tell us why it occurred. We
could test to see if a failure was caused by passing an invalid
track number to the setTrackMute
method. To do this,
we would call the getTracks
method of
Sequence
, which returns an array containing all of the
tracks in the sequence. If the track number specified in the
setTrackMute
call exceeds the length of this array,
then we know we specified an invalid track number.
If the mute request succeeded, then in our example, the fifth track will not sound when the sequence is playing, nor will any other tracks that are currently muted.
The method and techniques
for soloing a track are very similar to those for muting. To solo a
track, invoke the setTrackSolo
method of
Sequence:
void setTrackSolo(int track, boolean bSolo)As in
setTrackMute
, the first argument specifies the
zero-based track number, and the second argument, if
true
, specifies that the track should be in solo mode;
otherwise the track should not be soloed.
By default, a track is neither muted nor soloed.
Sequencer
has
an inner class called Sequencer.SyncMode
. A
SyncMode
object represents one of the ways in which a
MIDI sequencer's notion of time can be synchronized with a master
or slave device. If the sequencer is being synchronized to a
master, the sequencer revises its current time in response to
certain MIDI messages from the master. If the sequencer has a
slave, the sequencer similarly sends MIDI messages to control the
slave's timing.
There are three predefined
modes that specify possible masters for a sequencer:
INTERNAL_CLOCK
, MIDI_SYNC
, and
MIDI_TIME_CODE
. The latter two work if the sequencer
receives MIDI messages from another device. In these two modes, the
sequencer's time gets reset based on system real-time timing clock
messages or MIDI time code (MTC) messages, respectively. (See the
MIDI specification for more information about these types of
message.) These two modes can also be used as slave modes, in which
case the sequencer sends the corresponding types of MIDI messages
to its receiver. A fourth mode, NO_SYNC
, is used to
indicate that the sequencer should not send timing information to
its receivers.
By calling the
setMasterSyncMode
method with a supported
SyncMode
object as the argument, you can specify how
the sequencer's timing is controlled. Likewise, the
setSlaveSyncMode
method determines what timing
information the sequencer will send to its receivers. This
information controls the timing of devices that use the sequencer
as a master timing source.
Each track of a sequence can
contain many different kinds of MidiEvents
. Such
events include Note On and Note Off messages, program changes,
control changes, and meta events. The Java Sound API specifies
"listener" interfaces for the last two of these event types
(control change events and meta events). You can use these
interfaces to receive notifications when such events occur during
playback of a sequence.
Objects that support the
ControllerEventListener
interface can receive
notification when a Sequencer
processes particular
control-change messages. A control-change message is a standard
type of MIDI message that represents a change in the value of a
MIDI controller, such as a pitch-bend wheel or a data slider. (See
the MIDI specification for the complete list of control-change
messages.) When such a message is processed during playback of a
sequence, the message instructs any device (probably a synthesizer)
that's receiving the data from the sequencer to update the value of
some parameter. The parameter usually controls some aspect of sound
synthesis, such as the pitch of the currently sounding notes if the
controller was the pitch-bend wheel. When a sequence is being
recorded, the control-change message means that a controller on the
external physical device that created the message has been moved,
or that such a move has been simulated in software.
Here's how the
ControllerEventListener
interface is used. Let's
assume that you've developed a class that implements the
ControllerEventListener
interface, meaning that your
class contains the following method:
void controlChange(ShortMessage msg)Let's also assume that you've created an instance of your class and assigned it to a variable called
myListener
. If you
include the following statements somewhere within your program:
int[] controllersOfInterest = { 1, 2, 4 }; sequencer.addControllerEventListener(myListener, controllersOfInterest);then your class's
controlChange
method will be invoked
every time the sequencer processes a control-change message for
MIDI controller numbers 1, 2, or 4. In other words, when the
Sequencer
processes a request to set the value of any
of the registered controllers, the Sequencer
will
invoke your class's controlChange
method. (Note that
the assignments of MIDI controller numbers to specific control
devices is detailed in the MIDI 1.0 Specification.)
The
controlChange
method is passed a
ShortMessage
containing the controller number
affected, and the new value to which the controller was set. You
can obtain the controller number using the
ShortMessage.getData1
method, and the new setting of
the controller's value using the ShortMessage.getData2
method.
The other kind of special
event listener is defined by the MetaEventListener
interface. Meta messages, according to the Standard MIDI Files 1.0
specification, are messages that are not present in MIDI wire
protocol but that can be embedded in a MIDI file. They are not
meaningful to a synthesizer, but can be interpreted by a sequencer.
Meta messages include instructions (such as tempo change commands),
lyrics or other text, and other indicators (such as
end-of-track).
The
MetaEventListener
mechanism is analogous to
ControllerEventListener
. Implement the
MetaEventListener
interface in any class whose
instances need to be notified when a MetaMessage
is
processed by the sequencer. This involves adding the following
method to the class:
void meta(MetaMessage msg)
You register an instance of
this class by passing it as the argument to the Sequencer
addMetaEventListener
method:
boolean b = sequencer.addMetaEventListener (myMetaListener);This is slightly different from the approach taken by the
ControllerEventListener
interface, because you have to
register to receive all MetaMessages,
not just
selected ones of interest. If the sequencer encounters a
MetaMessage
in its sequence, it will invoke
myMetaListener.meta
, passing it the
MetaMessage
encountered. The meta
method
can invoke getType
on its MetaMessage
argument to obtain an integer from 0 to 127 that indicates the
message type, as defined by the Standard MIDI Files 1.0
specification.