Creating a Sound Manager

Whenever you play a new sound with the SimpleSoundPlayer, the following steps occur:

  1. Create a new thread.

  2. Create a new byte buffer.

  3. Create, open, and start a new Line.

That's a lot of object creation. Plus, a new thread must start and native resources that operate the Line must be allocated. All this adds up to one thing: lag. It takes too long from the time you request to play a sound to the time when the sound is actually played. Although the lag might not matter in some instances, such as when you play a startup sound, other times the lag is very noticeable, such as when you're blasting a gun several times a second. You want to eliminate the lag as much as possible. To reduce the lag, rework the sound-playing architecture. Instead of starting a new thread for every sound played, you'll use the ThreadPool class from , "Java Threads." This keeps several threads ready and waiting to be assigned a sound to play. Second, each thread in the thread pool can contain its own buffer and Line object, so you don't have to allocate those every time a sound is played. The drawback to this technique is that every line must be assigned an AudioFormat beforehand, so you can play only one type of audio format. This shouldn't be a problem—just pick an audio format that you want to use for your game, and make sure all your sounds have that same format.

The Sound Class

Now let's implement this architecture. First you'll create a simple class that contains a sound sample, called Sound, in Listing 4.9.

Listing 4.9 Sound.java
package com.brackeen.javagamebook.sound;
/**
 The Sound class is a container for sound samples. The sound
 samples are format-agnostic and are stored as a byte array.
*/
public class Sound {
 private byte[] samples;
 /**
 Create a new Sound object with the specified byte array.
 The array is not copied.
 */
 public Sound(byte[] samples) {
 this.samples = samples;
 }
 /**
 Returns this Sound's objects samples as a byte array.
 */
 public byte[] getSamples() {
 return samples;
 }
}


The Sound class is a wrapper class for a byte buffer that contains sound samples. It doesn't serve much of a purpose other than making the code more understandable and hiding the contents of a sound, in case you want to change it later.

The SoundManager Class

Next create the SoundManager class in Listing 4.10. Along with implementing similar functions to SimpleSoundPlayer, such as loading a sound from a file, SoundManager reduces the lag and gives you the capability to pause playing sounds.

