package com.limegroup.gnutella.gui.mp3;
/**
* BasicPlayer.
*
*-----------------------------------------------------------------------
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published
* by the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*----------------------------------------------------------------------
*/
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
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;
import javax.swing.SwingUtilities;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.metadata.AudioMetaData;
import com.limegroup.gnutella.util.ManagedThread;
/**
* This code was lifted from the jlGui project (below). Made some custom
* changes to fit into our framework. This version of BasicPlayer can handle
* very low bitrate (LBR, 16-74) files.
*
* BasicPlayer implements basics features of a player. The playback is done
* with a thread.
* BasicPlayer is the result of jlGui 0.5 from JavaZOOM and BaseAudioStream
* from Matthias Pfisterer JavaSound examples.
*
* @author E.B from JavaZOOM
*
* Homepage : http://www.javazoom.net
*
*/
public class BasicPlayer extends AbstractAudioPlayer implements Runnable
{
private static final Log LOG = LogFactory.getLog(BasicPlayer.class);
private static final int EXTERNAL_BUFFER_SIZE = 4000 * 4;
private Thread m_thread = null;
private Object m_dataSource;
private AudioInputStream m_audioInputStream;
private AudioFileFormat m_audioFileFormat;
private SourceDataLine m_line;
private FloatControl m_gainControl;
private FloatControl m_panControl;
/**
* These variables are used to distinguish stopped, paused, playing states.
* We need them to control Thread.
*/
public static final int PAUSED=1;
public static final int PLAYING=0;
public static final int STOPPED=2;
public static final int READY=3;
private int m_status = READY;
private long doSeek = -1;
private File _file = null;
// used to keep track of frames read...
private int m_framesRead = 0;
/**
* Constructs a Basic Player.
*/
public BasicPlayer()
{
m_dataSource = null;
m_audioInputStream = null;
m_audioFileFormat = null;
m_line = null;
m_gainControl = null;
m_panControl = null;
}
/*****----------------------------
* HOW TO BE A ABSTRACTAUDIOPLAYER
*****----------------------------/
/**
* Returns BasicPlayer status.
*/
public int getStatus() {
switch (m_status) {
case PAUSED:
return STATUS_PAUSED;
case PLAYING:
return STATUS_PLAYING;
case STOPPED:
return STATUS_STOPPED;
default:
return STATUS_STOPPED;
}
}
public void unpause() {
resumePlayback();
}
public void pause() {
pausePlayback();
}
public void stop() {
stopPlayback();
}
public boolean play(final File toPlay) throws IOException {
String reason;
try {
setDataSource(toPlay);
return startPlayback();
}
catch (UnsupportedAudioFileException ignored) {
reason = "UNSUPPORTED";
}
catch (LineUnavailableException ignored) {
reason = "UNAVAILABLE";
}
catch (FileNotFoundException ignored) {
reason = "MISSING";
}
catch (EOFException ignored) {
reason = "CORRUPT";
}
catch (IOException ignored) {
reason = "UNKNOWN";
}
catch (StringIndexOutOfBoundsException ignored) {
reason = "PARSE_PROBLEM";
}
final String raisin = reason;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
GUIMediator.showError("PLAYLIST_CANNOT_PLAY_FILE", toPlay, "PLAYLIST_FILE_" + raisin);
}
});
return false;
}
public int getFrameSeek() {
return 0;
}
public void refresh() {
if(getStatus() == STATUS_PLAYING) {
fireAudioPositionUpdated(m_framesRead);
}
}
/*****--------------------------------
* HOW TO BE A ABSTRACTAUDIOPLAYER END
*****-------------------------------/
/**
* Sets the data source as a file.
*/
private void setDataSource(File file) throws UnsupportedAudioFileException, LineUnavailableException, IOException
{
if (file != null)
{
m_dataSource = file;
initAudioInputStream();
}
}
/**
* Sets the data source as an url.
*/
private void setDataSource(URL url) throws UnsupportedAudioFileException, LineUnavailableException, IOException
{
if (url != null)
{
m_dataSource = url;
initAudioInputStream();
}
}
/**
* Inits Audio ressources from the data source.<br>
* - AudioInputStream <br>
* - AudioFileFormat
*/
private void initAudioInputStream() throws UnsupportedAudioFileException, LineUnavailableException, IOException
{
if (m_dataSource instanceof URL)
{
initAudioInputStream((URL) m_dataSource);
}
else if (m_dataSource instanceof File)
{
initAudioInputStream((File) m_dataSource);
}
}
/**
* Inits Audio ressources from file.
*/
private void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException
{
_file = file;
m_audioInputStream = AudioSystem.getAudioInputStream(file);
m_audioFileFormat = AudioSystem.getAudioFileFormat(file);
}
/**
* Inits Audio ressources from URL.
*/
private void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException
{
m_audioInputStream = AudioSystem.getAudioInputStream(url);
m_audioFileFormat = AudioSystem.getAudioFileFormat(url);
}
/**
* Inits Audio ressources from AudioSystem.<br>
* DateSource must be present.
*/
protected void initLine() throws LineUnavailableException
{
if (m_line == null)
{
createLine();
LOG.trace("Create Line OK ");
openLine();
}
else
{
AudioFormat lineAudioFormat = m_line.getFormat();
AudioFormat audioInputStreamFormat = m_audioInputStream == null ? null : m_audioInputStream.getFormat();
if (!lineAudioFormat.equals(audioInputStreamFormat))
{
m_line.close();
openLine();
}
}
}
/**
* Inits a DateLine.<br>
*
* We check if the line supports Volume and Pan controls.
*
* From the AudioInputStream, i.e. from the sound file, we
* fetch information about the format of the audio data. These
* information include the sampling frequency, the number of
* channels and the size of the samples. There information
* are needed to ask JavaSound for a suitable output line
* for this audio file.
* Furthermore, we have to give JavaSound a hint about how
* big the internal buffer for the line should be. Here,
* we say AudioSystem.NOT_SPECIFIED, signaling that we don't
* care about the exact size. JavaSound will use some default
* value for the buffer size.
*/
private void createLine() throws LineUnavailableException
{
if (m_line == null)
{
AudioFormat sourceFormat = m_audioInputStream.getFormat();
if(LOG.isDebugEnabled())
LOG.debug("Source format : " + sourceFormat);
AudioFormat targetFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
sourceFormat.getSampleRate(),
16,
sourceFormat.getChannels(),
sourceFormat.getChannels() * 2,
sourceFormat.getSampleRate(),
false);
if(LOG.isDebugEnabled())
LOG.debug("Target format: " + targetFormat);
m_audioInputStream = AudioSystem.getAudioInputStream(targetFormat, m_audioInputStream);
AudioFormat audioFormat = m_audioInputStream.getFormat();
if(LOG.isDebugEnabled())
LOG.debug("Create Line : " + audioFormat);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
m_line = (SourceDataLine) AudioSystem.getLine(info);
/*-- Display supported controls --*/
Control[] c = m_line.getControls();
for (int p=0;p<c.length;p++)
{
if(LOG.isDebugEnabled())
LOG.debug("Controls : "+c[p].toString());
}
/*-- Is Gain Control supported ? --*/
if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN))
{
m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN);
if(LOG.isDebugEnabled())
LOG.debug("Master Gain Control : ["+m_gainControl.getMinimum()+","+m_gainControl.getMaximum()+"],"+m_gainControl.getPrecision());
}
/*-- Is Pan control supported ? --*/
if (m_line.isControlSupported(FloatControl.Type.PAN))
{
m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN);
if(LOG.isDebugEnabled())
LOG.debug("Pan Control : ["+ m_panControl.getMinimum()+","+m_panControl.getMaximum()+"]," + m_panControl.getPrecision());
}
}
}
/**
* Opens the line.
*/
private void openLine() throws LineUnavailableException
{
if (m_line != null)
{
AudioFormat audioFormat = m_audioInputStream.getFormat();
if(LOG.isDebugEnabled())
LOG.debug("AudioFormat : "+audioFormat);
m_line.open(audioFormat, m_line.getBufferSize());
}
}
/**
* Stops the playback.<br>
*
* Player Status = STOPPED.<br>
* Thread should free Audio ressources.
*/
private void stopPlayback()
{
if ( (m_status == PLAYING) || (m_status == PAUSED) )
{
if (m_line != null)
m_line.flush();
if (m_line != null)
m_line.stop();
m_status = STOPPED;
LOG.debug("Stop called");
}
}
/**
* Pauses the playback.<br>
*
* Player Status = PAUSED.
*/
private void pausePlayback()
{
if (m_line != null)
{
if (m_status == PLAYING)
{
m_line.flush();
m_line.stop();
m_status = PAUSED;
LOG.debug("Pause called");
}
}
}
/**
* Resumes the playback.<br>
*
* Player Status = PLAYING.
*/
private void resumePlayback()
{
if (m_line != null)
{
if (m_status == PAUSED)
{
m_line.start();
m_status = PLAYING;
LOG.debug("Resume called");
}
}
}
/**
* Starts playback.
*/
private boolean startPlayback()
{
if ((m_status == STOPPED) || (m_status == READY))
{
LOG.debug("Start called");
if (!(m_thread == null || !m_thread.isAlive()))
{
LOG.debug("WARNING: old thread still running!!");
int cnt = 0;
while (m_status != READY)
{
try
{
if (m_thread != null)
{
cnt++;
m_thread.sleep(1000);
if (cnt > 2) m_thread.interrupt();
}
} catch (Exception e) {
LOG.debug("Waiting Error", e);
}
if(LOG.isDebugEnabled())
LOG.debug("Waiting ... "+cnt);
}
}
try
{
initLine();
} catch (Exception e) {
LOG.debug("cannot init Line", e);
//e.printStackTrace();
return false;
}
LOG.trace("Creating new ManagedThread");
m_thread = new ManagedThread(this, "BasicPlayer");
m_thread.setDaemon(true);
m_thread.start();
if (m_line != null)
{
m_line.start();
return true;
}
}
return false;
}
/**
* Main loop.
*
* Player Status == STOPPED => End of Thread + Freeing Audio Ressources.<br>
* Player Status == PLAYING => Audio stream data sent to Audio line.<br>
* Player Status == PAUSED => Waiting for another status.
*/
public void run()
{
LOG.debug("Thread Running");
//if (m_audioInputStream.markSupported()) m_audioInputStream.mark(m_audioFileFormat.getByteLength());
//else trace(1,getClass().getName(), "Mark not supported");
int nBytesRead = 1;
m_status = PLAYING;
int nBytesCursor = 0;
byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
float nFrameSize = (float) m_line.getFormat().getFrameSize();
float nFrameRate = m_line.getFormat().getFrameRate();
float bytesPerSecond = nFrameSize*nFrameRate;
int secondsTotal = Math.round((float)m_audioFileFormat.getByteLength()/bytesPerSecond);
try {
AudioMetaData amd = AudioMetaData.parseAudioFile(_file);
if(amd != null)
secondsTotal = amd.getLength();
} catch(IOException ignored) {}
fireSeekSetupRequired(secondsTotal);
try{
while ( (nBytesRead != -1) && (m_status != STOPPED) )
{
if (m_status == PLAYING)
{
try
{
if (doSeek > -1 )
{
// Seek implementation. WAV format only !
if (( getAudioFileFormat() != null) && (getAudioFileFormat().getType().toString().startsWith("WAV")) )
{
if ( (secondsTotal != AudioSystem.NOT_SPECIFIED) && (secondsTotal > 0) )
{
m_line.flush();
m_line.stop();
//m_audioInputStream.reset();
m_audioInputStream.close();
m_audioInputStream = AudioSystem.getAudioInputStream(_file);
nBytesCursor = 0;
if (m_audioFileFormat.getByteLength()-doSeek < abData.length) doSeek = m_audioFileFormat.getByteLength() - abData.length;
doSeek = doSeek - doSeek%4;
int toSkip = (int) doSeek;
// skip(...) instead of read(...) runs out of memory ?!
while ( (toSkip > 0) && (nBytesRead > 0) )
{
if (toSkip > abData.length) nBytesRead = m_audioInputStream.read(abData, 0, abData.length);
else nBytesRead = m_audioInputStream.read(abData, 0, toSkip);
toSkip = toSkip - nBytesRead;
nBytesCursor = nBytesCursor + nBytesRead;
}
m_line.start();
}
else {
if(LOG.isDebugEnabled())
LOG.debug("Seek not supported for this InputStream : "+secondsTotal);
}
}
else
{
if(LOG.isDebugEnabled())
LOG.debug("Seek not supported for this InputStream : "+secondsTotal);
}
doSeek = -1;
}
nBytesRead = m_audioInputStream.read(abData, 0, abData.length);
}
catch (Exception e)
{
if(LOG.isDebugEnabled())
LOG.debug("InputStream error : ("+nBytesRead+")", e);
e.printStackTrace();
m_status = STOPPED;
}
if (nBytesRead >= 0)
{
// make sure that you are writing an integral number of the
// frame size (nFrameSize). i think this may skip a few
// frames but probably not a big deal.
if (nBytesRead % nFrameSize != 0)
nBytesRead -= (nBytesRead % nFrameSize);
int nBytesWritten = m_line.write(abData, 0, nBytesRead);
nBytesCursor = nBytesCursor + nBytesWritten;
m_framesRead =
((int) Math.round((float)nBytesCursor/bytesPerSecond));
}
}
else
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOG.debug("can't sleep", e);
}
}
}
}finally {
// close the file and free the audio line.
try{
if (m_line != null){
try{
m_line.drain();
m_line.stop();
} finally {
try {
m_line.close();
}catch(SecurityException ignored){
LOG.trace("Cannot Free Audio ressources", ignored);
}
m_line = null;
}
}
}finally {
if (m_audioInputStream!=null)
try {
m_audioInputStream.close();
}catch(IOException ignored){}
}
}
LOG.trace("Thread Stopped");
firePlayComplete();
m_status = READY;
}
/*----------------------------------------------*/
/*-- Gain Control --*/
/*----------------------------------------------*/
/**
* Returns true if Gain control is supported.
*/
public boolean hasGainControl()
{
return m_gainControl != null;
}
/**
* Sets Gain value.
* Linear scale 0.0 <--> 1.0
* Threshold Coef. : 1/2 to avoid saturation.
*/
public void setGain(double fGain)
{
if (hasGainControl())
{
double minGainDB = getMinimum();
double ampGainDB = ((10.0f/20.0f)*getMaximum()) - getMinimum();
double cste = Math.log(10.0)/20;
double valueDB = minGainDB + (1/cste)*Math.log(1+(Math.exp(cste*ampGainDB)-1)*fGain);
//trace(1,getClass().getName(), "Gain : "+valueDB);
m_gainControl.setValue((float)valueDB);
}
}
/**
* Returns Gain value.
*/
public float getGain()
{
if (hasGainControl())
{
return m_gainControl.getValue();
}
else
{
return 0.0F;
}
}
/**
* Gets max Gain value.
*/
public float getMaximum()
{
if (hasGainControl())
{
return m_gainControl.getMaximum();
}
else
{
return 0.0F;
}
}
/**
* Gets min Gain value.
*/
public float getMinimum()
{
if (hasGainControl())
{
return m_gainControl.getMinimum();
}
else
{
return 0.0F;
}
}
/*----------------------------------------------*/
/*-- Pan Control --*/
/*----------------------------------------------*/
/**
* Returns true if Pan control is supported.
*/
public boolean hasPanControl()
{
return m_panControl != null;
}
/**
* Returns Pan precision.
*/
public float getPrecision()
{
if (hasPanControl())
{
return m_panControl.getPrecision();
}
else
{
return 0.0F;
}
}
/**
* Returns Pan value.
*/
public float getPan()
{
if (hasPanControl())
{
return m_panControl.getValue();
}
else
{
return 0.0F;
}
}
/**
* Sets Pan value.
* Linear scale : -1.0 <--> +1.0
*/
public void setPan(float fPan)
{
if (hasPanControl())
{
//trace(1,getClass().getName(), "Pan : "+fPan);
m_panControl.setValue(fPan);
}
}
/*----------------------------------------------*/
/*-- Seek --*/
/*----------------------------------------------*/
/**
* Sets Seek value.
* Linear scale : 0.0 <--> +1.0
*/
public void setSeek(double seek) throws IOException
{
double length = -1;
if ( (m_audioFileFormat != null) && (m_audioFileFormat.getByteLength() != AudioSystem.NOT_SPECIFIED) ) length = (double) m_audioFileFormat.getByteLength();
long newPos = (long) Math.round(seek*length);
doSeek = newPos;
}
/*----------------------------------------------*/
/*-- Audio Format --*/
/*----------------------------------------------*/
/**
* Returns source AudioFormat.
*/
public AudioFormat getAudioFormat()
{
if (m_audioFileFormat != null)
{
return m_audioFileFormat.getFormat();
}
else return null;
}
/**
* Returns source AudioFileFormat.
*/
public AudioFileFormat getAudioFileFormat()
{
if (m_audioFileFormat != null)
{
return m_audioFileFormat;
}
else return null;
}
/**
* Gets an InputStream from File.
*/
protected InputStream openInput(File file) throws IOException
{
InputStream fileIn = new FileInputStream(file);
BufferedInputStream bufIn = new BufferedInputStream(fileIn);
return bufIn;
}
/*----------------------------------------------*/
/*-- Misc --*/
/*----------------------------------------------*/
}