Mega Code Archive

 
Categories / Java / Development Class
 

A Swing component that can load and play a sound clip, displaying progress and controls

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