Playing Music

Although background music isn't found in every game you play, it can play an important role in a game. Music can set the mood—for example, an action game could play fast-paced music, while slower music might be better suited for a game that requires more thought. Also, music can change the player's emotions toward elements of the game. Is the music happy? Or is it dramatic? Happy music might be better suited for the easier levels, such as the first few levels of a game. Dramatic music could be played for the more difficult levels, such as when the player is fighting a powerful bad guy. When you've decided on the type of music you want, the next step is to figure out where the music comes from. Nope, it doesn't come from those voices in your head. Instead, games typically play music in one of three ways:

  • Streaming music from an audio track on a CD
  • Playing compressed music such as MP3 or Ogg Vorbis
  • Playing MIDI music

Playing CD Audio

Some DVD games have plain old Red tutorial Audio (the standard audio CD format) right on the CD. The benefit here is you get great-quality sound and it's easy to implement: Just tell the sound card to start playing the CD, without any other involvement from the game. The other cool aspect is that players can slip the game CD into their audio CD player and groove to the game's tracks. Unfortunately, CD audio takes up a lot of space, typically around 30MB for a three-minute song. If you have four three-minute songs, that's 120MB of space that could be used for more graphics or bigger levels. In addition, the Java Sound implementation doesn't support playback from a CD, so this option is out for you.

Playing MP3 and Ogg Vorbis

The second option is compressed music. MP3 and Ogg Vorbis formats are much smaller than that for CD audio, typically around 3MB for a three-minute song, and have near-CD quality. They've become increasingly more popular in games. The drawback here is that decoding MP3 or Ogg Vorbis files takes quite a chunk of processor power. Sound cards can't play compressed music directly, so the music is decoded while it's played. This means the extra processor use can interfere with other parts of your game. On faster, modern machines, the decoding won't be noticeable, but on slower, older machines, the decoding could take 20% to 40% of the processor or more. That could make other parts of the game, such as animation, seem slow or jerky. If the processor time doesn't matter to your game, you'll need to get an MP3 or Ogg Vorbis Java decoder. Java Zoom, at www.javazoom.net, has both. This site provides a plug-in through Java's Service Provider Interface that enables you to get an AudioInputStream that decodes an MP3 or Ogg Vorbis stream. Just include the necessary .jar file, and be sure to convert the AudioInputStream, like so:

// create the format used for playback
// (uncompressed, 44100Hz, 16-bit, mono, signed, little-endian)
AudioFormat playbackFormat =
 new AudioFormat(44100, 16, 1, true, false);
// open the source file AudioInputStream source =
 AudioSystem.getAudioInputStream(new File("music.ogg"));
// convert to playback format source = AudioSystem.getAudioInputStream(playbackFormat, source);


Also, be sure not to load the samples into memory. A compressed sound file might take up only 1MB for each minute of music, but the uncompressed samples would take 10MB for each minute. Instead, play any large sounds directly from the AudioInputStream, which streams the sound from disk. But which one should your game use, MP3 or Ogg Vorbis? Although MP3 is incredibly popular, Ogg Vorbis is license-free and claims better sound quality. The people playing your game won't notice or care, so go with whichever one suits your needs better. You can find more information on Ogg Vorbis at www.xiph.org/ogg/vorbis. Also, be sure to look into MP3 licensing issues at www.mp3licensing.com.

Playing MIDI Music

