2

I am developing a program that has numerous JButton objects, and I want each one to correspond to its own .wav file. Also, I want the sounds to work in a way such that they can overlap with other buttons' sounds, but it cannot overlap with itself (clicking a button while its sound is playing will restart the sound).

I tried using a single Clip object but I had trouble accomplishing what I stated above. As a result, I resorted to declaring a new Clip object for each button, but I have a feeling this is a rather inefficient solution to my issue.

How can I accomplish what I stated in the first paragraph in the most efficient fashion?

null
  • 2,060
  • 3
  • 23
  • 42

2 Answers2

4

There's a couple of ways you might be able to achieve this, but the basic idea is, you want to register a LineListener to a Clip and monitor for the LineEvent.Type.STOP event and reenable the button

For example. This looks for all the .wav files in a given directory and creates a button for each one. When clicked, the button (or more importantly, the underlying Action) is disabled and the audio is played. When it STOPs, the Action (and the button by extension) is re-enabled.

The Sound API can play multiple sounds simultaneous anyway

import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            File[] musicFiles = new File("a directory somewhere").listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.getName().toLowerCase().endsWith(".wav");
                }
            });

            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.fill = GridBagConstraints.HORIZONTAL;

            for (File music : musicFiles) {
                try {
                    JButton btn = new JButton(new AudioAction(music.getName(), music.toURI().toURL()));
                    add(btn, gbc);
                } catch (MalformedURLException ex) {
                    ex.printStackTrace();
                }
            }

        }

    }

    public class AudioAction extends AbstractAction {

        private URL audio;

        public AudioAction(String name, URL audioSource) {
            super(name);
            this.audio = audioSource;
        }

        public URL getAudioSource() {
            return audio;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setEnabled(false);
            try (InputStream is = getAudioSource().openStream()) {
                AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(is);
                Clip play = AudioSystem.getClip();
                play.addLineListener(new LineListener() {
                    @Override
                    public void update(LineEvent event) {
                        System.out.println(event.getFramePosition());
                        if (event.getType().equals(LineEvent.Type.STOP)) {
                            setEnabled(true);
                        }
                    }
                });
                play.open(audioInputStream);
                play.start();
            } catch (IOException | LineUnavailableException | UnsupportedAudioFileException exp) {
                exp.printStackTrace();
            }
        }

    }

}

nb: I tried using Clip#drain (in a background thread), but it only worked for the first clip, subsequent clips basically skipped over the method, thus the reason I went for the LineListener

Now with better resource management

