Java Sound API Compared with JMF and JOAL


MIDI

The previous section looked at the basic support in the Java Sound API for playing sampled audio. Now, I'll consider the other major part of the API, which is its support for playing MIDI sequences.

A key benefit of the MIDI is that it represents musical data in an efficient way, leading to drastic reductions in file sizes compared to sampled audio. For instance, files containing high-quality stereo sampled audio require about 10 MB per minute of sound, while a typical MIDI sequence may need less than 10 KB.

The secret to this phenomenal size reduction is that a MIDI sequence stores "instructions" for playing the music rather than the music itself. A simple analogy is that a sequence is the written score for a piece of music rather than a recording of it.

The drawback is that the sequence must be converted to audio output at runtime. This is achieved using a sequencer and synthesizer. Their configuration is shown in greatly simplified form in Figure 7-8.

Figure 7-8. A MIDI sequencer and synthesizer


A MIDI sequencer allows MIDI data sequences to be captured, stored, edited, combined, and performed, while the MIDI data's transformation into audio is being carried out by the synthesizer.

Continuing my analogy, the sequencer is the orchestral conductor who receives the score to play, perhaps making changes to it in the process. The synthesizer is the orchestra, made up of musicians playing different parts of the score. The musicians correspond to the MidiChannel objects in the synthesizer. They are allocated instruments from the sound banks, and play concurrently. Usually, a complete sequence (a complete score) is passed to the sequencer, but it's possible to send it a stream of MIDI events.

In J2SE 1.4.2 and earlier, the sequencer and synthesizer were represented by a single Sequencer object. This has changed in J2SE 5.0, and it's now necessary to obtain distinct Sequencer and Synthesizer objects and link them together using Receiver and TRansmitter objects.

A MIDI Sequence

A Sequence object represents a multitrack data structure, each track containing time-ordered MIDIEvent objects. These events are time-ordered, based on an internal "tick" value (a timestamp). Each event contains musical data in a MidiMessage object. The sequence structure is illustrated in Figure 7-9.

Figure 7-9. The internals of a MIDI sequence


Tracks are employed as an optional organizational layer to place "related" MIDI data together, and the synthesizer makes no use of the information. Java Sound supports Type 0 and Type 1 MIDI sequences, the main difference between them being that Type 0 files only have a single track.

MIDI messages are encoded using three subclasses of MidiMessage: ShortMessage, SysexMessage, and MetaMessage. SysexMessage deals with system-exclusive messages, such as patch parameters or sample data sent between MIDI devices, which are usually specific to the MIDI device. MetaMessages are used to transmit meta-information about the sequence, such as tempo settings, and instrument information.

ShortMessage is the most important class since it includes the NOTE_ON and NOTE_OFF messages for starting and terminating note playing on a given MidiChannel. Typically, one MidiEvent contains a NOTE_ON for beginning the playing, and a later MidiEvent holds a NOTE_OFF for switching it off. The duration of the note corresponds to the time difference between the tick values in the two events.

As shown in Figure 7-8, a program can directly communicate with the synthesizer, sending it a stream of MidiEvents or MidiMessages. The difference between the approaches is the timing mechanism; a stream of MidiEvents contains tick values, which the synthesizer can use to space out note playing and other activities. A stream of MidiMessages contains no timing data, so it's up to the program to send the messages at the required time intervals.

Examples of these techniques are given in the "MIDI Synthesis" section in Chapter 10.


The internal format of a MidiMessage is simple: there's an 8-bit status byte, which identifies the message type followed by two data bytes. Depending on the message, one or both of these bytes may be utilized. The byte size means that values usually range between 0 and 127.

