A Swing component that can load and play a sound clip, displaying progress and controls : MIDI « Development Class « Java






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

 








Related examples in the same category

1.Play Piano
2.Plays sounds streaming from a URL
3.Program the MIDI percussion channel with a Swing window