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