Listing 4.10 SoundManager.java
package com.brackeen.javagamebook.sound;
import java.io.*;
import javax.sound.sampled.*;
import javax.sound.midi.*;
import com.brackeen.javagamebook.util.ThreadPool;
import com.brackeen.javagamebook.util.LoopingByteInputStream;
/**
 The SoundManager class manages sound playback. The
 SoundManager is a ThreadPool, with each thread playing back
 one sound at a time. This allows the SoundManager to
 easily limit the number of simultaneous sounds being played.
 <p>Possible ideas to extend this class:<ul>
 <li>add a setMasterVolume() method, which uses Controls to
 set the volume for each line.
 <li>don't play a sound if more than, say, 500ms have passed
 since the request to play
 </ul>
*/
public class SoundManager extends ThreadPool {
 private AudioFormat playbackFormat;
 private ThreadLocal localLine;
 private ThreadLocal localBuffer;
 private Object pausedLock;
 private boolean paused;
 /**
 Creates a new SoundManager using the maximum number of
 simultaneous sounds.
 */
 public SoundManager(AudioFormat playbackFormat) {
 this(playbackFormat,
 getMaxSimultaneousSounds(playbackFormat));
 }
 /**
 Creates a new SoundManager with the specified maximum
 number of simultaneous sounds.
 */
 public SoundManager(AudioFormat playbackFormat,
 int maxSimultaneousSounds)
 {
 super(maxSimultaneousSounds);
 this.playbackFormat = playbackFormat;
 localLine = new ThreadLocal();
 localBuffer = new ThreadLocal();
 pausedLock = new Object();
 // notify threads in pool it's okay to start
 synchronized (this) {
 notifyAll();
 }
 }
 /**
 Gets the maximum number of simultaneous sounds with the
 specified AudioFormat that the default mixer can play.
 */
 public static int getMaxSimultaneousSounds(
 AudioFormat playbackFormat)
 {
 DataLine.Info lineInfo = new DataLine.Info(
 SourceDataLine.class, playbackFormat);
 Mixer mixer = AudioSystem.getMixer(null);
 return mixer.getMaxLines(lineInfo);
 }
 /**
 Does any clean up before closing.
 */
 protected void cleanUp() {
 // signal to unpause
 setPaused(false);
 // close the mixer (stops any running sounds)
 Mixer mixer = AudioSystem.getMixer(null);
 if (mixer.isOpen()) {
 mixer.close();
 }
 }
 public void close() {
 cleanUp();
 super.close();
 }
 public void join() {
 cleanUp();
 super.join();
 }
 /**
 Sets the paused state. Sounds may not pause immediately.
 */
 public void setPaused(boolean paused) {
 if (this.paused != paused) {
 synchronized (pausedLock) {
 this.paused = paused;
 if (!paused) {
 // restart sounds
 pausedLock.notifyAll();
 }
 }
 }
 }
 /**
 Returns the paused state.
 */
 public boolean isPaused() {
 return paused;
 }
 /**
 Loads a Sound from the file system. Returns null if an
 error occurs.
 */
 public Sound getSound(String filename) {
 return getSound(getAudioInputStream(filename));
 }
 /**
 Loads a Sound from an AudioInputStream.
 */
 public Sound getSound(AudioInputStream audioStream) {
 if (audioStream == null) {
 return null;
 }
 // get the number of bytes to read
 int length = (int)(audioStream.getFrameLength() *
 audioStream.getFormat().getFrameSize());
 // read the entire stream
 byte[] samples = new byte[length];
 DataInputStream is = new DataInputStream(audioStream);
 try {
 is.readFully(samples);
 }
 catch (IOException ex) {
 ex.printStackTrace();
 }
 // return the samples
 return new Sound(samples);
 }
 /**
 Creates an AudioInputStream from a sound from the file
 system.
 */
 public AudioInputStream getAudioInputStream(String filename) {
 try {
 // open the source file
 AudioInputStream source =
 AudioSystem.getAudioInputStream(new File(filename));
 // convert to playback format
 return AudioSystem.getAudioInputStream(
 playbackFormat, source);
 }
 catch (UnsupportedAudioFileException ex) {
 ex.printStackTrace();
 }
 catch (IOException ex) {
 ex.printStackTrace();
 }
 catch (IllegalArgumentException ex) {
 ex.printStackTrace();
 }
 return null;
 }
 /**
 Plays a sound. This method returns immediately.
 */
 public InputStream play(Sound sound) {
 return play(sound, null, false);
 }
 /**
 Plays a sound with an optional SoundFilter, and optionally
 looping. This method returns immediately.
 */
 public InputStream play(Sound sound, SoundFilter filter,
 boolean loop)
 {
 InputStream is;
 if (sound != null) {
 if (loop) {
 is = new LoopingByteInputStream(
 sound.getSamples());
 }
 else {
 is = new ByteArrayInputStream(sound.getSamples());
 }
 return play(is, filter);
 }
 return null;
 }
 /**
 Plays a sound from an InputStream. This method
 returns immediately.
 */
 public InputStream play(InputStream is) {
 return play(is, null);
 }
 /**
 Plays a sound from an InputStream with an optional
 sound filter. This method returns immediately.
 */
 public InputStream play(InputStream is, SoundFilter filter) {
 if (is != null) {
 if (filter != null) {
 is = new FilteredSoundStream(is, filter);
 }
 runTask(new SoundPlayer(is));
 }
 return is;
 }
 /**
 Signals that a PooledThread has started. Creates the
 Thread's line and buffer.
 */
 protected void threadStarted() {
 // wait for the SoundManager constructor to finish
 synchronized (this) {
 try {
 wait();
 }
 catch (InterruptedException ex) { }
 }
 // use a short, 100ms (1/10th sec) buffer for filters that
 // change in real-time
 int bufferSize = playbackFormat.getFrameSize() *
 Math.round(playbackFormat.getSampleRate() / 10);
 // create, open, and start the line
 SourceDataLine line;
 DataLine.Info lineInfo = new DataLine.Info(
 SourceDataLine.class, playbackFormat);
 try {
 line = (SourceDataLine)AudioSystem.getLine(lineInfo);
 line.open(playbackFormat, bufferSize);
 }
 catch (LineUnavailableException ex) {
 // the line is unavailable - signal to end this thread
 Thread.currentThread().interrupt();
 return;
 }
 line.start();
 // create the buffer
 byte[] buffer = new byte[bufferSize];
 // set this thread's locals
 localLine.set(line);
 localBuffer.set(buffer);
 }
 /**
 Signals that a PooledThread has stopped. Drains and
 closes the Thread's Line.
 */
 protected void threadStopped() {
 SourceDataLine line = (SourceDataLine)localLine.get();
 if (line != null) {
 line.drain();
 line.close();
 }
 }
 /**
 The SoundPlayer class is a task for the PooledThreads to
 run. It receives the thread's Line and byte buffer from
 the ThreadLocal variables and plays a sound from an
 InputStream.
 <p>This class only works when called from a PooledThread.
 */
 protected class SoundPlayer implements Runnable {
 private InputStream source;
 public SoundPlayer(InputStream source) {
 this.source = source;
 }
 public void run() {
 // get line and buffer from ThreadLocals
 SourceDataLine line = (SourceDataLine)localLine.get();
 byte[] buffer = (byte[])localBuffer.get();
 if (line == null || buffer == null) {
 // the line is unavailable
 return;
 }
 // copy data to the line
 try {
 int numBytesRead = 0;
 while (numBytesRead != -1) {
 // if paused, wait until unpaused
 synchronized (pausedLock) {
 if (paused) {
 try {
 pausedLock.wait();
 }
 catch (InterruptedException ex) {
 return;
 }
 }
 }
 // copy data
 numBytesRead =
 source.read(buffer, 0, buffer.length);
 if (numBytesRead != -1) {
 line.write(buffer, 0, numBytesRead);
 }
 }
 }
 catch (IOException ex) {
 ex.printStackTrace();
 }
 }
 }
}


