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}