/** * Copyright (C) 2013 Colorado School of Mines * * This file is part of the Interface Software Development Kit (SDK). * * The InterfaceSDK is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The InterfaceSDK is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with the InterfaceSDK. If not, see <http://www.gnu.org/licenses/>. */ package edu.mines.acmX.exhibit.stdlib.sound; import java.io.File; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.BooleanControl; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; /** * A Track is responsible for playing the audio from a single * mp3 file. It is created by using the constructor which accepts * the name of the mp3 file. * * Typically, this class is not referenced directly other than * creating the track, and setting the controls for the Track which * include gain, muting, and a 32 channel equalizer. * * Normally a Song is created and is responsible for playing any Tracks * that might have been created. * * @author John * */ public class Track { private File soundFile; private AudioInputStream audioInputStream; private AudioInputStream decodedAudioInputStream; private AudioFormat audioFormat; private AudioFormat decodedAudioFormat; private SourceDataLine sourceDataLine; private CountDownLatch startLatch; //Used by the Song class to be started with the other Tracks private FloatControl gainControl; private BooleanControl muteControl; private float gain = 0.0f; private boolean mute = false; private float[] equalizer = new float[32]; private boolean isPlaying = false; /** * Creates a Track from the supplied file path. * @param fileName */ public Track(String fileName) { soundFile = new File(fileName); System.out.println("absolute path: " + soundFile.getAbsolutePath()); } /** * Starts playing the Track. */ public void startPlaying() { if (!isPlaying) { isPlaying = true; new PlayThread().start(); } } /** * Stops playing the Track. */ public void stopPlaying() { isPlaying = false; } /** * Returns whether the Track is currently playing. * @return */ public boolean isPlaying() { return isPlaying; } /** * Returns the sound file associated with this Track. * @return */ public File getSoundFile() { return soundFile; } /** * Sets the start latch for synchronizing the Tracks with each * other. This does not typically get called, except by the Song * class when starting to play the Tracks. * @param startLatch */ public void setStartLatch(CountDownLatch startLatch) { this.startLatch = startLatch; } /** * Sets the gain control value for the Track. * @param gain */ public void setGain(float gain) { this.gain = gain; } /** * Sets the mute control value for the track. * @param mute */ public void setMute(boolean mute) { this.mute = mute; } /** * Applies an EQ filter based on a 32 channel EQ that * is set by the float array. The indices of the array correspond * to each of the frequency levels that can be adjusted, with the * lower indices corresponding to the lower frequencies (bass), and the * higher indices corresponding to the higher frequencies (treble). * * This method makes no assumption on the size of the array passed to it * and will only set the first 32 indices in the array. * * @param equalizer */ public void setEqualizer(float[] equalizer) { System.arraycopy(equalizer, 0, this.equalizer , 0, 32); } /* * An internal method that sets the values for a BooleanControl. */ private void adjustBooleanControl(BooleanControl control, boolean value) { control.setValue(value); } /* * An internal method that ensures the values used to set a FloatControl * are within the bounds of the control. */ private void adjustFloatControl(FloatControl control, float value) { if (value < control.getMaximum() && value > control.getMinimum()) control.setValue(value); else if (value > control.getMaximum()) { control.setValue(control.getMaximum()); } else if (value < control.getMinimum()) { control.setValue(control.getMinimum()); } } /* * An internal method that returns the correct decoded audio format for the * types of files being used. */ private AudioFormat getDecodedAudioFormat(AudioFormat baseFormat) { AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED; float sampleRate = baseFormat.getSampleRate(); int sampleSizeInBits = 16; int channels = baseFormat.getChannels(); int frameSize = baseFormat.getChannels() * 2; float frameRate = baseFormat.getSampleRate(); boolean bigEndian = false; return new AudioFormat(encoding, sampleRate, sampleSizeInBits, channels, frameSize, frameRate, bigEndian); } /* * An internal class responsible for playing the Track without blocking the * main execution. */ private class PlayThread extends Thread { private final int BUFFER_SIZE = 2000; //Decent buffer size found with experimentation private byte tempBuffer[] = new byte[BUFFER_SIZE]; //Used to transfer data from the input and output streams public void run() { try { //Wait until the CountDownLatch has been 'lifted' startLatch.await(); try { //Read the sound file audioInputStream = AudioSystem.getAudioInputStream(soundFile); audioFormat = audioInputStream.getFormat(); //Decode the sound file decodedAudioFormat = getDecodedAudioFormat(audioFormat); decodedAudioInputStream = AudioSystem.getAudioInputStream(decodedAudioFormat, audioInputStream); //Get a source data line to put the sound file on DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, decodedAudioFormat); sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo); sourceDataLine.open(decodedAudioFormat); //Get the controls for the stream if they are supported if (sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) { gainControl = (FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN); } if (sourceDataLine.isControlSupported(BooleanControl.Type.MUTE)) { muteControl = (BooleanControl) sourceDataLine.getControl(BooleanControl.Type.MUTE); } float[] equalizerControl = new float[32]; if (decodedAudioInputStream instanceof javazoom.spi.PropertiesContainer) { @SuppressWarnings("rawtypes") Map properties = ((javazoom.spi.PropertiesContainer) decodedAudioInputStream).properties(); equalizerControl = (float[]) properties.get("mp3.equalizer"); } //Start streaming the sound file sourceDataLine.start(); int count; while ((count = decodedAudioInputStream.read(tempBuffer, 0, tempBuffer.length)) != -1 && isPlaying) { if (count > 0) { //Set the controls for the stream if they are supported if (sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) { adjustFloatControl(gainControl, gain); } if (sourceDataLine.isControlSupported(BooleanControl.Type.MUTE)) { adjustBooleanControl(muteControl, mute); } System.arraycopy(equalizer, 0, equalizerControl, 0, 32); //Write the data line to the buffer sourceDataLine.write(tempBuffer, 0, count); } } //The sound file is finished at this point //Clean up the data line sourceDataLine.drain(); sourceDataLine.close(); //Close the stream audioInputStream.close(); decodedAudioInputStream.close(); isPlaying = false; } catch (IOException e) { System.out.println("IO Error opening " + soundFile.getAbsolutePath()); e.printStackTrace(); System.exit(0); } catch (UnsupportedAudioFileException e) { System.out.println("Error opening " + soundFile.getAbsolutePath()); e.printStackTrace(); System.exit(0); } catch (LineUnavailableException e) { e.printStackTrace(); System.exit(0); } } catch (InterruptedException e) { System.out.println("Track play was interrupted."); e.printStackTrace(); } } } }