The SoundManager class extends the ThreadPool class, which we moved to the com.brackeen.javagamebook.util package. The SoundManager class has an inner class, SoundPlayer, which does the work of copying sound data to a Line. SoundPlayer is an implementation of the Runnable interface, so it can be used as a task for a thread in the thread pool. An addition to SoundPlayer over SimpleSoundPlayer is that it stops copying data if the SoundManager is in the paused state. If it is in the paused state, SoundPlayer calls wait(), which causes the thread to wait idly until it is notified. SoundManager notifies all waiting threads when it is unpaused.

Thread-Local Variables

One thing you wanted to accomplish in SoundManager is to make sure each thread has its own Line and byte buffer so you can reuse them without having to create new objects every time a sound is played. To give each thread in the thread pool its own Line and byte buffer, you'll take advantage of thread-local variables. Whereas local variables are variables that are local to a block of code, thread-local variables are variables that have a different value for every thread. In this example, the SoundManager class has the thread-local variables, localLine and localBuffer. Each thread that accesses these variables can have its own Line and byte buffer, and no other thread can access another thread's local variables. Thread-local variables are created with the ThreadLocal class. For thread-local variables to work, you need to cheat a little here and update the ThreadPool class. You need a way to create the thread-local variables when a thread starts and to do any cleanup of the thread-local variables when the thread dies. To do this, in PooledThread, signal the ThreadPool class when each thread starts and stops:

public void run() {
 // signal that this thread has started
 threadStarted();
 while (!isInterrupted()) {
 // get a task to run
 Runnable task = null;
 try {
 task = getTask();
 }
 catch (InterruptedException ex) { }
 // if getTask() returned null or was interrupted,
 // close this thread.
 if (task == null) {
 break;
 }
 // run the task, and eat any exceptions it throws
 try {
 task.run();
 }
 catch (Throwable t) {
 uncaughtException(this, t);
 }
 }
 // signal that this thread has stopped
 threadStopped();
}


In the ThreadPool class, the threadStarted() and threadStopped() methods don't do anything, but in SoundManager, they're put to use. The threadStarted() method creates a new Line and a new byte buffer, and adds them to the thread-local variables. In the threadStopped() method, the Line is drained and closed. Besides reducing the lag and enabling you to pause playing sounds, SoundManager provides easier methods for playing sound. It takes care of the creation of ByteArrayInputStreams or FilteredSoundStreams, so all you have to do is pass it a Sound object and an optional SoundFilter. That's it for the nifty new sound manager. So far, you've learned to play sound, use sound filters, and even emulate 3D sound. Now you'll move on to the other sound topic: music.



   
Comments