One source of confusion for a programmer familiar with MIDI is that the MidiMessage class and its subclasses do not correspond to the names used in the MIDI specification (online at http://www.midi.org). ShortMessage includes the MIDI channel voice, channel mode, system common, and system real-time messagesin other words, everything except system exclusive and meta-events. In the rest of this chapter, I'll use the Java Sound MIDI class names as opposed to those names used in the specification.

Playing a MIDI Sequence

PlayMidi.java (stored in SoundExamps/SoundPlayer/) loads a MIDI sequence and plays it once:

     public static void main(String[] args)     { if (args.length != 1) {         System.out.println("Usage: java PlayMidi <midi file>");         System.exit(0);       }       new PlayMidi(args[0]);       System.exit(0);    // required in J2SE 1.4.2. or earlier     }

As with PlayClip, the call to exit( ) must be present in J2SE 1.4.2 or earlier, but is unnecessary in J2SE 5.0.


The PlayMidi class implements the MetaEventListener interface to detect when the sequence has reached the end of its tracks:

     public class PlayMidi implements MetaEventListener     {       // midi meta-event constant used to signal the end of a track       private static final int END_OF_TRACK = 47;       private final static String SOUND_DIR = "Sounds/";       private Sequencer sequencer;   // globals       private Synthesizer synthesizer;       private Sequence seq = null;       private String filename;       private DecimalFormat df;               :  // the rest of the class     }

The PlayMidi constructor initializes the sequencer and synthesizer, loads the sequence, and starts it playing:

     public PlayMidi(String fnm)     {       df = new DecimalFormat("0.#");  // 1 dp       filename = SOUND_DIR + fnm;       initSequencer( );       loadMidi(filename);       play( );       // wait for the sound to finish playing; guess at 10 mins!       System.out.println("Waiting");       try {         Thread.sleep(600000);   // 10 mins in ms       }       catch(InterruptedException e)       { System.out.println("Sleep Interrupted"); }     }

As with PlayClip, PlayMidi waits to give the sequence time to play. When the sequence finishes, the call to meta( ) allows PlayMidi to exit from its slumbers ahead of time.

initSequence( ) obtains a sequencer and synthesizer from the MIDI system and links them together. It also sets up the meta-event listener:

     private void initSequencer( )     {       try {              sequencer = MidiSystem.getSequencer( );         if (sequencer == null) {           System.out.println("Cannot get a sequencer");           System.exit(0);         }         sequencer.open( );         sequencer.addMetaEventListener(this);         // maybe the sequencer is not the same as the synthesizer         // so link sequencer --> synth (this is required in J2SE 5.0)         if (!(sequencer instanceof Synthesizer)) {           System.out.println("Linking the sequencer to a synthesizer");           synthesizer = MidiSystem.getSynthesizer( );           synthesizer.open( );           Receiver synthReceiver = synthesizer.getReceiver( );           Transmitter seqTransmitter = sequencer.getTransmitter( );           seqTransmitter.setReceiver(synthReceiver);         }         else           synthesizer = (Synthesizer) sequencer;              // I don't use the synthesizer in this simple code,              // so storing it as a global isn't really necessary       }       catch (MidiUnavailableException e){         System.out.println("No sequencer available");         System.exit(0);       }     } // end of initSequencer( )

loadMidi( ) loads the sequence by calling MidiSystem.getSequence( ) inside a large TRy-catch block to catch the many possible kinds of errors that can occur:

     private void loadMidi(String fnm)     {       try {         seq = MidiSystem.getSequence( getClass( ).getResource(fnm) );         double duration = ((double) seq.getMicrosecondLength( )) / 1000000;         System.out.println("Duration: " + df.format(duration)+" secs");       }        // several catch blocks go here; see the code for details     }

play( ) loads the sequence into the sequencer and starts it playing:

     private void play( )     { if ((sequencer != null) && (seq != null)) {         try {           sequencer.setSequence(seq);  // load MIDI into sequencer           sequencer.start( );   // start playing it         }         catch (InvalidMidiDataException e) {           System.out.println("Corrupted/invalid midi file: " + filename);           System.exit(0);         }       }     }

start( ) will return immediately, and PlayMidi will go to sleep back in the constructor.

meta( ) is called frequently as the sequence begins playing, but I'm only interested in responding to the end-of-track event:

     public void meta(MetaMessage event)     { if (event.getType( ) == END_OF_TRACK) {         System.out.println("Exiting...");         close( );         System.exit(0);       }     }



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net