On Day 11, you learned all about sound and how it is used in computer games, as well as how to find and record your own sounds. However, you didn't learn anything about how to actually implement sound in Java. Sure, you might have sampled a bunch of neat sounds, but they aren't of much use until you under-stand how to play them in a real applet.
Today you learn all about sound and how it works in Java. You find out all the not-so-gory details about how sound is represented in Java, along with the classes and methods used to load and play sounds. It turns out that the current release of Java has pretty limited support for sound. Nevertheless, more than enough audio support is there to liven up Java games. You finish up today's lesson by using the sound support in Java 1.0 to build a pretty neat applet that plays multiple sound effects.
The following topics are covered in today's lesson:
The current sound support in Java comes in the form of a class and a few methods in the Applet class. The AudioClip class, which is part of the applet package, models a digital audio sound clip in the AU file format. You learn about this class next. You learn about the methods supporting sound in the Applet class later in today's lesson.
The AU file format, which you learned about in yesterday's lesson, is currently the only sound format supported by Java. If you recall, it is designed around 8,000 Hz mono 8-bit ULAW encoded audio clips. This is a fairly low-quality sound format, and it severely limits Java in providing professional audio capabilities. However, in the current context of the Web, just being able to play AU audio clips in Java is plenty for many applets.
As far as games go, the quality of the sound isn't always as crucial as you might think. Many sound effects (animal noises, for example) don't require very high-quality audio. You find this out firsthand in this lesson when you implement an applet using various animal sound effects.
The AudioClip class models a sound clip in Java. It is an abstract class, so you can't directly create instances of it. The only way to create AudioClip objects is by calling one of the getAudioClip methods of the Applet class. You'll learn more about that in a moment. But first, take a look at the methods in the AudioClip class.
public abstract void play()
public abstract void loop()
public abstract void stop()
As you can see, these methods are very high-level and quite simplistic. You can't ask for a much easier interface than just calling play to play an audio clip and stop to stop an audio clip. The only twist is the loop method, which plays an audio clip repeatedly in a loop until you explicitly call stop to stop it. The loop method is useful when you have an audio clip that needs to be repeated, such as a music clip or a footstep sound.
None of the methods in AudioClip require parameters; the AudioClip object is entirely self-contained. For this reason, there isn't a lot to learn about using the AudioClip class. By simply understanding the three methods implemented by the AudioClip class (play, loop, stop), you are practically already a Java sound expert!
Before your ego gets too inflated, remember that you still haven't learned the details of how to create an AudioClip object. As I mentioned a little earlier, you use one of the Applet class's getAudioClip methods to create and initialize an AudioClip object. The two versions of getAudioClip are as follows:
public AudioClip getAudioClip(URL url)
public AudioClip getAudioClip(URL url, String name)
Note |
I mentioned earlier that you can't create an AudioClip object directly because AudioClip is an abstract class. Because the class is abstract, you might be wondering how an AudioClip object can be created at all. Technically, it is impossible to ever create an object based on an abstract class. However, in the case of AudioClip, you use the getAudioClip method to get a platform-specific AudioClip derived object. In other words, getAudioClip acts as a native method that returns a native class derived from AudioClip. The method and class are native because sound support varies so widely on different platforms. The purpose of the AudioClip, therefore, is to standardize the interface for the native audio clip classes, which results in a general, platform-independent programming solution. |
The only difference between these two getAudioClip methods is whether or not the URL parameter contains a complete reference to the name of the audio clip. In the first version, it is assumed that the URL contains the complete name; the second version uses the name in a separate name parameter. You will typically use the second version, because you can easily retrieve the base URL of the applet or the HTML document in which the applet is embedded. You do this by using either the getCodeBase or the getDocumentBase method of Applet, like this:
AudioClip clip1 = getAudioClip(getCodeBase(), "sound1.au");
AudioClip clip2 = getAudioClip(getDocumentBase(), "sound2.au");
The getCodeBase method returns the base URL of the applet itself, whereas getDocumentBase returns the base URL of the HTML document containing the applet. It is usually smarter to use getCodeBase to specify the base URL for loading resources used by a Java applet. The reason for this is that it is often useful to organize Java applets into a directory structure beneath the HTML documents in which they appear. Furthermore, you usually reference images and sounds either from the same directory where the applet is located or from a subdirectory beneath it.
Tip |
I like to organize images and sounds used by an applet in a directory called Res, beneath the directory containing the actual Java classes. This isolates the executable part of an applet from the resource content used by the applet, resulting in a more organized file structure. |
In the audio discussion thus far, I might have led you to believe that you must use an AudioClip object to play sounds in Java. This isn't entirely true! The truth is that you are only required to create an AudioClip object if you want to play looped sounds. For normal (nonlooped) sounds, you have the option of using one of the play methods in the Applet class instead of using an AudioClip object. The definitions for the play methods implemented in Applet are as follows:
public void play(URL url)
public void play(URL url, String name)
These play methods take exactly the same parameters taken by the getAudioClip methods. In fact, the play methods in Applet actually call getAudioClip to get an AudioClip object and then use the AudioClip object's play method to play the sound. In this way, the Applet play methods basically provide a higher level method of playing audio. This is evident in Listing 12.1, which shows the Java 1.0 source code for the Applet play methods.
Listing 12.1. The Java 1.0 Applet class's play methods.
public void play(URL url) {
AudioClip clip = getAudioClip(url);
if (clip != null) {
clip.play();
}
}
public void play(URL url, String name) {
AudioClip clip = getAudioClip(url, name);
if (clip != null) {
clip.play();
}
}
The Applet play methods both create a temporary AudioClip object and then use it to play the sound by calling its play method. This provides an even higher level of interface to playing nonlooped sounds than the AudioClip class provides.
Now that you are a Java sound expert (at least in theory), it's time to put your newfound knowledge to work in a sample applet. Because this book is ultimately about writing games, it's important for you to never compromise in making your applets as entertaining as possible. For this reason, the applet you're going to develop to demonstrate sound is a little more than a simple sound player. As a matter of fact, it's quite wild! Figure 12.1 shows a screen of the WildAnimals applet, which uses the Java AudioClip class to generate some entertaining results. The source code, executable, images, and sounds for WildAnimals are located on the accompanying CD-ROM.
Figure 12.1 : The WildAnimals sample applet.
The screen shot of WildAnimals doesn't quite convey the real purpose of the applet. So, at this point, I encourage you to run it for yourself from the CD-ROM to get the real effect. Just in case you're the impatient type and choose to skip the wild animal experience, I'll fill you in on what's happening. WildAnimals randomly plays a variety of wild animal sounds to go with the eyes that are staring at you from the darkness.
Now that you know what it does, let's take a look at the implementation of WildAnimals. The WildAnimals class really only defines one member variable beyond the variables required for animation that you've already seen in prior applets:
private AudioClip[] clip = new AudioClip[5];
This array of AudioClip objects is used to store the wild animal sounds.
The init method in WildAnimals initializes the audio clips by calling the getAudioClip method:
public void init() {
// Load and track the images
tracker = new MediaTracker(this);
Eyes.initResources(this, tracker, 0);
// Load the audio clips
clip[0] = getAudioClip(getCodeBase(), "Res/Crow.au");
clip[1] = getAudioClip(getCodeBase(), "Res/Hyena.au");
clip[2] = getAudioClip(getCodeBase(), "Res/Monkey.au");
clip[3] = getAudioClip(getCodeBase(), "Res/Tiger.au");
clip[4] = getAudioClip(getCodeBase(), "Res/Wolf.au");
}
After the audio clips are initialized in init using getAudioClip, they are ready to be played.
The eyes you see in the applet are implemented as sprites, which you'll learn about later in today's lesson. These sprites are created in the run method, which also creates and initializes the sprite list. Listing 12.2 contains the source code for the run method.
Listing 12.2. The WildAnimals class's run method.
public void run() {
try {
tracker.waitForID(0);
}
catch (InterruptedException e) {
return;
}
// Create and add the sprites
sv = new SpriteVector(new ColorBackground(this, Color.black));
for (int i = 0; i < 8; i++) {
sv.add(new Eyes(this, new Point(Math.abs(rand.nextInt() %
size().width), Math.abs(rand.nextInt() % size().width)),
i % 2, Math.abs(rand.nextInt() % 200)));
}
// Update everything
long t = System.currentTimeMillis();
while (Thread.currentThread() == animate) {
// Update the animations
sv.update();
repaint();
// Play an animal sound
if ((rand.nextInt() % 15) == 0)
clip[Math.abs(rand.nextInt() % 5)].play();
try {
t += delay;
Thread.sleep(Math.max(0, t - System.currentTimeMillis()));
}
catch (InterruptedException e) {
break;
}
}
}
Beyond creating the sprites for WildAnimals, the run method also handles playing the random animal sounds. This is carried out by using the nextInt method of the Random object to get a random number between -15 and 15. This random number is checked to see whether it is equal to 0, in which case a sound is played. This creates a 1-in-31 chance of a sound being played each time through the update loop. There is no magic surrounding the range of the random numbers; it was determined by trying out different values. When a sound is to be played, nextInt is used again to randomly select which sound to play. That's all there is to playing the random sounds.
That covers all the unique aspects of the WildAnimals class. However, you still haven't seen how the eye sprites are implemented. The Eye class implements a blinking eye sprite that can be either small or large. It uses a static two-dimensional array of Image objects to store the frame animations for the blinking eye in each size. Like all derived Sprite classes you've seen, the images are initialized in the initResources method:
public static void initResources(Applet app, MediaTracker tracker, int id) {
for (int i = 0; i < 4; i++) {
image[0][i] = app.getImage(app.getCodeBase(), "Res/SmEye" + i + ".gif");
tracker.addImage(image[0][i], id);
image[1][i] = app.getImage(app.getCodeBase(), "Res/LgEye" + i + ".gif");
tracker.addImage(image[1][i], id);
}
}
Figure 12.2 shows what the animation images for the eye look like.
Figure 12.2 : The images used by the Eye class.
The Eye class contains two member variables, blinkDelay and blinkTrigger, for managing the rate at which it blinks:
protected int blinkDelay,
blinkTrigger;
blinkDelay determines how long the eye waits until it blinks again, and blinkTrigger is the counter used to carry out the wait. They are both initialized to the blink delay parameter passed into the constructor of Eye:
public Eyes(Component comp, Point pos, int i, int bd) {
super(comp, image[i], 0, 1, 2, pos, new Point(0, 0), 0, Sprite.BA_WRAP);
blinkTrigger = blinkDelay = bd;
}
The only overridden method in Eye is incFrame, which handles incrementing the animation frame:
protected void incFrame() {
if ((frameDelay > 0) && (--frameTrigger <= 0) &&
(--blinkTrigger <= 0)) {
// Reset the frame trigger
frameTrigger = frameDelay;
// Increment the frame
frame += frameInc;
if (frame >= 4) {
frame = 3;
frameInc = -1;
}
else if (frame <= 0) {
frame = 0;
frameInc = 1;
blinkTrigger = blinkDelay;
}
}
}
It is necessary to override incFrame so that you can add the blinking functionality. This is done by decrementing blinkTrigger and seeing whether it has reached zero. If so, it's time to blink! Notice that the blink is still dependent on the frame delay, which is very important. This is important because you don't want an added feature, such as blinking, to interrupt a basic function of the sprite, such as the frame delay.
The incFrame method does one other thing worth pointing out. If you think about it, a blink must consist of going through the frame animations forward (to close the eye) and then backward (to open the eye again). The standard implementation of incFrame in Sprite, which you saw last week, always goes in a constant direction-that is, forward or backward as determined by the sign of the frameInc member variable. In Eye's incFrame, you want the frame animations to go forward and then backward without having to fool with frameInc. The if-else clause in incFrame solves this problem beautifully.
That finishes up the WildAnimals sample applet. It proves that sound in Java is not only fun, but it is also easy to implement!
Today you learned all about how sound is used in Java. You started off by learning how Java supports sound through the AudioClip class and a few methods in the Applet class. You then progressed to building a complete applet using sound to create somewhat of a virtual wilderness at night. It showed you how easy it is to add sound to Java applets. It also was a good example of how the sprite classes can be used in new and creative ways.
You now have all the background necessary to add sound to any Java applet you write, including games. Speaking of games, it's almost time for you to write another one. But that will have to wait until tomorrow!
Q | Do I have to do anything special to mix sounds in Java? |
A | No. The Java sound support automatically handles mixing sounds that are being played at the same time. This might seem trivial, but it is actually a very nice feature. |
Q | How do I play MIDI music in Java? |
A | Right now, you can't. The current version of Java (1.0) doesn't provide any support for MIDI, but hopefully it will appear in a later release. Sun has promised more extensive multimedia features in the near future. For now, you can record music as an audio clip and then loop it; more on this tomorrow. |
Q | In the WildAnimals applet, how can I make the eyes blink faster? |
A | Decrease the blink delay parameter passed into the constructor. More specifically, decrease the number used in the modulus operation after the call to nextInt. |
Q | How can I add more animals to the WildAnimals applet? |
A | The first step is to record or find more animal sounds and copy them to the Res directory. You then need to increase the size of the clip array of AudioClip objects and load the new sounds in init using the getAudioClip method. Finally, in the call to play in the run method, increase the number used to index into the clip array (it is currently 5). |
The Workshop section provides questions and exercises to help you get a better feel for the material you learned today. Try to answer the questions and at least think about the exercises before moving on to tomorrow's lesson. You'll find the answers to the questions in appendix A, "Quiz Answers."