Search code examples
javaswingaudiojavasoundclip

Playing multiple sound clips using Clip objects


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?


Solution

  • 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;
                }
            }
    
        }
    
    }