A user recently came up with an interesting project today that frankly I had never really played around with too much… playing sound clips using Java’s AudioSystem. The problem he was having was two fold actually. How does one go about throttling a sound (preventing the user from triggering the sound multiple times so that they overlap themselves and causing a scrambled mess that sounds like spirits you would hear on the TV from poltergeist) and prevent Java from eating up memory doing it as well. So on this entry of the programming underground I will talk a bit about what was going on and show you an example I created to help solve both situations. All this and more right here on the Programming Underground!
So as I had said, the problem was two fold. First we have the issue of the sound overlapping on itself. This was because he was playing the clip (whether it be the same clip or could be another) before the last play was finished. This create two separate objects playing two different clips at the same time. It just so happens he wanted this clip to play when the user entered a certain area of screen coordinates. In my example, I use a simple play button to trigger the play instead. What we need to do to remedy this is first setup a flag test situation which will let us know if the current clip is currently active in playback. Based on this test, we can choose whether or not to let the sound play again. In this circumstance, we don’t want the user to be able to play the sound a second time if the first one is not finished. Now we could easily make a queue here where we queue up the playing if we wanted to keep track of every time the user wants to play the sound, but this would get very cumbersome and may eat more memory if the user decides to just play the sound 10,000 times.
The trick here, and the piece that he was looking for, was the isActive() method of the DataLine interface. He could have also used isRunning() to do the same thing. We just need to know if the line is actually active. Either one will work. Since he was playing a Clip, an inherited interface of the DataLine interface, it inherited the isActive() method. This method returns a boolean to tell if the current sound clip is currently in playback or capture mode. This test is the flag condition we need to determine when a user can do a playback of the sound (as I will show you below in a moment) and when.
The second problem was a memory leak that was happening because he would start each of these overlapping clips, but would not close them. The way he had originally put it not only was he starting multiple clips at once, that would overlap, but they were also not being closed. The memory was left there idle after its sounds was played. But what is a good way to close these clips? Well, the elegant way is to listen for when the clip actually stops. So not only are we throttling how often the clip plays, but also closing it properly each time it is done playing. To listen for when it stops playing, we used a LineListener.
The LineListener is a listener which listens to the underlying line of the the sound clip used by the AudioSystem classes. Think of it is like a line of communication or stream. While the sound is being played, this stream is active. When the stream is first started it lets all LineListener’s (it has registered with it) know that the line is starting. It also keeps them informed any time the state of that clip changes. This is a great way to listen for the start of a sound clip, the end and when it enters other modes.
Below is the example I have put together to demonstrate all this. Keep in mind that this is just a sample that is demonstrating the two solutions simultaneously.
import javax.swing.*; import java.awt.event.*; import java.awt.*; import java.io.*; import javax.sound.sampled.*; // For sound handling import javax.sound.sampled.LineEvent.Type.*; // For LineEvent types used in listener public class PlaySoundSample extends JFrame implements ActionListener, LineListener { JButton play; // Audio variables File soundFile; Clip clip; AudioInputStream soundIn; AudioFormat format; DataLine.Info info; public PlaySoundSample() { setTitle("PlaySound Example"); setSize(300,300); setDefaultCloseOperation(EXIT_ON_CLOSE); play = new JButton("Play"); play.addActionListener(this); // Add play button to the bottom portion of the frame (so we could possibly put visualizer or something in the top half???) add(play,BorderLayout.SOUTH); // Configure our variables for playing format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, AudioSystem.NOT_SPECIFIED, 16, 2, 4, AudioSystem.NOT_SPECIFIED, true); info = new DataLine.Info(Clip.class, format); // Our test file, be sure to change this to your own sounds or load it dynamically soundFile = new File("c:\\test.wav"); } public static void main(String[] args) { PlaySoundSample playSound = new PlaySoundSample(); playSound.setVisible(true); } // Listens for when the play button is pressed (Part of the ActionListener) public void actionPerformed(ActionEvent e) { try { // Play the sound if it is currently not set to playing a clip or that clip is not ACTIVELY PLAYING the sound. if ((clip == null) || (!clip.isActive())) { soundIn = AudioSystem.getAudioInputStream(soundFile); // Get the Data line for our clip, open it using the audio input stream from the actual sound file (loading the sound file into the clip) // Then start it clip = (Clip)AudioSystem.getLine(info); clip.open(soundIn); clip.start(); // Attach line listener to the clip clip.addLineListener(this); } } catch (Exception ex) { System.out.println("There was an error!"); } } // Listens for when the clip has stopped playing and closes it. (Part of the LineListener) public void update(LineEvent event) { if (event.getType().equals(LineEvent.Type.STOP)) { clip.close(); } } }
This code essentially builds a quick little JFrame with a play button that lurks across the bottom. I used the play button to quickly activate the playing of a clip for testing purposes. As you can see from the ActionListener’s actionPerformed method, we simply check if the clip is either null (being played the first time) or is not active. If either case is true, we can go about setting up the clip for another play and play it. One line I want to point out here is that we attach the LineListener to this clip so that we can listen to its states of play as the clip progresses. Any time the clip changes its state, it will let our listener know.
The second part that is of interest is the update method of our LineListener. This update event is the event notified when our clip changes state. When it changes state, the clip will send the update event a LineEvent object which we can use to evaluate what type of state actually happened. Here we are listening for a STOP event. We could easily expand this to listen for start events as well as others if you wanted to. But when we detect the clip has stopped, we close the clip and free its resources. Now the clip is ready for another pass and we are throttling and freeing the memory properly.
But anyways, I thought it was an interesting little project and certainly something worth discussing on the blog today. Feel free to take whatever code out of this you wish and use it for your own projects. Just like everything on the Programming Underground, we won’t stiff you like big brother governments would. Knowledge is a free commodity as far as I am concerned. Enjoy and thanks for reading! 🙂