001package bradleyross.music.imported;
002import java.io.*;
003// import java.net.*;
004// import java.awt.*;
005import java.awt.event.*;
006import javax.swing.*;
007import javax.swing.JSlider;
008import javax.swing.event.*;
009import javax.swing.border.*;
010import javax.sound.sampled.*;
011import javax.sound.midi.*;
012/*
013 * The following is taken from the LICENSE.txt file at
014 * https://github.com/oreillymedia/java_cookbook_3e
015 * 
016 * Copyright (c) Ian F. Darwin, http://www.darwinsys.com/, 1996-2013.
017 * All rights reserved. Software written by Ian F. Darwin and others.
018 *
019 * Redistribution and use in source and binary forms, with or without
020 * modification, are permitted provided that the following conditions
021 * are met:
022 * 1. Redistributions of source code must retain the above copyright
023 *    notice, this list of conditions and the following disclaimer.
024 * 2. Redistributions in binary form must reproduce the above copyright
025 *    notice, this list of conditions and the following disclaimer in the
026 *    documentation and/or other materials provided with the distribution.
027 *
028 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''
029 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
030 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
031 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
032 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
033 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
034 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
035 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
036 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
037 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
038 * POSSIBILITY OF SUCH DAMAGE.
039 *
040 * Java, the Duke mascot, and all variants of Sun's Java "steaming coffee
041 * cup" logo are trademarks of Oracle Americas (formerly Sun Microsystems).
042 * Sun's, and James Gosling's, pioneering role in inventing and promulgating 
043 * (and standardizing) the Java language and environment is gratefully acknowledged.
044 *
045 * The pioneering role of Dennis Ritchie and Bjarne Stroustrup, of AT&T, for
046 * inventing predecessor languages C and C++ is also gratefully acknowledged.
047 */
048/**
049 * This class is a Swing component that can load and play a sound clip,
050 * displaying progress and controls.  The main( ) method is a test program.
051 * This component can play sampled audio or MIDI files, but handles them 
052 * differently. For sampled audio, time is reported in microseconds, tracked in
053 * milliseconds and displayed in seconds and tenths of seconds. For midi
054 * files time is reported, tracked, and displayed in MIDI "ticks".
055 * This program does no transcoding, so it can only play sound files that use
056 * the PCM encoding.
057 * 
058 * <p>The first and only parameter for the main method is the name of the file
059 *    that provides the audio or midi sample.</p>
060 * <p>This is taken from Section 17.4 of the Third Edition of Java Examples in a Nutshell,
061 *    "Playing sounds with javax.sound".</p>
062 */
063@SuppressWarnings("serial")
064public class SoundPlayer extends JComponent {
065    boolean midi;            // Are we playing a midi file or a sampled one?
066    Sequence sequence;       // The contents of a MIDI file
067    Sequencer sequencer;     // We play MIDI Sequences with a Sequencer
068    Clip clip;               // Contents of a sampled audio file
069    boolean playing = false; // whether the sound is currently playing
070
071    // Length and position of the sound are measured in milliseconds for 
072    // sampled sounds and MIDI "ticks" for MIDI sounds
073    int audioLength;         // Length of the sound.  
074    int audioPosition = 0;   // Current position within the sound
075
076    // The following fields are for the GUI
077    JButton play;             // The Play/Stop button
078    JSlider progress;         // Shows and sets current position in sound
079    JLabel time;              // Displays audioPosition as a number
080    Timer timer;              // Updates slider every 100 milliseconds
081
082    // The main method just creates a SoundPlayer in a Frame and displays it
083    public static void main(String[  ] args) 
084        throws IOException,
085               UnsupportedAudioFileException,
086               LineUnavailableException,
087               MidiUnavailableException,
088               InvalidMidiDataException
089    {
090        SoundPlayer player;
091        File file;
092        if (args.length == 0) {
093                file=new File("/Users/bradleyross/Downloads/AGNICRT.MID");
094        } else {
095        file = new File(args[0]); 
096        } 
097        // This is the file we'll be playing
098        // Determine whether it is midi or sampled audio
099        boolean ismidi;
100        try {
101            // We discard the return value of this method; we just need to know
102            // whether it returns successfully or throws an exception
103            MidiSystem.getMidiFileFormat(file);
104            ismidi = true;
105        }
106        catch(InvalidMidiDataException e) {
107            ismidi = false;
108        }
109
110        // Create a SoundPlayer object to play the sound.
111        player = new SoundPlayer(file, ismidi);
112
113        // Put it in a window and play it
114        JFrame f = new JFrame("SoundPlayer");
115        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
116        f.getContentPane( ).add(player, "Center");
117        f.pack( );
118        f.setVisible(true);
119    }
120
121    // Create a SoundPlayer component for the specified file.
122    public SoundPlayer(File f, boolean isMidi)
123        throws IOException,
124               UnsupportedAudioFileException,
125               LineUnavailableException,
126               MidiUnavailableException,
127               InvalidMidiDataException
128    {
129        if (isMidi) {     // The file is a MIDI file
130            midi = true;
131            // First, get a Sequencer to play sequences of MIDI events
132            // That is, to send events to a Synthesizer at the right time.
133            sequencer = MidiSystem.getSequencer( );  // Used to play sequences
134            sequencer.open( );                       // Turn it on.
135
136            // Get a Synthesizer for the Sequencer to send notes to
137            Synthesizer synth = MidiSystem.getSynthesizer( );
138            synth.open( );  // acquire whatever resources it needs
139            
140            // The Sequencer obtained above may be connected to a Synthesizer
141            // by default, or it may not.  Therefore, we explicitly connect it.
142            Transmitter transmitter = sequencer.getTransmitter( );
143            Receiver receiver = synth.getReceiver( );
144            transmitter.setReceiver(receiver);
145            
146            // Read the sequence from the file and tell the sequencer about it
147            sequence = MidiSystem.getSequence(f);
148            sequencer.setSequence(sequence);
149            audioLength = (int)sequence.getTickLength( ); // Get sequence length
150        }
151        else {            // The file is sampled audio
152            midi = false;
153            // Getting a Clip object for a file of sampled audio data is kind
154            // of cumbersome.  The following lines do what we need.
155            AudioInputStream ain = AudioSystem.getAudioInputStream(f);
156            try {
157                DataLine.Info info =
158                    new DataLine.Info(Clip.class,ain.getFormat( ));
159                clip = (Clip) AudioSystem.getLine(info);
160                clip.open(ain);
161            }
162            finally { // We're done with the input stream.
163                ain.close( );
164            }
165            // Get the clip length in microseconds and convert to milliseconds
166            audioLength = (int)(clip.getMicrosecondLength( )/1000);
167        }
168
169        // Now create the basic GUI
170        play = new JButton("Play");                // Play/stop button
171        progress = new JSlider(0, audioLength, 0); // Shows position in sound
172        time = new JLabel("0");                    // Shows position as a #
173
174        // When clicked, start or stop playing the sound
175        play.addActionListener(new ActionListener( ) {
176                public void actionPerformed(ActionEvent e) {
177                    if (playing) stop( ); else play( );
178                }
179            });
180
181        // Whenever the slider value changes, first update the time label.
182        // Next, if we're not already at the new position, skip to it.
183        progress.addChangeListener(new ChangeListener( ) {
184                public void stateChanged(ChangeEvent e) {
185                    int value = progress.getValue( );
186                    // Update the time label
187                    if (midi) time.setText(value + "");
188                    else time.setText(value/1000 + "." +
189                                      (value%1000)/100);
190                    // If we're not already there, skip there.
191                    if (value != audioPosition) skip(value);
192                }
193            });
194        
195        // This timer calls the tick( ) method 10 times a second to keep 
196        // our slider in sync with the music.
197        timer = new javax.swing.Timer(100, new ActionListener( ) {
198                public void actionPerformed(ActionEvent e) { tick( ); }
199            });
200        
201        // put those controls in a row
202        Box row = Box.createHorizontalBox( );
203        row.add(play);
204        row.add(progress);
205        row.add(time);
206        
207        // And add them to this component.
208        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
209        this.add(row);
210
211        // Now add additional controls based on the type of the sound
212        if (midi) addMidiControls( );
213        else addSampledControls( );
214    }
215
216    /** Start playing the sound at the current position */
217    public void play( ) {
218        if (midi) sequencer.start( );
219        else clip.start( );
220        timer.start( );
221        play.setText("Stop");
222        playing = true;
223    }
224
225    /** Stop playing the sound, but retain the current position */
226    public void stop( ) {
227        timer.stop( );
228        if (midi) sequencer.stop( );
229        else clip.stop( );
230        play.setText("Play");
231        playing = false;
232    }
233
234    /** Stop playing the sound and reset the position to 0 */
235    public void reset( ) {
236        stop( );
237        if (midi) sequencer.setTickPosition(0);
238        else clip.setMicrosecondPosition(0);
239        audioPosition = 0; 
240        progress.setValue(0);
241    }
242
243    /** Skip to the specified position */
244    public void skip(int position) { // Called when user drags the slider
245        if (position < 0 || position > audioLength) return;
246        audioPosition = position;
247        if (midi) sequencer.setTickPosition(position);
248        else clip.setMicrosecondPosition(position * 1000);
249        progress.setValue(position); // in case skip( ) is called from outside
250    }
251
252    /** Return the length of the sound in ms or ticks */
253    public int getLength( ) { return audioLength; }
254
255    // An internal method that updates the progress bar.
256    // The Timer object calls it 10 times a second.
257    // If the sound has finished, it resets to the beginning
258    void tick( ) {
259        if (midi && sequencer.isRunning( )) {
260            audioPosition = (int)sequencer.getTickPosition( );
261            progress.setValue(audioPosition);
262        }
263        else if (!midi && clip.isActive( )) {
264            audioPosition = (int)(clip.getMicrosecondPosition( )/1000);
265            progress.setValue(audioPosition);
266        }
267        else reset( );  
268    }
269
270    // For sampled sounds, add sliders to control volume and balance
271    void addSampledControls( ) {
272        try {
273            FloatControl gainControl =
274                (FloatControl)clip.getControl(FloatControl.Type.MASTER_GAIN);
275            if (gainControl != null) this.add(createSlider(gainControl));
276        }
277        catch(IllegalArgumentException e) {
278            // If MASTER_GAIN volume control is unsupported, just skip it
279        }
280
281        try {
282            // FloatControl.Type.BALANCE is probably the correct control to
283            // use here, but it doesn't work for me, so I use PAN instead.
284            FloatControl panControl =
285                (FloatControl)clip.getControl(FloatControl.Type.PAN);
286            if (panControl != null) this.add(createSlider(panControl));
287        }
288        catch(IllegalArgumentException e) {  }
289    }
290
291
292    // Return a JSlider component to manipulate the supplied FloatControl
293    // for sampled audio.
294    JSlider createSlider(final FloatControl c) {
295        if (c == null) return null;
296        final JSlider s = new JSlider(0, 1000);
297        final float min = c.getMinimum( );
298        final float max = c.getMaximum( );
299        final float width = max-min;
300        float fval = c.getValue( );
301        s.setValue((int) ((fval-min)/width * 1000));
302
303        java.util.Hashtable labels = new java.util.Hashtable(3);
304        labels.put(new Integer(0), new JLabel(c.getMinLabel( )));
305        labels.put(new Integer(500), new JLabel(c.getMidLabel( )));
306        labels.put(new Integer(1000), new JLabel(c.getMaxLabel( )));
307        s.setLabelTable(labels);
308        s.setPaintLabels(true);
309
310        s.setBorder(new TitledBorder(c.getType( ).toString( ) + " " +
311                                     c.getUnits( )));
312
313        s.addChangeListener(new ChangeListener( ) {
314                public void stateChanged(ChangeEvent e) {
315                    int i = s.getValue( );
316                    float f = min + (i*width/1000.0f);
317                    c.setValue(f);
318                }
319            });
320        return s;
321    }
322
323    // For Midi files, create a JSlider to control the tempo,
324    // and create JCheckBoxes to mute or solo each MIDI track.
325    void addMidiControls( ) {
326        // Add a slider to control the tempo
327        final JSlider tempo = new JSlider(50, 200);
328        tempo.setValue((int)(sequencer.getTempoFactor( )*100));
329        tempo.setBorder(new TitledBorder("Tempo Adjustment (%)"));
330        java.util.Hashtable<Integer,JLabel> labels = new java.util.Hashtable<Integer,JLabel>( );
331        labels.put(new Integer(50), new JLabel("50%"));
332        labels.put(new Integer(100), new JLabel("100%"));
333        labels.put(new Integer(200), new JLabel("200%"));
334        tempo.setLabelTable(labels);
335        tempo.setPaintLabels(true);
336        // The event listener actually changes the tempo
337        tempo.addChangeListener(new ChangeListener( ) {
338                public void stateChanged(ChangeEvent e) {
339                    sequencer.setTempoFactor(tempo.getValue( )/100.0f);
340                }
341            });
342
343        this.add(tempo);
344
345        // Create rows of solo and checkboxes for each track
346        Track[  ] tracks = sequence.getTracks( );
347        for(int i = 0; i < tracks.length; i++) {
348            final int tracknum = i;
349            // Two checkboxes per track
350            final JCheckBox solo = new JCheckBox("solo");
351            final JCheckBox mute = new JCheckBox("mute");
352            // The listeners solo or mute the track
353            solo.addActionListener(new ActionListener( ) {
354                    public void actionPerformed(ActionEvent e) {
355                        sequencer.setTrackSolo(tracknum,solo.isSelected( ));
356                    }
357                });
358            mute.addActionListener(new ActionListener( ) {
359                    public void actionPerformed(ActionEvent e) {
360                        sequencer.setTrackMute(tracknum,mute.isSelected( ));
361                    }
362                });
363
364            // Build up a row
365            Box box = Box.createHorizontalBox( );
366            box.add(new JLabel("Track " + tracknum));
367            box.add(Box.createHorizontalStrut(10));
368            box.add(solo);
369            box.add(Box.createHorizontalStrut(10));
370            box.add(mute);
371            box.add(Box.createHorizontalGlue( ));
372            // And add it to this component
373            this.add(box);
374        }
375    }
376}