Finally, there's MIDI music. MIDI music isn't sampled music like other sound formats; instead, it works more like a composer's sheet music, giving instructions on which note to play on each instrument. The audio system synthesizes to each note according to the pitch, instrument, and volume, among other parameters. Because MIDI files contain instructions instead of samples, MIDI files are very small compared to sampled sound formats and are often measured in kilobytes rather than megabytes. Because the music is synthesized, the quality might not be as high as that of sampled music. Some instruments won't sound realistic, or the music might sound a little too mechanical. A creative musician can usually mask the deficiencies of MIDI music, however. The Java Sound API synthesizes MIDI music through the use of a soundbank, which is a collection of instruments. Unfortunately, although the Java SDK includes a soundbank, the Java runtime does not. If a soundbank isn't found, the hardware MIDI port, which has unreliable timing in Java Sound, is used. For this reason, it's recommended to use a soundbank. The Java SDK includes a minimal-quality soundbank, and you can download higher-quality soundbanks from http://java.oracle.com/products/java-media/sound/soundbanks.html and include them with your game. The Java Sound API provides MIDI sound capabilities in the javax.sound.midi package. To play MIDI music, you need two objects in this package: Sequence and Sequencer. A Sequence object contains the MIDI data, and a Sequencer sends a Sequence to the MIDI synthesizer. Here's an example of playing a MIDI file:

// open the midi file Sequence sequence = MidiSystem.getSequence(new File(filename));
// open the sequencer Sequencer sequencer = MidiSystem.getSequencer();
sequencer.open();
// play the midi sequence sequencer.setSequence(sequence);
sequencer.start();


By default, the Sequencer plays a Sequence once and then stops. But in a game, you usually want to loop the music. To loop a Sequence, you need to be notified when the music is done playing and start the Sequencer again. An example of how to loop music is in the MidiPlayer class in Listing 4.11. It implements the MetaEventListener interface, which notifies the class when the Sequence is done playing via the end-of-track message.

Listing 4.11 MidiPlayer.java
package com.brackeen.javagamebook.sound;
import java.io.File;
import java.io.IOException;
import javax.sound.midi.*;
public class MidiPlayer implements MetaEventListener {
 // Midi meta event
 public static final int END_OF_TRACK_MESSAGE = 47;
 private Sequencer sequencer;
 private boolean loop;
 private boolean paused;
 /**
 Creates a new MidiPlayer object.
 */
 public MidiPlayer() {
 try {
 sequencer = MidiSystem.getSequencer();
 sequencer.open();
 sequencer.addMetaEventListener(this);
 }
 catch ( MidiUnavailableException ex) {
 sequencer = null;
 }
 }
 /**
 Loads a sequence from the file system. Returns null if
 an error occurs.
 */
 public Sequence getSequence(String filename) {
 try {
 return MidiSystem.getSequence(new File(filename));
 }
 catch (InvalidMidiDataException ex) {
 ex.printStackTrace();
 return null;
 }
 catch (IOException ex) {
 ex.printStackTrace();
 return null;
 }
 }
 /**
 Plays a sequence, optionally looping. This method returns
 immediately. The sequence is not played if it is invalid.
 */
 public void play(Sequence sequence, boolean loop) {
 if (sequencer != null && sequence != null) {
 try {
 sequencer.setSequence(sequence);
 sequencer.start();
 this.loop = loop;
 }
 catch (InvalidMidiDataException ex) {
 ex.printStackTrace();
 }
 }
 }
 /**
 This method is called by the sound system when a meta
 event occurs. In this case, when the end-of-track meta
 event is received, the sequence is restarted if
 looping is on.
 */
 public void meta(MetaMessage event) {
 if (event.getType() == END_OF_TRACK_MESSAGE) {
 if (sequencer != null && sequencer.isOpen() && loop) {
 sequencer.start();
 }
 }
 }
 /**
 Stops the sequencer and resets its position to 0.
 */
 public void stop() {
 if (sequencer != null && sequencer.isOpen()) {
 sequencer.stop();
 sequencer.setMicrosecondPosition(0);
 }
 }
 /**
 Closes the sequencer.
 */
 public void close() {
 if (sequencer != null && sequencer.isOpen()) {
 sequencer.close();
 }
 }
 /**
 Gets the sequencer.
 */
 public Sequencer getSequencer() {
 return sequencer;
 }
 /**
 Sets the paused state. Music may not immediately pause.
 */
 public void setPaused(boolean paused) {
 if (this.paused != paused && sequencer != null) {
 this.paused = paused;
 if (paused) {
 sequencer.stop();
 }
 else {
 sequencer.start();
 }
 }
 }
 /**
 Returns the paused state.
 */
 public boolean isPaused() {
 return paused;
 }
}