import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            File[] musicFiles = new File("...").listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.getName().toLowerCase().endsWith(".wav");
                }
            });

            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.fill = GridBagConstraints.HORIZONTAL;

            for (File music : musicFiles) {
                try {
                    JButton btn = new JButton(new AudioAction(music.getName(), music.toURI().toURL()));
                    add(btn, gbc);
                } catch (MalformedURLException exp) {
                    exp.printStackTrace();
                }
            }

        }

    }

    public class AudioAction extends AbstractAction {

        private AudioPlayer player;

        public AudioAction(String name, URL audioSource) {
            super(name);
            player = new AudioPlayer(audioSource);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (player.isPlaying()) {
                player.stop();
            } else {
                try {
                    player.play();
                } catch (IOException | LineUnavailableException | UnsupportedAudioFileException ex) {
                    ex.printStackTrace();
                }
            }
        }

    }

    public class AudioPlayer {

        private Clip clip;
        private URL url;

        public AudioPlayer(URL url) {
            this.url = url;
        }

        public boolean isPlaying() {
            return clip != null && clip.isRunning();
        }

        protected void open() throws IOException, LineUnavailableException, UnsupportedAudioFileException {
            clip = AudioSystem.getClip();
            clip.open(AudioSystem.getAudioInputStream(url.openStream()));
        }

        public void play() throws IOException, LineUnavailableException, UnsupportedAudioFileException {
            if (clip == null || !clip.isRunning()) {
                open();
                clip.setFramePosition(0);
                clip.start();
            }
        }

        public void stop() {
            if (clip != null && clip.isRunning()) {
                clip.stop();
                clip.flush();
                dispose();
            }
        }

        public void dispose() {
            try {
                clip.close();
            } finally {
                clip = null;
            }
        }

    }

}
Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks, but I have a couple questions. Why can't the Clip object be instantiated outside of the AudioAction's actionPerformed method? Also, why does a new LineListener have to be added to the clip each time a sound is played? Wouldn't reinstantiating the Clip and adding new LineListeners use up memory and slow down the program? I tried instantiating the Clip object and adding one LineListener to it outside of the method but ran into a `Clip is already open with ...` error. – null Jun 15 '15 at 08:44
  • 1
    In theory, there is no real reason why the `Clip` can't be managed externally, but you are further increasing the complexity, with this approach, you have a single unit of encapsulated work, this makes it self manageable. Normally I make a "audio" class which has play/stop/resume functionality which allows you to create multiple audio files, which are all managed by a single class. In the case of this example, it's unlikely to increase the memory usage significantly as the clip becomes "unreachable" it will become eligible for GC, but this was an example about how to make it play ;) – MadProgrammer Jun 15 '15 at 08:57
  • Also, given that the button disables itself when the audio is playing, you will normally only have one clip playing at a time – MadProgrammer Jun 15 '15 at 08:57
  • As a [extended example](http://stackoverflow.com/questions/30823537/sound-problems-in-java?noredirect=1#comment49705145_30823537) – MadProgrammer Jun 15 '15 at 10:47
  • If I am understanding correctly, you are instantiating a Clip object for each sound, since a new AudioAction is added to each JButton. With each AudioAction, a new Clip object and LineListener are instantiated. So if you had X sound files, you would have X Clip objects and X LineListeners, right? Also, what is an example of when the Clip object would be eligible for garbage collection? Additionally, in this example, the sound plays, and the button is enabled only after it ends. Instead, how would I go about restarting the sound everytime the button is pressed i.e. while the sound is playing? – null Jun 15 '15 at 23:55
  • Yes, but each sound would need it's own `Clip` and each `Clip` would need it's own listener in order to be of any use. A `Clip` would become eligible when there are no longer any strong references to it or any of the resources it might be managing. This would true for when the clip is no longer playing, but you would need to test it. – MadProgrammer Jun 16 '15 at 00:01
  • 1
    To "restart" the `Clip`, you would first need to maintain a class instance field of the `Clip`, when the `actionPerformed` method is triggered, you would check to see if the `Clip` is null or not, creating if it is, if not, call `stop` on it and then call `start` again. – MadProgrammer Jun 16 '15 at 00:02
  • I tried using [this](https://gist.github.com/anonymous/999a91640e103968a74d) for the `AudioAction` class, and it works to an extent. The only downfall I observe is that if I play a sound quickly and repeatedly, the sound will work fine at first and will repeatedly play. However, after 30 or so seconds of repeated playing, the sound will sometimes skip and not play. In other words, sounds start skipping after repeated playing. This is only noticeable in short sounds, and longer sounds don't have an apparent issue. How can I fix this? – null Jun 16 '15 at 00:39
  • I think your `gist` expired – MadProgrammer Jun 16 '15 at 00:43
  • Here's a [Pastebin link](http://pastebin.com/gB88a0tk). Also, I notice that when I play any or all of the sounds enough times, the "sound skipping" will appear in the short sounds. In other words, I don't only need to play the same sound over and over for the sound skipping to occur. – null Jun 16 '15 at 00:50
  • It "might" be the `AudioInputStream` is been closed, but I'm not 100% sure – MadProgrammer Jun 16 '15 at 00:53
  • 1
    Do you have multiple audio streams open or does it occur on as well if you only have one? – MadProgrammer Jun 16 '15 at 00:58
  • I tried pointing the program to a directory containing only one short `.wav` file. There is significantly less skipping, maybe only 1 in 30 sounds skip as opposed to a directory of over 20 sound files, where there was upwards of 1 in 5 clicks skipping. – null Jun 16 '15 at 01:11
  • It simply could be a resource issue – MadProgrammer Jun 16 '15 at 01:15
  • 1
    I've revamped the example (or added a new one), which basically closes and deference the `Clip` instance when you stop it. Since I only have about 3 wave files, it's kind of hard to test to any great extent – MadProgrammer Jun 16 '15 at 01:33
  • I see, thanks for posting. In terms of resource and memory management, do you think it would be better to instantiate all needed Clip objects at once or to instantiate each one when needed (i.e. when a button is clicked) and then dereference it after the sound is played? Would either one have a significant advantage when it comes to performance? – null Jun 16 '15 at 02:07
  • 1
    I think it's better to lazy load them as you need them and get rid of them when you don't. The only "significant" performance issue you might have with using lazy loading like this is might take time to load the audio each time, but I didn't see any significant delay when I was testing – MadProgrammer Jun 16 '15 at 02:10
1

The one clip per button should be fine. When the user clicks the button run this to restart the clip:

sound.stop();
sound.setFramePosition(0);// set the location to the start of the file
sound.play();// restart your sound
J Atkin
  • 3,080
  • 2
  • 19
  • 33