FileDocCategorySizeDatePackage
SoundPlayer.javaAPI DocExample11967Sat Jan 24 10:44:38 GMT 2004je3.sound

SoundPlayer.java

/*
 * 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.
 */
package je3.sound;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.sound.sampled.*;
import javax.sound.midi.*;

/**
 * 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);
	}
    }
}