SoundPlayer.java Source code

Java tutorial

Introduction

Here is the source code for SoundPlayer.java

Source

/*
 * Copyright (c) 2004 David Flanagan.  All rights reserved.
 * This code is from the book Java Examples in a Nutshell, 3nd Edition.
 * It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
 * You may study, use, and modify it for any non-commercial purpose,
 * including teaching and use in open-source projects.
 * You may distribute it non-commercially as long as you retain this notice.
 * For a commercial use license, or to purchase the book, 
 * please visit http://www.davidflanagan.com/javaexamples3.
 */

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Track;
import javax.sound.midi.Transmitter;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * This class is a Swing component that can load and play a sound clip,
 * displaying progress and controls. The main() method is a test program. This
 * component can play sampled audio or MIDI files, but handles them differently.
 * For sampled audio, time is reported in microseconds, tracked in milliseconds
 * and displayed in seconds and tenths of seconds. For midi files time is
 * reported, tracked, and displayed in MIDI "ticks". This program does no
 * transcoding, so it can only play sound files that use the PCM encoding.
 */
public class SoundPlayer extends JComponent {
    boolean midi; // Are we playing a midi file or a sampled one?

    Sequence sequence; // The contents of a MIDI file

    Sequencer sequencer; // We play MIDI Sequences with a Sequencer

    Clip clip; // Contents of a sampled audio file

    boolean playing = false; // whether the sound is current playing

    // Length and position of the sound are measured in milliseconds for
    // sampled sounds and MIDI "ticks" for MIDI sounds
    int audioLength; // Length of the sound.

    int audioPosition = 0; // Current position within the sound

    // The following fields are for the GUI
    JButton play; // The Play/Stop button

    JSlider progress; // Shows and sets current position in sound

    JLabel time; // Displays audioPosition as a number

    Timer timer; // Updates slider every 100 milliseconds

    // The main method just creates an SoundPlayer in a Frame and displays it
    public static void main(String[] args) throws IOException, UnsupportedAudioFileException,
            LineUnavailableException, MidiUnavailableException, InvalidMidiDataException {
        SoundPlayer player;

        File file = new File(args[0]); // This is the file we'll be playing
        // Determine whether it is midi or sampled audio
        boolean ismidi;
        try {
            // We discard the return value of this method; we just need to know
            // whether it returns successfully or throws an exception
            MidiSystem.getMidiFileFormat(file);
            ismidi = true;
        } catch (InvalidMidiDataException e) {
            ismidi = false;
        }

        // Create a SoundPlayer object to play the sound.
        player = new SoundPlayer(file, ismidi);

        // Put it in a window and play it
        JFrame f = new JFrame("SoundPlayer");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(player, "Center");
        f.pack();
        f.setVisible(true);
    }