MidiPlayer also provides methods for opening Sequences and pausing the music. To pause, MidiPlayer calls the stop() method of the Sequencer object, which stops the Sequence without resetting its position. Calling start() resumes the paused sequence. All this happens in MidiPlayer's setPaused() method.

Creating Adaptive Music

Now that you can play MIDI music, let's discuss one of its advantages: easy implementation of adaptive music. Adaptive music is music that changes based on the state of the game. For instance, if the player is battling a large number of enemies, the music might be fast and loud. Conversely, the music might be quiet when the player is walking around exploring rooms alone. The change in music could happen at any time—for example, the player could be strolling along one second, and then 100 robots could be trying to kill him the next. So, changing the music smoothly can be a challenge. You can adapt songs to the game state in two ways:

  • Change songs
  • Modify the song currently playing

Because the actions of a player can change at any time, changing songs is a more difficult task. The change can't be abrupt, or it will be distracting. Songs can be designed so that they have "change points" to signify places where a song change can occur. Also, songs need to transition smoothly. To do this, the first song can fade out while the next song is fading in. Also, while the first song is fading out, its tempo could change to match the tempo of the next song. Or, you could just take the easy way out and insert a sound of a scratching phonograph needle. The second option is to simply modify the exiting song. You can do this by changing the tempo or volume, or adding another instrument to it. Adding or taking away an instrument is easy to do with MIDI sequences. MIDI music is typically organized into tracks, with each track playing a specific instrument. For instance, there might be one track for guitar, one for keyboards, and so on. You can mute or unmute a track with one method:

sequencer.setTrackMute(trackNum, true);


Here, trackNum is an integer representing the track number you want to mute. If you don't know what track belongs to what instrument in your MIDI file, you might have to experiment by muting each track, one by one. Try manipulating the MIDI music in the MidiTest class in Listing 4.12. It's a short test that uses MidiPlayer to start a song, first with the drum track off. After playing through once, it plays the song again with the drum track on.

Listing 4.12 MidiTest.java
import java.io.File;
import java.io.IOException;
import javax.sound.midi.*;
import com.brackeen.javagamebook.sound.MidiPlayer;
/**
 An example that plays a Midi sequence. First, the sequence
 is played once with track 1 turned off. Then the sequence is
 played once with track 1 turned on. Track 1 is the drum track
 in the example midi file.
*/
public class MidiTest implements MetaEventListener {
 // The drum track in the example Midi file
 private static final int DRUM_TRACK = 1;
 public static void main(String[] args) {
 new MidiTest().run();
 }
 private MidiPlayer player;
 public void run() {
 player = new MidiPlayer();
 // load a sequence
 Sequence sequence =
 player.getSequence("../sounds/music.midi");
 // play the sequence
 player.play(sequence, true);
 // turn off the drums
 System.out.println("Playing (without drums)...");
 Sequencer sequencer = player.getSequencer();
 sequencer.setTrackMute(DRUM_TRACK, true);
 sequencer.addMetaEventListener(this);
 }
 /**
 This method is called by the sound system when a meta
 event occurs. In this case, when the end-of-track meta
 event is received, the drum track is turned on.
 */
 public void meta(MetaMessage event) {
 if (event.getType() == MidiPlayer.END_OF_TRACK_MESSAGE) {
 Sequencer sequencer = player.getSequencer();
 if (sequencer.getTrackMute(DRUM_TRACK)) {
 // turn on the drum track
 System.out.println("Turning on drums...");
 sequencer.setTrackMute(DRUM_TRACK, false);
 }
 else {
 // close the sequencer and exit
 System.out.println("Exiting...");
 player.close();
 System.exit(0);
 }
 }
 }
}


Just like when you play sampled sound, you must explicitly exit the VM when you're done. MidiTest exits the VM when it receives the second end-of-track message.



   
Comments