JaVa
   

Creating a Sound Manager

In the previous examples we have simply looked at how to play single sound effects (i.e., one at a time). As you can guess, it would be a very rare case in a game that you would ever just have a single sound effect; most have hundreds of different sounds, with many being played at the same time. The solution to this is to create a class to manage your sound effects easily, so that is what we are going to do in this section. In Java, there are 64 channels through which sound can be played simultaneously. However, 32 of these channels are reserved for MIDI playback, leaving us with 32 usable channels for our sound effects. So we need to create a class that will handle the loading and playback of sound effects by managing the sound channels for us. We will call this class SoundManager, and we need to implement the LineListener interface, as we need to react to events happening with the sound channels (lines). This class definition can be seen here:

public class SoundManager implements LineListener


Note we have also declared the following members of the class:

private Clip channelArray[];
private int channelSoundIdRef[];
private Vector soundByteData;
public static final int AUTO_ASSIGN_CHANNEL = -1;


channelArray holds an array of the 32 channels, which are known as clips in Java. The second array, called channelSoundIdRef, holds the numbers of the sounds that are playing in the related channels. The third is a vector to hold the actual byte data of each of the sound effects that are loaded into the manager. Finally, we have a definition called AUTO_ASSIGN_CHANNEL, which we will see the use for soon. So now that we know the members, we need to initialize these members in the constructor to the default values. This can be seen in the following block of code:

public SoundManager()
{ // Initialize the vector to hold the sound data...
 soundByteData = new Vector(); channelSoundIdRef = new int[32];
 channelArray = new Clip[32];
 for(int i=0; i<32; i++)
 {
 channelSoundIdRef[i] = -1;
 }
}


So here we create a vector for soundByteData and also allocate storage for an array of 32 integer values to the channelSoundIdRef array. In addition, we allocate storage for 32 clip objects to the channelArray. Finally, we initialize all the elements of the channelSoundIdRef to be –1, as there are no sounds playing in any of the channels when the sound manager is created. Next, we need functionality within the class so that we can load sound files into it. This is going to be achieved by means of an addSound method. Let's look at how we create this now. Take a string parameter into the method, which will denote the relative or absolute path and filename of the sound file that is to be loaded. Then create a File object by passing the string parameter into the File class constructor. This can be seen here:

public int addSound(String filepath)
{
 File soundFile = new File(filepath);


Once we have the soundFile object, we can then check if the sound that is trying to be loaded exists by calling the isFile method of the soundFile object.

if(!soundFile.isFile())
{
 System.out.println("Sound File '"+filepath+"' does not exist!");
 System.exit(1);
}


Next, we need to allocate an array of bytes, which will be used to store the sound data within memory (accessing the file every time we wish to play the file would be sinful!). Do this by calling the length method of our soundFile object to find out the size of the file in bytes, which is returned as a long, so a simple cast to an int is also required. This can be seen in the following line of code:

byte[] tempArray = new byte[(int)soundFile.length()];


Then attempt to read in the file by means of a DataInputStream, which is created by passing in a FileInputStream object that is created by passing in the soundFile object. The read method is then called on the stream with the tempArray empty array of bytes passed into it, so the bytes in the file will be read into the tempArray object. The stream is then closed with the close method. Note also that we enclose this within a try/catch block, as it is possible that an IOException could be thrown. This can be seen here:

try
{
 DataInputStream inputStream = new DataInputStream(new
 FileInputStream(soundFile));
 inputStream.read(tempArray);
 inputStream.close();
} catch(IOException e)
{
 System.out.println(e);
 System.out.println("There was a problem reading the sound file: "+filepath);
 System.exit(1);
}


Next, place the reference to the array of bytes that we have read into the vector that we created in the constructor called soundByteData. This call can be seen here:

soundByteData.add(tempArray);


Finally, return the position in which the sound was added into the vector. This can be used later to reference the sound within the manager (i.e., to play it). This final line of the method can be seen here:

return soundByteData.size()-1;


Okay, so we can now load the sound data into memory. Next we need functionality to actually play the sounds, so we will create a method called play to do just that. The play method will take three parameters, the first being the ID of the sound to be played (remember the addSound method returned the sound's ID). The second will be a Boolean value to state whether the sound should be looped or not, and the final parameter specifies on which channel (0-31) the sound should be played. This is declared as follows:

public void play(int soundId, boolean loop, int channelId)


If you remember, we made a definition called AUTO_ASSIGN_CHANNEL that can be used instead of a channel ID, which will tell the method to automatically assign a free channel to the sound. First ensure that the channel is a valid number (either 0-31 or –1—the AUTO_ASSIGN_CHANNEL value). This can be accomplished with the following simple if statement.

if(channelId < -1 || channelId >= 32)
{
 System.out.println("Channel ID was out of range");
 return; }


Next, we need to ensure that the soundId that has been specified is contained within the range of the soundByteData vector, so we can check this with the following if statement:

// Check the soundId is valid...
 if(soundId < 0 || soundId >= soundByteData.size())
 {
 System.out.println("Sound ID was out of range");
 return; }


Now that we know the parameters are valid, we then need to either assign the sound to a channel or find a suitable channel, depending on the channelId parameter. First, however, create a temporary integer called validChannelId, which will be used regardless of whether the channel is auto assigned or not to hold the final channel to be used. Initially, this will be set to –1, as in no channel has been found. So if a channel must be assigned automatically, we need to loop through the array of channels (channelArray) to find a channel that is not currently being used (either it will be null or not open). If we find one, simply assign it to the validChannelId variable, then break out of the loop. This can be seen in the following block of code:

for(int i=0; i<32; i++)
{
 if(channelArray[i] == null || !channelArray[i].isOpen())
 {
 // this one will do...
 validChannelId = i;
 break;
 } }


Next, check if a valid channel could be found and if not, return from the method without doing anything else. This can be seen here:

if(validChannelId == -1)
{
 System.out.println("Could not find a suitable channel");
 return;
}


Alternatively, if a channel ID has been specified, we need to first stop the channel if it is currently playing, which is handled by the stopChannel method that we will implement soon. Then once we have ensured the channel has stopped, assign that channel ID to the validChannelId variable. This can be seen in the following two lines of code:

stopChannel(channelId);
validChannelId = channelId;


Next, once we have a valid channel, we need to try and obtain an AudioInputStream, using the sound data from within the correct element of the soundByteData vector (specified by the soundId parameter that was passed into the method).

try
{
 AudioInputStream audioInputStream = AudioSystem
 .getAudioInputStream(new ByteArrayInputStream((byte[])
 soundByteData.get(soundId)));


After we have our input stream, we need to get the format of the audio from it so we can then in turn get a line that is capable of playing that audio format. Obtaining the format can be seen in the following line of code:

AudioFormat audioFormat = audioInputStream.getFormat();


Once we have the format, we can then set up the line with the following line of code (as we did in the previous examples).

DataLine.Info dataLineInfo = new
 DataLine.Info(Clip.class,
 audioInputStream.getFormat(),
 ((int)audioInputStream.getFrameLength() *
 audioFormat.getFrameSize()));


We then get a line by calling the static getLine method to play the sound and store the assigned clip (channel) into the correct position in the channelArray array (denoted by the validChannelId variable). This can be seen here:

channelArray[validChannelId] = (Clip)
 AudioSystem.getLine(dataLineInfo);


Next, we need to add a LineListener to the channel to handle the stopping of the audio, which we will look at the implementation for soon. The code to add the LineListener can be seen here:

channelArray[validChannelId].addLineListener(this);


Then, open the stream and note the sound ID that is playing in the channel in the channelSoundIdRef array. This can be seen here:

channelArray[validChannelId].open(audioInputStream);
channelSoundIdRef [validChannelId] = soundId;


We then check if the sound is to be played once or looped indefinitely and react accordingly to it. This can be seen in this block of code:

if(loop == true)
 channelArray[validChannelId].loop(Clip.LOOP_CONTINUOUSLY);
else
 channelArray[validChannelId].loop(1);


Note that we have been within a try block since we attempted to obtain the AudioInputStream, so we need to catch any possible exceptions that could have been thrown. The exception handling code can be seen here:

catch(Exception e)
{ System.out.println(e);
 if(channelArray[validChannelId] != null)
 {
 if(channelArray[validChannelId].isOpen())
 {
 channelArray[validChannelId].close();
 }
 channelArray[validChannelId] = null;
 channelSoundIdRef[validChannelId] = -1;
 }
}


All we are doing here is cleaning the channel if something goes wrong by closing it if it is open and then setting it to null and the channelSoundIdRef back to –1, meaning there is no sound (ID) playing in that channel. So far, we can add sounds and play them. Now let's look at stopping them! For this, we will implement two methods—the first for stopping a sound and the second for stopping a channel directly. The first will take a single integer parameter, which will denote the ID of the sound that needs to be stopped. Of course, it is possible that more than one occurrence of the same sound may be playing, so in this method we will just stop the first occurrence of the sound. The complete method can be seen here:

public void stop(int soundId)
{
 // find the first occurrence of the sound and stop it...
 for(int i=0; i<32; i++)
 {
 if(channelSoundIdRef[i] == soundId)
 {
 // reset the channel...
 System.out.println("Stopping Channel "+i);
 channelArray[i].stop();
 break;
 }
 }
}


As you can see, we loop through the channelSoundIdRef trying to find the requested soundId, and if we find it, we then stop the associated channel and break out of the method. Simple! The second method is called stopChannel and allows a channel to be stopped directly by passing in a channel ID (0-31). The complete definition of this method can be seen here:

public void stopChannel(int channelId)
{
 if(isChannelPlaying(channelId))
 {
 channelArray[channelId].stop();
 // note the 'update' method closes the channel properly
 }
}


So basically, all we do here is take in the channel ID as an integer parameter and check if it is playing by calling the isChannelPlaying method (which we will create next), passing in the channel ID. If it is playing, we simply call the stop method of the channel.

Note 

Each time the stop method is called, it will trigger an event in the LineListener, which we will look at soon.

In the previous method, we called the method isChannelPlaying, so let's define that now. All we want this method to do is return a Boolean denoting whether the specified channel is playing or not. We can tell if it is playing if the channel is not null and is open so we can declare the method as follows.

public boolean isChannelPlaying(int channelId)
{
 return (channelArray[channelId] != null &&
 channelArray[channelId].isOpen());
}


It may also be useful if we had a method that could tell us if a sound was playing or not, so let's make a method that can do just that. This method will be called isSoundPlaying and will take a sound ID (integer) as a parameter. All this method needs to do is loop through the channelSoundIdRef array and see if it can match any of the values within the array to the sound ID that was passed into the method. The complete definition for this method can be seen here:

public boolean isSoundPlaying(int soundId)
{
 // check to see if any occurrence of the sound is playing...
 for(int i=0; i<32; i++)
 {
 if(channelSoundIdRef[i] == soundId)
 {
 return true;
 }
 }
 return false;
}


Next, because we implemented the LineListener in this class, we need to implement the update method that is called every time a LineEvent occurs. However, we are only interested in when a line's stop method is called, which generates a LineEvent.Type.STOP event. So the first thing we need to do is define our method and check for this event, which is done with the following few lines of code:

public void update(LineEvent e)
{
 // handles samples stopping...
 if(e.getType() == LineEvent.Type.STOP)
 {


If we get a stop event, we basically have to update our member information to reflect it, so we first need to cycle through our list of channels to find out which one triggered the event. We can do this by comparing the elements of the channelArray to the getLine method of the LineEvent object e that was passed into the update method. Once we find the line, we can then set the reference to null and also update the correct element in the channelSoundIdRef array to say there is no sound playing in that channel. This can be seen in the following block of code:

for(int i=0; i<32; i++)
{
 if(channelArray[i] == e.getLine())
 {
 // reset the channel...
 System.out.println("Closing Channel "+i);
 channelArray[i] = null; channelSoundIdRef[i] = -1;
 }
}


Finally, all we need to do in this method is close the line (channel), which we do by calling the close method that can be seen in the following line of code:

e.getLine().close();


The last method we are going to implement in our sound manager is one to allow us to stop all the channels playing. To do this, all we need to do is cycle through the 32 channels, calling the stopChannel method for each one. This can be seen in this final block of code:

public void stopAllChannels()
{
 // stop active channels...
 for(int i=0; i<32; i++)
 stopChannel(i);
}


Before we move on, let's look at the complete code listing for the sound manager.

Code Listing 11-7: The sound manager
import java.util.*;
import java.io.*;
import javax.sound.sampled.*; // Import the Sound API public class SoundManager implements LineListener
{
 public SoundManager()
 { // Initialize the vector to hold the sound data...
 soundByteData = new Vector(); channelSoundIdRef = new int[32];
 channelArray = new Clip[32];
 for(int i=0; i<32; i++)
 {
 channelSoundIdRef[i] = -1;
 }
 }
 public int addSound(String filepath)
 {
 File soundFile = new File(filepath);
 if(!soundFile.isFile())
 {
 System.out.println("Sound File '"+filepath+"' does not
 exist!");
 System.exit(1);
 }
 byte[] tempArray = new byte[(int)soundFile.length()];
 try
 {
 DataInputStream inputStream = new DataInputStream(new
 FileInputStream(soundFile));
 inputStream.read(tempArray);
 inputStream.close();
 } catch(IOException e)
 {
 System.out.println(e);
 System.out.println("There was a problem reading the sound file: "+filepath);
 System.exit(1);
 }
 // Add it to the vector...
 soundByteData.add(tempArray);
 // return its position in the vector...
 return soundByteData.size()-1;
 }
 public void play(int soundId, boolean loop, int channelId)
 {
 // Check the channelId is valid...
 if(channelId < -1 || channelId >= 32)
 {
 System.out.println("Channel ID was out of range");
 return; }
 // Check the soundId is valid...
 if(soundId < 0 || soundId >= soundByteData.size())
 {
 System.out.println("Sound ID was out of range");
 return; }
 int validChannelId = -1;
 if(channelId == AUTO_ASSIGN_CHANNEL)
 {
 // we need to find a suitable channel...
 // first find a free channel...
 for(int i=0; i<32; i++)
 {
 if(channelArray[i] == null ||
 !channelArray[i].isOpen())
 {
 // this one will do...
 validChannelId = i;
 break;
 } }
 if(validChannelId == -1)
 {
 System.out.println("Could not find a suitable
 channel");
 return;
 }
 }
 else
 {
 // we need to ensure the selected channel is stopped...
 stopChannel(channelId);
 // set the valid channel id...
 validChannelId = channelId;
 }
 System.out.println("Allocating Channel "+validChannelId);
 try
 {
 AudioInputStream audioInputStream =
 AudioSystem.getAudioInputStream(new
 ByteArrayInputStream((byte[])
 soundByteData.get(soundId)));
 // retrieve the audio format...
 AudioFormat audioFormat = audioInputStream.getFormat();
 // set the line up
 DataLine.Info dataLineInfo = new
 DataLine.Info(Clip.class,
 audioInputStream.getFormat(),
 ((int)audioInputStream.getFrameLength() *
 audioFormat.getFrameSize()));
 // assign a clip (channel) for the sample
 channelArray[validChannelId] = (Clip)
 AudioSystem.getLine(dataLineInfo);
 channelArray[validChannelId].addLineListener(this);
 channelArray[validChannelId].open(audioInputStream);
 channelSoundIdRef[validChannelId] = soundId;
 // play the clip (or loop it)
 if(loop == true)
 channelArray[validChannelId].loop
 (Clip.LOOP_CONTINUOUSLY);
 else
 channelArray[validChannelId].loop(1);
 }
 catch(Exception e)
 { System.out.println(e);
 if(channelArray[validChannelId] != null)
 {
 if(channelArray[validChannelId].isOpen())
 {
 channelArray[validChannelId].close();
 }
 channelArray[validChannelId] = null;
 channelSoundIdRef[validChannelId] = -1;
 }
 }
 }
 public void stop(int soundId)
 {
 // find the first occurrence of the sound and stop it...
 for(int i=0; i<32; i++)
 {
 if(channelSoundIdRef[i] == soundId)
 {
 // reset the channel...
 System.out.println("Stopping Channel "+i);
 channelArray[i].stop();
 break;
 }
 }
 }
 public void stopChannel(int channelId)
 {
 if(isChannelPlaying(channelId))
 {
 channelArray[channelId].stop();
 // note the 'update' method closes the channel properly
 }
 }
 public boolean isChannelPlaying(int channelId)
 {
 return (channelArray[channelId] != null &&
 channelArray[channelId].isOpen());
 }
 public boolean isSoundPlaying(int soundId)
 {
 // check to see if any occurence of the sound is playing...
 for(int i=0; i<32; i++)
 {
 if(channelSoundIdRef[i] == soundId)
 {
 return true;
 }
 }
 return false;
 }
 public void update(LineEvent e)
 {
 // handles samples stopping...
 if(e.getType() == LineEvent.Type.STOP)
 {
 // find the channel this line relates to...
 for(int i=0; i<32; i++)
 {
 if(channelArray[i] == e.getLine())
 {
 // reset the channel...
 System.out.println("Closing Channel "+i);
 channelArray[i] = null; channelSoundIdRef[i] = -1;
 }
 }
 // close the line...
 e.getLine().close();
 } }
 public void stopAllChannels()
 {
 // stop active channels...
 for(int i=0; i<32; i++)
 stopChannel(i);
 }
 private Clip channelArray[];
 private int channelSoundIdRef[];
 private Vector soundByteData;
 public static final int AUTO_ASSIGN_CHANNEL = -1;
}


Java End example

So there you have it—a cool sound manager that will allow you to play up to 32 sounds simultaneously!

JaVa
   
Comments