Playing a Sound
To get started, you'll create a SimpleSoundPlayer to play sound. This class loads samples from an AudioInputStream into a byte array. It also plays sound from any InputStream by copying data from it to a Line. In the SimpleSoundPlayer example in Listing 4.1, the samples loaded are converted to an InputStream by using a ByteArrayInputStream. This enables you to read samples from memory instead of from disk. You could just write the byte array directly to the Line, but you'll need to read from InputStreams to add some more advanced functionality later. Because you're using a ByteArrayInputStream wrapped around a byte array, you can create as many ByteArrayInputStreams for the same sound as you want, so you can play multiple copies of the same sound simultaneously.
Listing 4.1 SimpleSoundPlayer.java
import java.io.*; import javax.sound.sampled.*; /** The SimpleSoundPlayer encapsulates a sound that can be opened from the file system and later played. */ public class SimpleSoundPlayer { public static void main(String[] args) { // load a sound SimpleSoundPlayer sound = new SimpleSoundPlayer("../sounds/voice.wav"); // create the stream to play InputStream stream = new ByteArrayInputStream(sound.getSamples()); // play the sound sound.play(stream); // exit System.exit(0); } private AudioFormat format; private byte[] samples; /** Opens a sound from a file. */ public SimpleSoundPlayer(String filename) { try { // open the audio input stream AudioInputStream stream = AudioSystem.getAudioInputStream( new File(filename)); format = stream.getFormat(); // get the audio samples samples = getSamples(stream); } catch (UnsupportedAudioFileException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } } /** Gets the samples of this sound as a byte array. */ public byte[] getSamples() { return samples; } /** Gets the samples from an AudioInputStream as an array of bytes. */ private byte[] getSamples(AudioInputStream audioStream) { // get the number of bytes to read int length = (int)(audioStream.getFrameLength() * format.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 samples; } /** Plays a stream. This method blocks (doesn't return) until the sound is finished playing. */ public void play(InputStream source) { // use a short, 100ms (1/10th sec) buffer for real-time // change to the sound stream int bufferSize = format.getFrameSize() * Math.round(format.getSampleRate() / 10); byte[] buffer = new byte[bufferSize]; // create a line to play to SourceDataLine line; try { DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); line = (SourceDataLine)AudioSystem.getLine(info); line.open(format, bufferSize); } catch (LineUnavailableException ex) { ex.printStackTrace(); return; } // start the line line.start(); // copy data to the line try { int numBytesRead = 0; while (numBytesRead != -1) { numBytesRead = source.read(buffer, 0, buffer.length); if (numBytesRead != -1) { line.write(buffer, 0, numBytesRead); } } } catch (IOException ex) { ex.printStackTrace(); } // wait until all data is played line.drain(); // close the line line.close(); } }
In SimpleSoundPlayer, the getSamples(AudioInputStream) method reads from an AudioInputStream and stores the data in the samples byte array. The play() method reads data from an InputStream to a buffer and then writes the buffer to a SourceDataLine, which plays the sound. Also, the main() method in SimpleSoundPlayer tests the class by playing the voice.wav sound. Note that because of a bug in Java Sound, Java programs won't exit by themselves. Usually, the Java VM exits when there are only daemon threads running, but when you use Java Sound, a nondaemon thread always runs in the background. So, to exit your Java programs that use Java Sound, be sure to call System.exit(0). Well, you can play sounds yourself, but what if you want to play a sound repeatedly in a loop? This could be really useful for background ambient sounds or, say, for a buzzing fly. To loop sound, you don't even need to make any changes to the SimpleSoundPlayer. Instead of using a ByteArrayInputStream, you'll create a LoopingByteInputStream in Listing 4.2, which works similarly to ByteArrayInputStream. The only difference is that LoopingByteInputStream indefinitely reads the byte array in a loop until its close() method is called.
Listing 4.2 LoopingByteInputStream.java
package com.brackeen.javagamebook.util; import java.io.ByteArrayInputStream; import java.io.IOException; /** The LoopingByteInputStream is a ByteArrayInputStream that loops indefinitely. The looping stops when the close() method is called. <p>Possible ideas to extend this class:<ul> <li>Add an option to only loop a certain number of times. </ul> */ public class LoopingByteInputStream extends ByteArrayInputStream { private boolean closed; /** Creates a new LoopingByteInputStream with the specified byte array. The array is not copied. */ public LoopingByteInputStream(byte[] buffer) { super(buffer); closed = false; } /** Reads <code>length</code> bytes from the array. If the end of the array is reached, the reading starts over from the beginning of the array. Returns -1 if the array has been closed. */ public int read(byte[] buffer, int offset, int length) { if (closed) { return -1; } int totalBytesRead = 0; while (totalBytesRead < length) { int numBytesRead = super.read(buffer, offset + totalBytesRead, length - totalBytesRead); if (numBytesRead > 0) { totalBytesRead += numBytesRead; } else { reset(); } } return totalBytesRead; } /** Closes the stream. Future calls to the read() methods will return 1. */ public void close() throws IOException { super.close(); closed = true; } }
There's nothing special about LoopingByteInputStream. It extends ByteArrayInputStream, and whenever the end of the stream is reached, it calls the reset() method to start reading from the beginning of the array again. Now you can easily play and loop sound stored in a byte array. Also, because you have access to all the sound samples, you can manipulate the samples to create different effects, or filters.