Play Non-Trivial Audio



Play Non-Trivial Audio

When loading an entire audio clip into memory is a bad idea (or just impossible), you have to take JavaSound responsibilities into your own hands.

Playing JavaSound audio with a Clip [Hack #71] is a pretty convenient way to play a short sound, like a sound effect for a desktop application. The only problem is that the Clip loads all the audio into memory, which could have a couple of bad side effects:

  • It makes your application use more memory, which could cause problems.

  • The audio you need might not fit into memory at all.

You might have run into this second point if you tried to load a really big audio file into a Clip. For example, I took a 3 minute, 45 second track from a CD and converted it to 8-bit mono PCM in an AIFF file, which ended up being 9.4 MB. You can guess what happened:

	[aeris:HacksBook/Media/52] cadamson% java CoreJavaSound			
	javax.sound.sampled.LineUnavailableException: Failed to allocate clip data: 
	Requested buffer too large.
		at com.sun.media.sound.MixerClip.implOpen(MixerClip.java:536)
		at com.sun.media.sound.MixerClip.open(MixerClip.java:161)
		at com.sun.media.sound.MixerClip.open(MixerClip.java:249)
		at CoreJavaSound.<init>(CoreJavaSound.java:39)
		at CoreJavaSound.main(CoreJavaSound.java:17)

Unfortunately, most of the JavaSound code you'll find on the Web deals with Clips only and not with getting a Line for larger files, or potentially endless streams for that matter. Why? Perhaps because JavaSound doesn't do it for youyou are responsible for reading bytes and feeding them to JavaSound!

Grabbing a DataLine

This hack is going to play an uncompressed (i.e., PCM) AIFF or WAV file of arbitrary length by getting a DataLine for the data and then repeatedly reading the data from disk and writing it to the DataLine.

PCM stands for Pulse Code Modulation, which means that analog audio has been sampled at regular intervals and quantized (i.e., each sample is expressed as a numeric value). It's the lowest-level, most common denominator data that JavaSound understands, since it can be delivered directly to a sound system for playback.


The class to do this will be called PCMFilePlayer. Given a file, its responsibilities are to:

  • Verify that the file contains PCM data (signed or unsigned).

  • Get a Line for this format.

  • Kick off a thread to read bytes from the file and write them to the Line, which plays them.

Reading and writing bytes doesn't sound too bad, but JavaSound imposes another requirement on you: you have to send complete frames, not just a bunch of bytes, to the Line. A frame is one complete sample of audio in whatever format you're dealing with. For the PCM formats supported by this hack, a frame can be one of three sizes:

  • 1 byte for 8-bit mono sound

  • 2 bytes for either 8-bit stereo sound or 16-bit mono sound

  • 4 bytes for 16-bit stereo sound

The implication for the read-write loop is that if you read some number of bytes that leave you off an even frame boundary, then you have to save the partial frame you've read, not send it to the Line, and instead append it to the beginning of the next read.

Finally, when you reach the end of the file, you need to call Line.drain() to make sure it plays out all the data you've sent it, and then close the Line.

The code for the PCMLinePlayer is shown in Figure.

Playing uncompressed audio files in JavaSound
import javax.sound.sampled.*;

public class PCMFilePlayer implements Runnable { 
	File file;
	AudioInputStream in;
	SourceDataLine line;
	int frameSize;
	byte[] buffer = new byte [32 * 1024]; // 32k is arbitrary
	Thread playThread;
	boolean playing;
	boolean notYetEOF;

	public PCMFilePlayer (File f)
		throws IOException,
			UnsupportedAudioFileException,
			LineUnavailableException {
		file = f;
		in = AudioSystem.getAudioInputStream (f);
		AudioFormat format = in.getFormat();
		AudioFormat.Encoding formatEncoding = format.getEncoding();
		if (! (formatEncoding.equals (AudioFormat.Encoding.PCM_SIGNED) ||
			   formatEncoding.equals (AudioFormat.Encoding.PCM_UNSIGNED))) 
		   throw new UnsupportedAudioFileException (
                              file.getName() + " is not PCM audio");
       System.out.println ("got PCM format");        
	   frameSize = format.getFrameSize(); 
	   DataLine.Info info =
		   new DataLine.Info (SourceDataLine.class, format); 
	   System.out.println ("got info"); 
	   line = (SourceDataLine) AudioSystem.getLine (info); 
	   System.out.println ("got line");        
	   line.open(); 
	   System.out.println ("opened line"); 
	   playThread = new Thread (this); 
	   playing = false; 
	   notYetEOF = true;        
	   playThread.start();
	}
	public void run() {
		int readPoint = 0;
		int bytesRead = 0;

		try {
			while (notYetEOF) {
				if (playing) {
				bytesRead = in.read (buffer, 
							 readPoint, 
							 buffer.length - readPoint);
                   if (bytesRead == -1) { 
				notYetEOF = false; 
				break;
				}
				// how many frames did we get,
				// and how many are left over?
				int frames = bytesRead / frameSize;
				int leftover = bytesRead % frameSize;
				// send to line
				line.write (buffer, readPoint, bytesRead-leftover);
				// save the leftover bytes
				System.arraycopy (buffer, bytesRead,
						  buffer, 0, 
						  leftover); 
                    readPoint = leftover;
				} else { 
				// if not playing                   
				// Thread.yield(); 
				try { Thread.sleep (10);} 
				catch (InterruptedException ie) {}
				}
			} // while notYetEOF
			System.out.println ("reached eof");
			line.drain();
			line.stop();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		} finally {
			// line.close();
		}
	} // run

	public void start() {
		playing = true;
		if (! playThread.isAlive())
			playThread.start();
		line.start();
	}

	public void stop() {
		playing = false;
		line.stop();
	}
   
	public SourceDataLine getLine() {
		return line;
	}

	public File getFile() {		
		return file; 
	} 
}

Notice in the constructor that, as with the Clip, the way to get an actual Line object is to construct a DataLine.Info object and then pass that to AudioSystem. This time, you construct a DataLine.Info class with both the SourceDataLine classyou need this subclass of Line because it provides the write() method with which you supply bytes to the Lineand an AudioFormat object describing the data you'll be supplying. Assuming that doesn't throw a LineUnavailableException (and it shouldn't, because the format is already known to be PCM, which JavaSound always supports), you'll have a line that you can open and start writing bytes to.

As mentioned previously, the key issue for the thread that reads bytes from the file and writes them to the Line is that it has to be aware of frame boundaries. In this code, readPoint indicates the index of the buffer to start reading bytes into. When you have an incomplete frame after reading from the input stream, you copy the bytes from the incomplete frame to the front of the buffer in preparation for the next read. For example, if you have a frame size of 4, and bytesRead % 4 equals 3, then you copy those 3 bytes to the front of the buffer and set readPoint to 3. The next read() will start at 3, and the first byte read into the buffer will complete the frame from the previous read().

Big Files, Big Sound

Since this is still in the realm of JavaSound, much of what was shown in the Clip-based hack still works. A demo application simply has to provide PCMFilePlayer with a file and then start it. Since PCMFilePlayer exposes its Line through a get method, you can even wire up as a LineListener and get notified of STOP, START, OPEN, and CLOSE LineEvents. Figure shows the simple GUI, using PCMFilePlayer.

Playing arbitrarily long uncompressed WAV or AIFF audio
import javax.sound.sampled.*;

public class StreamingLineSound extends Object 
	   implements LineListener {
	File soundFile;
	JDialog playingDialog;
	PCMFilePlayer player;
	
	public static void main (String[] args) {
		JFileChooser chooser = new JFileChooser();
		chooser.showOpenDialog(null);
		File f = chooser.getSelectedFile();
	try {
		StreamingLineSound s = new StreamingLineSound (f);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

public StreamingLineSound (File f)
	throws LineUnavailableException, IOException,
		   UnsupportedAudioFileException { 
	soundFile = f; 
	// prepare a dialog to display while playing        
	JOptionPane pane = new JOptionPane ("Playing " + f.getName(),
							JOptionPane.PLAIN_MESSAGE); 
    playingDialog = pane.createDialog (null, "Streaming Sound");
    playingDialog.pack();

	player = new PCMFilePlayer (soundFile);
	player.getLine().addLineListener (this);
	player.start();

}
// LineListener
public void update (LineEvent le) {
	LineEvent.Type type = le.getType();
	if (type == LineEvent.Type.OPEN) {
		System.out.println ("OPEN");
	} else if (type == LineEvent.Type.CLOSE) {
		System.out.println ("CLOSE");
		System.exit (0);
	} else if (type == LineEvent.Type.START) {
		System.out.println ("START");
		playingDialog.setVisible(true);
	} else if (type == LineEvent.Type.STOP) {
		System.out.println ("STOP");
		playingDialog.setVisible(false);
		player.line.close();

	} 
  } 
}

When run, this class shows a dialog box (seen in Figure), identical to the one produced in "Play a Sound with JavaSound" [Hack #71]. The only difference is that this one can stay up potentially indefinitely, since the player can keep reading and writing bytes forever.

Playing a large AIFF file in JavaSound