    // Create an SoundPlayer component for the specified file.
    public SoundPlayer(File f, boolean isMidi) throws IOException, UnsupportedAudioFileException,
            LineUnavailableException, MidiUnavailableException, InvalidMidiDataException {
        if (isMidi) { // The file is a MIDI file
            midi = true;
            // First, get a Sequencer to play sequences of MIDI events
            // That is, to send events to a Synthesizer at the right time.
            sequencer = MidiSystem.getSequencer(); // Used to play sequences
            sequencer.open(); // Turn it on.

            // Get a Synthesizer for the Sequencer to send notes to
            Synthesizer synth = MidiSystem.getSynthesizer();
            synth.open(); // acquire whatever resources it needs

            // The Sequencer obtained above may be connected to a Synthesizer
            // by default, or it may not. Therefore, we explicitly connect it.
            Transmitter transmitter = sequencer.getTransmitter();
            Receiver receiver = synth.getReceiver();
            transmitter.setReceiver(receiver);

            // Read the sequence from the file and tell the sequencer about it
            sequence = MidiSystem.getSequence(f);
            sequencer.setSequence(sequence);
            audioLength = (int) sequence.getTickLength(); // Get sequence length
        } else { // The file is sampled audio
            midi = false;
            // Getting a Clip object for a file of sampled audio data is kind
            // of cumbersome. The following lines do what we need.
            AudioInputStream ain = AudioSystem.getAudioInputStream(f);
            try {
                DataLine.Info info = new DataLine.Info(Clip.class, ain.getFormat());
                clip = (Clip) AudioSystem.getLine(info);
                clip.open(ain);
            } finally { // We're done with the input stream.
                ain.close();
            }
            // Get the clip length in microseconds and convert to milliseconds
            audioLength = (int) (clip.getMicrosecondLength() / 1000);
        }

        // Now create the basic GUI
        play = new JButton("Play"); // Play/stop button
        progress = new JSlider(0, audioLength, 0); // Shows position in sound
        time = new JLabel("0"); // Shows position as a #

        // When clicked, start or stop playing the sound
        play.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (playing)
                    stop();
                else
                    play();
            }
        });

        // Whenever the slider value changes, first update the time label.
        // Next, if we're not already at the new position, skip to it.
        progress.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                int value = progress.getValue();
                // Update the time label
                if (midi)
                    time.setText(value + "");
                else
                    time.setText(value / 1000 + "." + (value % 1000) / 100);
                // If we're not already there, skip there.
                if (value != audioPosition)
                    skip(value);
            }
        });

        // This timer calls the tick() method 10 times a second to keep
        // our slider in sync with the music.
        timer = new javax.swing.Timer(100, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                tick();
            }
        });

        // put those controls in a row
        Box row = Box.createHorizontalBox();
        row.add(play);
        row.add(progress);
        row.add(time);

        // And add them to this component.
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        this.add(row);

        // Now add additional controls based on the type of the sound
        if (midi)
            addMidiControls();
        else
            addSampledControls();
    }

    /** Start playing the sound at the current position */
    public void play() {
        if (midi)
            sequencer.start();
        else
            clip.start();
        timer.start();
        play.setText("Stop");
        playing = true;
    }

    /** Stop playing the sound, but retain the current position */
    public void stop() {
        timer.stop();
        if (midi)
            sequencer.stop();
        else
            clip.stop();
        play.setText("Play");
        playing = false;
    }

    /** Stop playing the sound and reset the position to 0 */
    public void reset() {
        stop();
        if (midi)
            sequencer.setTickPosition(0);
        else
            clip.setMicrosecondPosition(0);
        audioPosition = 0;
        progress.setValue(0);
    }

    /** Skip to the specified position */
    public void skip(int position) { // Called when user drags the slider
        if (position < 0 || position > audioLength)
            return;
        audioPosition = position;
        if (midi)
            sequencer.setTickPosition(position);
        else
            clip.setMicrosecondPosition(position * 1000);
        progress.setValue(position); // in case skip() is called from outside
    }

    /** Return the length of the sound in ms or ticks */
    public int getLength() {
        return audioLength;
    }

    // An internal method that updates the progress bar.
    // The Timer object calls it 10 times a second.
    // If the sound has finished, it resets to the beginning
    void tick() {
        if (midi && sequencer.isRunning()) {
            audioPosition = (int) sequencer.getTickPosition();
            progress.setValue(audioPosition);
        } else if (!midi && clip.isActive()) {
            audioPosition = (int) (clip.getMicrosecondPosition() / 1000);
            progress.setValue(audioPosition);
        } else
            reset();
    }

    // For sampled sounds, add sliders to control volume and balance
    void addSampledControls() {
        try {
            FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
            if (gainControl != null)
                this.add(createSlider(gainControl));
        } catch (IllegalArgumentException e) {
            // If MASTER_GAIN volume control is unsupported, just skip it
        }

        try {
            // FloatControl.Type.BALANCE is probably the correct control to
            // use here, but it doesn't work for me, so I use PAN instead.
            FloatControl panControl = (FloatControl) clip.getControl(FloatControl.Type.PAN);
            if (panControl != null)
                this.add(createSlider(panControl));
        } catch (IllegalArgumentException e) {
        }
    }

    // Return a JSlider component to manipulate the supplied FloatControl
    // for sampled audio.
    JSlider createSlider(final FloatControl c) {
        if (c == null)
            return null;
        final JSlider s = new JSlider(0, 1000);
        final float min = c.getMinimum();
        final float max = c.getMaximum();
        final float width = max - min;
        float fval = c.getValue();
        s.setValue((int) ((fval - min) / width * 1000));

        java.util.Hashtable labels = new java.util.Hashtable(3);
        labels.put(new Integer(0), new JLabel(c.getMinLabel()));
        labels.put(new Integer(500), new JLabel(c.getMidLabel()));
        labels.put(new Integer(1000), new JLabel(c.getMaxLabel()));
        s.setLabelTable(labels);
        s.setPaintLabels(true);

        s.setBorder(new TitledBorder(c.getType().toString() + " " + c.getUnits()));

        s.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                int i = s.getValue();
                float f = min + (i * width / 1000.0f);
                c.setValue(f);
            }
        });
        return s;
    }

    // For Midi files, create a JSlider to control the tempo,
    // and create JCheckBoxes to mute or solo each MIDI track.
    void addMidiControls() {
        // Add a slider to control the tempo
        final JSlider tempo = new JSlider(50, 200);
        tempo.setValue((int) (sequencer.getTempoFactor() * 100));
        tempo.setBorder(new TitledBorder("Tempo Adjustment (%)"));
        java.util.Hashtable labels = new java.util.Hashtable();
        labels.put(new Integer(50), new JLabel("50%"));
        labels.put(new Integer(100), new JLabel("100%"));
        labels.put(new Integer(200), new JLabel("200%"));
        tempo.setLabelTable(labels);
        tempo.setPaintLabels(true);
        // The event listener actually changes the tmpo
        tempo.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                sequencer.setTempoFactor(tempo.getValue() / 100.0f);
            }
        });

        this.add(tempo);

        // Create rows of solo and checkboxes for each track
        Track[] tracks = sequence.getTracks();
        for (int i = 0; i < tracks.length; i++) {
            final int tracknum = i;
            // Two checkboxes per track
            final JCheckBox solo = new JCheckBox("solo");
            final JCheckBox mute = new JCheckBox("mute");
            // The listeners solo or mute the track
            solo.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    sequencer.setTrackSolo(tracknum, solo.isSelected());
                }
            });
            mute.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    sequencer.setTrackMute(tracknum, mute.isSelected());
                }
            });

            // Build up a row
            Box box = Box.createHorizontalBox();
            box.add(new JLabel("Track " + tracknum));
            box.add(Box.createHorizontalStrut(10));
            box.add(solo);
            box.add(Box.createHorizontalStrut(10));
            box.add(mute);
            box.add(Box.createHorizontalGlue());
            // And add it to this component
            this.add(box);
        }
    }
}