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.

Screenshot


   
Comments