VidWorks Entertainment New Developer Training Program
Tutorial #7
It's official. We're done with graphics. I believe we've taught you everything we can. From here on, it's all up to you. At least graphics-wise it is. Although once in a while, maybe Dwayne might drop in with some clever new technique. Maybe.
Anyways, onto Sound & Music!
Sound
For sound and music, we'll be using the Java Sound API, which is already part of the JDK and JRE. The Java Sound API is very easy to use, and at the same time, very powerful. You can find it in packages javax.sound.sampled (for WAVs, AIFFs, AUs, and eventually, MP3s) and javax.sound.midi (for, um, MIDIs).
The core of the sampled sound package is a class called AudioSystem. As far as you and I are concerned, we will always be dealing with this class statically and as a utility for getting access to other sound resources.
Now, usually, we'll be playing sounds off a file. On Windows, this is usually a WAV file. In order to read the file off the disk, we'll be using something called an AudioInputStream. The AudioSystem class has this method that we can use:
static AudioInputStream getAudioInputStream(File
file)
But it's not enough to just have opened an audio file. We need something to play that file for us. In Java Sound, we'll use an object called a Clip. We can obtain a clip using the following AudioSystem method:
static Clip getClip()
Now, we have an AudioInputStream and a Clip. How do we load the AudioInputStream into the Clip? With this Clip member function:
void open(AudioInputStream stream)
Finally, to actually play the sound:
void start()
Note that the start method sets the sound to play in the background. The current thread can continue to run while the sound is playing. Also note that the sound playing doesn't constitute an actual thread, and if all other threads end, the sound will automatically end as well.
To stop the sound, we have this method:
void stop()
Note that the stop method only stops the sound. It doesn't rewind the position, so if you start the sound back up again, it'll continue playing from where it left off. It's more of a pause than the stop. If you want to reset the position, you can always use:
void setFramePosition(int
frames)
void setMicrosecondPosition(long microseconds)
where the first position is zero.
Also, after you're done using the Clip, be sure to close() both the Clip and the AudioInputStream.
Anyways, a code sample:
/* SoundDemo.java */ import java.io.*; import javax.sound.sampled.*; public class SoundDemo { public
static void main(String[] args) { try
{ //
get the sound clip AudioInputStream myStream = AudioSystem.getAudioInputStream(new File("tada.wav")); Clip
myClip = AudioSystem.getClip(); myClip.open(myStream); //
play the clip and wait myClip.start(); System.out.println("Playing sound..."); Thread.sleep(3000); System.out.println("The thread wakes up..."); //
close the sound clip myClip.stop(); myClip.close(); myStream.close(); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
(No screenshot will be provided since this is command line and is very non-graphical. In fact, I foresee not having any more screenshots for the rest of the series.)
See? I told you Java Sound was very easy. Anyways, the code sample has some println's to demonstrate that the playing of the sound occurs outside of the current thread of execution. Thus, we have to put the thread to sleep if we don't want the Clip to immediately stop after starting up again.
Playing Sounds Simultaneously
This is a very trivial extension. If we want to play two sounds simultaneously, all we have to do is open up two AudioInputStreams and two Clips, and Java will automatically do it for us (assuming our hardware can handle it--most can).
I'll jump right into the code sample:
/* SoundDemo2.java */ import java.io.*; import javax.sound.sampled.*; public class SoundDemo2 { public
static void main(String[] args) { try
{ //
get first sound clip AudioInputStream stream1 = AudioSystem.getAudioInputStream(new File("tada.wav")); Clip
clip1 = AudioSystem.getClip(); clip1.open(stream1); //
get second sound clip AudioInputStream stream2 = AudioSystem.getAudioInputStream(new File("scream.wav")); Clip
clip2 = AudioSystem.getClip(); clip2.open(stream2); //
start both clips clip1.start(); clip2.start(); //
wait while sounds play Thread.sleep(3000); //
close the sound clips and streams clip1.stop(); clip2.stop(); clip1.close(); clip2.close(); stream1.close(); stream2.close(); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
Playing Sounds Sequentially
In order to play two sounds, one right after the other, we need to know when the first sound ended. There are two ways to do this: the first is to ask how long the first sound is, then Thread.sleep()'ing for that exact amount. Unfortunately, that's rather cumbersome and clunky.
Fortunately, there's a much better way. Remember events? That's right, we can get an event when a sound stops (or starts, is opened, or is closed for that matter).
First, we need to have our class implement the LineListener, which has the following method:
void update(LineEvent event)
The LineEvent object has the following methods:
long getFramePosition() - returns the next frame to be played, orr AudioSystem.NOT_SPECIFIED
Line getLine() - note that a Clip is a sub-class (well, technically sub-interface) of a Line
LineEvent.Type getType() - can be LineEvent.Type.OPEN, LineEvent.Type.START, LineEvent.Type.STOP, or LineEvent.Type.CLOSE
Using these 3 methods, we can find out which Line (in our case, Clip) generated the event, what kind of event it was, and where in the audio is the Clip.
To register our class as a LineListener, we can call the following method on our Clip:
void addLineListener(LineListener
listener)
Now, we know everything we need to know to play sounds sequentially, so I'll give you this code sample:
/* SoundDemo3.java */ import java.io.*; import javax.sound.sampled.*; public class SoundDemo3 implements LineListener { AudioInputStream stream1, stream2; Clip
clip1, clip2; //
returns true if successful public boolean initDemo() { try
{ //
get first sound clip stream1
= AudioSystem.getAudioInputStream(new File("tada.wav")); clip1
= AudioSystem.getClip(); clip1.open(stream1); //
get second sound clip stream2
= AudioSystem.getAudioInputStream(new File("scream.wav")); clip2
= AudioSystem.getClip(); clip2.open(stream2); //
add as a line listener clip1.addLineListener(this); clip2.addLineListener(this); }
catch (Exception e) { System.out.println(e.getMessage()); return
false; } return
true; } public
void update(LineEvent event) { Line
eventLine = event.getLine(); LineEvent.Type eventType = event.getType(); if
(eventLine == clip1 && eventType
== LineEvent.Type.STOP) { //
clip 1 has stopped, start clip 2 clip2.start(); }
else if (eventLine == clip2 && eventType == LineEvent.Type.STOP)
{ //
clip 2 has stopped, clean up and go home clip1.close(); clip2.close(); try
{ stream1.close(); stream2.close(); }
catch (IOException e) { System.out.println("Unknown error while closing streams!"); }
finally { System.exit(0); } } } public
static void main(String[] args) { //
initialize the demo SoundDemo3
demo = new SoundDemo3(); if
(!demo.initDemo()) { System.out.println("Error initializing demo!"); System.exit(1); } //
start the first clip demo.clip1.start(); //
give the sounds time to play //
if they don't give an event when they stop, timeout after 15 seconds try
{ Thread.sleep(15000); }
catch (InterruptedException e) { System.out.println("Unknown error in main()"); } } } |
First of all, note that we had to move almost everything out of our main() method. Why? Because only an instantiated object can be a LineListener, so we had to move everything to a non-static context.
Just a brief explanation: in our update() method, if we detect that clip 1 has stopped, we play clip 2. If we detect that clip 2 has stopped, we close all our Clips and AudioInputStreams and quit.
Finally, notice that we have to sleep at the end of our main() method. This is because the threads that play the sounds aren't actually threads, so if main() ends, the program ends and the sounds stop. There is most likely a way to change this behavior. However, that is outside the scope of this tutorial and will be left to the reader if he or she is interested in the subject.
Looping Sounds
What if you want to play a sound in a loop? Well, we could just wait for a sound stopped event and then reposition the Clip and start it up again. However, there is a much simpler solution that's actually more powerful.
The Clip object has these following loop related methods:
void setLoopPoints(int startFrame, int endFrame) - the first frame is 0; you can also use -1 for the end
void loop(int count) - loops for the specified number of timess, or loops continuously if count is equal to Clip.LOOP_CONTINUOUSLY
Now, often times, we don't know anything about the frames in the sound file, only about the time. Ideally, we should be able to specify loop points in terms of microseconds or something. Failing that, we could just use these Clip member methods to derive a conversion from time to frames:
int getFrameLength()
long getMicrosecondLength()
Now, you should know enough to understand this code sample without any explanation:
/* SoundLoop.java */ import java.io.*; import javax.sound.sampled.*; public class SoundLoop { public
static void main(String[] args) { try
{ //
get the sound clip AudioInputStream myStream = AudioSystem.getAudioInputStream(new File("tada.wav")); Clip
myClip = AudioSystem.getClip(); myClip.open(myStream); //
calculate frames per second double
fps = myClip.getFrameLength() * 1.0 /
myClip.getMicrosecondLength(); //
set the loop points //
we want to loop from 0.5 seconds to 1 second //
(500,000 microseconds to 1,000,000 microseconds) int startFrame = (int) Math.floor(500000 * fps); int endFrame = (int) Math.floor(1000000 * fps); myClip.setLoopPoints(startFrame,
endFrame); //
loop the clip and wait myClip.loop(Clip.LOOP_CONTINUOUSLY); Thread.sleep(15000); //
close the sound clip myClip.stop(); myClip.close(); myStream.close(); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
Playing MP3s
Everyone
likes MP3s. It's decent quality music at a reasonable size, just like the
Now, issues aside, we'd like very much to be able to play MP3s in our Java applications. Unfortunately, Java by default doesn't provide any support for MP3s. Fortunately, Sun released an MP3 plug-in in November 2004. I know it says Java Media Framework on it, but the plug-in also grants this MP3 ability to Java Sound as well.
But wait! You just installed the plug-in and took SoundDemo.java and replaced tada.wav with [your favorite MP3].mp3, and it said not supported. What gives?
Well, if you look at the error message more closely, you'll see that it's because it has an unknown frame size and bit rate and stuff like that. This is because the MP3 is still encoded. We need to decode the MP3 into a more playable form.
First, let's get the format of the current AudioInputStream. Yes, I am aware that the error message claimed that Java Sound doesn't know anything about the format, but it left out a few details--namely sample rate and number of channels. We'll need these numbers to generate new numbers for the format.
Now, the format is stored in an object called AudioFormat, which you can retrieve from the AudioInputStream by calling
AudioFormat getFormat()
Next, we want to create a new AudioFormat based on the old AudioFormat. The AudioFormat constructor of interest is:
AudioFormat(AudioFormat.Encoding encoding, float sampleRate,
int sampleSizeInBits, int channels, int frameSize, float frameRate, boolean bigEndian)
Now, once we create a new AudioFormat, we can decode our AudioInputStream by calling this static method on AudioSystem:
static AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream sourceStream)
where targetFormat is the AudioFormat we just created and sourceStream is our original AudioInputStream.
Using this information, we can in general use the following block of code to do all our MP3 related work in Java Sound:
AudioFormat myFormat = myStream.getFormat();
AudioFormat decodedFormat =
new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
myFormat.getSampleRate(),
16,
myFormat.getChannels(),
myFormat.getChannels() * 2,
myFormat.getSampleRate(),
false);
AudioInputStream decodedStream =
AudioSystem.getAudioInputStream(decodedFormat, myStream);
Be sure to open the decoded stream rather than the original stream.
Now, splicing these 3 lines of code into SoundDemo.java will get us this code sample, which plays MP3s!
/* MP3Demo.java */ import java.io.*; import javax.sound.sampled.*; public class MP3Demo { public
static void main(String[] args) { //
command line output so it looks like the program is doing something System.out.println("Loading MP3..."); try
{ AudioInputStream myStream = AudioSystem.getAudioInputStream(new File("[your favorite MP3].mp3")); // this is the extra work it takes to
decode the MP3 AudioFormat myFormat = myStream.getFormat(); AudioFormat decodedFormat = new
AudioFormat(AudioFormat.Encoding.PCM_SIGNED, myFormat.getSampleRate(), 16, myFormat.getChannels(), myFormat.getChannels() * 2, myFormat.getSampleRate(), false); AudioInputStream decodedStream
= AudioSystem.getAudioInputStream(decodedFormat,
myStream); //
load the MP3 into a clip and play it Clip
myClip = AudioSystem.getClip(); myClip.open(decodedStream); myClip.start(); Thread.sleep(60000); System.out.println("Cutting MP3 off short!"); myClip.stop(); myClip.close(); myStream.close(); //
note the extra stream that we need to close decodedStream.close(); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
It should be trivial to play two or more MP3s in sequence or to loop an MP3.
Also, I'm not providing you with an MP3 to play with for this example, so be sure to replace [your favorite MP3] with, um, your favorite MP3.
Playing
Many
of you young ones laugh, but
(And,
you know, somehow I doubt
In any case, I should get off my soapbox and continue with the tutorial.
javax.sound.midi
parallels javax.sound.sampled
in many ways. Where sampled sound has AudioSystem,
Java MIDI has MidiSystem. In sampled sound, we needed
a sound file (AudioInputStream) and the resource to
play that file (Clip). In Java MIDI, we also have MIDI files (Sequence) and
Anyways,
to get a Sequence from a
static Sequence getSequence(File file)
We can also use the MidiSystem to get a Sequencer:
static Sequencer getSequencer()
Now, unlike a Clip, a single Sequencer can be used for multiple MIDI Sequences. We don't actually need to pass it the Sequence until after we've already opened the Sequencer. First, call open() on the Sequencer to reserve its resources. Then, we can load the Sequence with this:
void setSequence(Sequence sequence)
setSequence() can be called as many times as you'd like in the lifetime of the Sequencer. This makes playing several different MIDIs a lot easier than playing several different Clips (although never at the same time).
Finally, the Sequencer has start() and stop(), just like a Clip. They even have the exact same behavior, namely when the Sequencer starts playing, it plays the MIDI Sequence in a separate pseudo-thread, and the stop() method acts more like a pause than a stop.
Finally, I'll show you the code sample. You'll see that Java MIDI parallels sampled sound very well.
/* MidiDemo.java */ import java.io.*; import javax.sound.midi.*; public class MidiDemo { public
static void main(String[] args) { try
{ //
get Sequencer Sequencer
mySequencer = MidiSystem.getSequencer(); mySequencer.open(); //
get Sequence
mySequence =
MidiSystem.getSequence(new File("DwayneJeng-Donuts.mid")); mySequencer.setSequence(mySequence); //
start the mySequencer.start(); System.out.println("Starting the Thread.sleep(60000); System.out.println("Cutting MIDI off short!"); //
stop and close the MIDI Sequencer mySequencer.stop(); mySequencer.close(); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
Finally, like sampled sound, MIDIs can loop as well. Here are the related Sequencer methods:
void setLoopCount(int count) - can be set to Sequencer.LOOP_CONTINUOUSLY to loop continuously
void setLoopStartPoint(long tick) - note that a tick is usually equivalent to a quarter note
void setLoopEndPoint(long tick) - the MIDI Sequence starts with 0; -1 can be used for the end of the Sequence
In
addition, in
Conclusion
That concludes the sound and music tutorial. Now, you know everything you need to know about the basics of sound and music in Java. Next time: Networking!
Homework Assignment!
Add sound effects and a music soundtrack to one of the graphical programs you developed in one of the previous tutorials.