package org.limewire.player.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
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 org.limewire.player.api.AudioSource;
import org.limewire.util.FileUtils;
import org.limewire.util.GenericsUtils;
import org.limewire.util.GenericsUtils.ScanMode;
import org.tritonus.share.sampled.TAudioFormat;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/**
* <p>
* This handles creation/destruction of an audio source. This includes
* extracting audio properties about the source and any decoding that may
* need to take place. Once created, the owner can safely read/write from
* the input stream and to the dataline.
* </p>
* <p>
* When opening an input stream to read from, encoded formats such as
* .mp3, .flac, .ogg, .mp4, etc.., must be wrapped in their own unique
* audioInputStream which will decode all input streams into a PCM format.
* PCM is a format that the sound card will understand.
* </p>
* The process of initializing a new song to read from is as follows:
* <pre>
* - create an AudioInputStream (this creates AudioFormat information about the
* encoding of the input stream, such as # of channels, sameple rate,
* encoding format, etc.)
* - create a decoded AudioInputStream (now that we have the AudioFormat information
* about the encoded audio source, we can construct a proper AudioInputStream
* that will decode the audio source into PCM format)
* - Optional: extract audio properties about stream
* - create a SourceDataLine (depending on a the AudioFormat of the input stream and the
* sound card, a proper data line must be created to write to. An input Stream
* in mono will be handled differently than one in stereo, etc.) The sourceDataLine
* will write the information to the sound card for playback.
* - reading/writing - this is handled by the Object that created this instance
* - Finally: upon completion of writing the song, the input streams and
* sourceDataLine must be closed and discarded.
* </pre>
*/
public class LimeAudioFormat {
/**
* Property values that are loaded into the properties map if we can't
* parse the data using TAudioFileFormat.
*/
public static final String AUDIO_LENGTH_BYTES = "audio.length.bytes";
public static final String AUDIO_LENGTH_FRAMES = "audio.length.frames";
public static final String AUDIO_TYPE = "audio.type";
public static final String AUDIO_FRAMERATE_FPS = "audio.framerate.fps";
public static final String AUDIO_FRAMESIZE_BYTES = "audio.framesize.bytes";
public static final String AUDIO_SAMPLERATE_HZ = "audio.samplerate.hz";
public static final String AUDIO_SAMPLESIZE_BITS = "audio.samplesize.bits";
public static final String AUDIO_CHANNELS = "audio.channels";
/**
* Stream for reading from the audioSource.
*/
private AudioInputStream audioInputStream;
/**
* Stream for writing audio data to sound card from.
*/
private SourceDataLine sourceDataLine;
/**
* Stream for reading in encodedFormat. After decoding the inputStream, its
* no longer possible to use stream.available() to give us the correct
* current location so we save a reference of the encodedStream.
*/
private AudioInputStream encodedAudioInputStream;
/**
* Audio source currently reading from (url, file, input stream).
*/
private AudioSource audioSource;
/**
* Properties of the current audio source.
*/
private Map<String, Object> properties;
/**
* Total length of the current song in bytes.
*/
private long totalLength;
/**
* Control for the gain on the sourceDataLine.
*/
private FloatControl gainControl;
/**
* Loads a file into the player and initializes all the input and output streams.
* After loading the audio source, it is safe to begin reading/writing the audio
* source to the data line.
*/
public LimeAudioFormat(File file, long position) throws UnsupportedAudioFileException,
IOException, LineUnavailableException, NullPointerException {
// this( new AudioSource(file), position );
}
/**
* Loads a stream into the player and initializes all the input and output streams
* After loading the audio source, it is safe to begin reading/writing the audio
* source to the data line.
*/
public LimeAudioFormat(InputStream stream, long position) throws UnsupportedAudioFileException,
IOException, LineUnavailableException, NullPointerException {
// this( new AudioSource(stream), position );
}
/**
* Loads an audioSource into the player and initializes all the input and output streams
* After loading the audio source, it is safe to begin reading/writing the audio
* source to the data line.
*/
public LimeAudioFormat(AudioSource audioSource, long position) throws UnsupportedAudioFileException,
IOException, LineUnavailableException, NullPointerException {
if (audioSource == null)
throw new NullPointerException("Couldn't load song");
this.audioSource = audioSource;
encodedAudioInputStream = createAudioInputStream(audioSource, position);
properties = createProperties(audioSource);
if( audioSource.getFile() != null )
totalLength = audioSource.getFile().length();
else
totalLength = encodedAudioInputStream.available();
audioInputStream = createDecodedAudioInputStream(encodedAudioInputStream);
sourceDataLine = createSourceDataLine(audioInputStream);
}
/**
* Creates an audioInputStream for reading from. An audioInputStream is
* an inputStream with a specified audio format and length. Unlike
* InputStreams, the length is expressed in frames not bytes and the
* AudioFormat contains specifications for how the input stream is encoded
* such as number of bytes per frame, sample rate, # of channels, etc..
* <p>
* NOTE: The audioInputStream returned here is not guaranteed to
* write to the sound card. Most audio sources, even .wav files already
* in PCM format, need to be decoded to a proper format that the
* sourceDataLine can understand prior to reading from.
*
* @param source audio source to read from, either a file, url or
* inputStream
* @param skip number of frames from the beginning of the file to skip
* @return AudioInputStream - based on <code>source</code> creates an input
* stream containing audioFormat properties about the encoding of the
* stream
*/
public static AudioInputStream createAudioInputStream(AudioSource source,
long skip) throws UnsupportedAudioFileException, IOException, NullPointerException {
AudioInputStream stream;
if (source.getFile() != null) {
// skip doesn't guarantee to return to skip the exact number of frames
// requested, don't try and skip to close to the EOF to avoid overflow
if( skip < 0 || skip > source.getFile().length() - 10000 )
skip = 0;
// use RandomAccessStreams to speed up mp3 searches since its encoded
if (source.getFile().getName().toLowerCase(Locale.US).endsWith(".mp3")) {
RandomAudioInputStream i = new RandomAudioInputStream(
new RandomAccessFile(source.getFile(), "rw"));
i.skip(skip);
stream = AudioSystem.getAudioInputStream(i);
} else {
stream = AudioSystem.getAudioInputStream(source.getFile());
stream.skip(skip);
}
} else if (source.getStream() != null) {
stream = AudioSystem.getAudioInputStream(source.getStream());
} else if ( source.getURL() != null ) {
stream = AudioSystem.getAudioInputStream(source.getURL().openStream());
}
else
throw new IllegalArgumentException("Attempting to open invalid audio source");
return stream;
}
/**
* Creates a decoded audioInputStream. All audio input streams must be in a
* PCM format compatible with the OS and sound card in order to written
* correctly by the sound card. To write to the soundcard we open a source
* data line to read data from the input stream. The sourceDataLine expects
* data to be in a specific audio format regardless of how the data is encoded.
* <p>
* To ensure that all supported formats are decoded properly, the original
* audioInputStream is decoded into a new audioInputStream. The java AudioSystem
* uses a bit of reflection to create a new AudioInputStream which can decode
* a given audioInputStream into a PCM formatted stream.
*
*
* @param audioInputStream encoded inputStream to read from which contains
* specific audioFormat properties such as a number of channels,
* encoding method, sample rate, etc..
* @return AudioInputStream a decoded audioInputStream in PCM format
*/
public static AudioInputStream createDecodedAudioInputStream(
AudioInputStream audioInputStream) {
AudioFormat sourceFormat = audioInputStream.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, sourceFormat, LimeWirePlayer.EXTERNAL_BUFFER_SIZE);
// if audioInputStream already in PCM format of audio card, do nothing
if( AudioSystem.isLineSupported(info)) {
return audioInputStream;
} else {
int nSampleSizeInBits = sourceFormat.getSampleSizeInBits();
if (nSampleSizeInBits <= 0)
nSampleSizeInBits = 16;
if ((sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW)
|| (sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW))
nSampleSizeInBits = 16;
if (nSampleSizeInBits != 8)
nSampleSizeInBits = 16;
AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(),
nSampleSizeInBits, sourceFormat.getChannels(), sourceFormat
.getChannels()
* (nSampleSizeInBits / 8),
sourceFormat.getSampleRate(), false);
info = new DataLine.Info(SourceDataLine.class, targetFormat, LimeWirePlayer.EXTERNAL_BUFFER_SIZE);
// Use reflection to try and load the proper decoded to create a decoded stream.
return AudioSystem.getAudioInputStream(targetFormat, audioInputStream);
}
}
/**
* Opens a sourceDataLine for writing to an audio card from a given inputstream.
* SourceDataLines are the link between the source of an audiostream and the java Mixer.
* From the Mixer, all the input streams are combined and written to the sound card.
* SourceDataLines wrap a given audioInputStream and ensures that
* all inputs to the mixer are in the same format.
* <p>
* Each audioInputStream contains an audioFormat( ie. # of channels, frame size, sample
* rate, etc.). A SourceDataLine is created based on the audioFormat's properties.
*
* @param audioInputStreamthe decoded audio input stream that is being read from
* @return SourceDataLine a properly formated data line to write to based on the
* audio format of the audioInputStream
*/
private SourceDataLine createSourceDataLine(AudioInputStream audioInputStream)
throws LineUnavailableException {
return createSourceDataLine(audioInputStream, -1);
}
/**
* Opens a sourceDataLine for writing to an audio card from a given inputstream.
* SourceDataLines are the link between the source of an audiostream and the java Mixer.
* From the Mixer, all the input streams are combined and written to the sound card.
* SourceDataLines wrap a given audioInputStream and ensures that
* all inputs to the mixer are in the same format.
* <p>
* Each audioInputStream contains an audioFormat( ie. # of channels, frame size, sample
* rate, etc.). A SourceDataLine is created based on the audioFormat's properties.
*
* @param audioInputStream the decoded audio input stream that is being read from
* @return SourceDataLine a properly formated data line to write to based on the
* audio format of the audioInputStream
*/
private SourceDataLine createSourceDataLine(
AudioInputStream audioInputStream, int bufferSize)
throws LineUnavailableException {
if( audioInputStream == null )
throw new NullPointerException("input stream is null");
AudioFormat audioFormat = audioInputStream.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class,
audioFormat, AudioSystem.NOT_SPECIFIED);
SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
if (bufferSize <= 0)
bufferSize = line.getBufferSize();
line.open(audioFormat, bufferSize);
/*-- Is Gain Control supported ? --*/
if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
gainControl = (FloatControl) line
.getControl(FloatControl.Type.MASTER_GAIN);
}
return line;
}
/**
* Creates a map of properties about the current inputstream. Unlike many inputStreams,
* audioInputStreams have a variety of extra properties associated with them such as
* <pre>
* - frame size
* - sample rate
* - frames per second
* - audio type
* - length in # of frames
* - # of audio channels
* - etc.
* </pre>
* This information is often useful to the application that initiated the song. This information
* is extracted in case another class wishes to use it.
*
* @param source the audio source that the audioInputStream is created from for reading
* @return a Map<String,Object> containing properties about the audio source
*/
private static Map<String, Object> createProperties(AudioSource source)
throws UnsupportedAudioFileException, IOException {
AudioFileFormat audioFileFormat;
Map<String, Object> properties = new HashMap<String, Object>();
if (source.getFile() != null) {
audioFileFormat = AudioSystem.getAudioFileFormat(source.getFile());
} else if (source.getStream() != null) {
audioFileFormat = AudioSystem
.getAudioFileFormat(source.getStream());
} else
return properties;
if (audioFileFormat instanceof TAudioFileFormat) {
// Tritonus SPI compliant audio file format.
properties = GenericsUtils.scanForMap(
((TAudioFileFormat) audioFileFormat).properties(),
String.class, Object.class, ScanMode.REMOVE);
// Clone the Map because it is not mutable.
Map<String, Object> newMap = new HashMap<String, Object>(properties);
properties = newMap;
}
// Add JavaSound properties.
if (audioFileFormat.getByteLength() > 0)
properties.put(AUDIO_LENGTH_BYTES, audioFileFormat.getByteLength());
if (audioFileFormat.getFrameLength() > 0)
properties.put(AUDIO_LENGTH_FRAMES, audioFileFormat.getFrameLength());
if (audioFileFormat.getType() != null)
properties.put(AUDIO_TYPE, (audioFileFormat.getType().toString()));
AudioFormat audioFormat = audioFileFormat.getFormat();
if (audioFormat.getFrameRate() > 0)
properties.put(AUDIO_FRAMERATE_FPS, audioFormat.getFrameRate());
if (audioFormat.getFrameSize() > 0)
properties.put(AUDIO_FRAMESIZE_BYTES, audioFormat.getFrameSize());
if (audioFormat.getSampleRate() > 0)
properties.put(AUDIO_SAMPLERATE_HZ, audioFormat.getSampleRate());
if (audioFormat.getSampleSizeInBits() > 0)
properties.put(AUDIO_SAMPLESIZE_BITS, audioFormat
.getSampleSizeInBits());
if (audioFormat.getChannels() > 0)
properties.put(AUDIO_CHANNELS, audioFormat.getChannels());
if (audioFormat instanceof TAudioFormat) {
// Tritonus SPI compliant audio format.
Map<String, Object> addproperties = GenericsUtils.scanForMap(
((TAudioFormat) audioFormat).properties(), String.class,
Object.class, ScanMode.REMOVE);
properties.putAll(addproperties);
}
return properties;
}
/**
* @return the audio source of the inputStream
*/
public AudioSource getSource() {
return audioSource;
}
/**
* @return the audioInputStream for reading from
*/
public AudioInputStream getAudioInputStream() {
return audioInputStream;
}
/**
* @return the SourceDataLine for writing to
*/
public SourceDataLine getSourceDataLine() {
return sourceDataLine;
}
/**
* @return the properties associated with this audio source
* such as sampleRate, framesize, number of frames, etc..
*/
public Map<String, Object> getProperties() {
if(properties != null )
return properties;
else
return new HashMap<String,Object>();
}
/**
* @return the total number of frames in the input stream
*/
public long totalLength() {
return totalLength;
}
/**
* @return the number of frames left to read.
*/
public int available() {
int avail = -1;
if ( encodedAudioInputStream != null ) {
try {
avail = encodedAudioInputStream.available();
} catch (IOException e) {
//don't catch, can't read from stream
}
}
return avail;
}
/**
* Prior to writing to a new or stopped sourceDataLine, the dataLine needs to
* be opened.
*/
public void startSourceDataLine(){
if( sourceDataLine != null && !sourceDataLine.isRunning())
sourceDataLine.start();
}
/**
* Stops the current sourceDataLine from writing. This should be called when the
* stream has been paused with intent to reopen it.
*/
public void stopSourceDataLine(){
if( sourceDataLine != null && sourceDataLine.isRunning()){
sourceDataLine.flush();
sourceDataLine.stop();
}
}
/**
* @return frame position in the current song being played
*/
public int getEncodedStreamPosition() {
return (int)(totalLength - available());
}
/**
* Seeks to a current position in the song.
*
* @param position position from the beginning of the file to seek to
* @return the number of bytes actually skipped
*/
public long seek(long position) {
//TODO: modify javazoom mp3 decoder to support RandomAccessFiles at the bit level
// and add a new interface for a seek method.
return -1;
}
/**
* Closes all the open streams. This is a convenience method for when the
* the song is done being read from.
*/
public void closeStreams() {
//close our IO streams
FileUtils.close(encodedAudioInputStream);
FileUtils.close(audioInputStream);
if (sourceDataLine != null) {
sourceDataLine.stop();
sourceDataLine.close();
}
}
/**
* Returns Gain value.
*/
public float getGainValue() {
if (hasGainControl()) {
return gainControl.getValue();
} else {
return 0.0F;
}
}
/**
* Gets max Gain value.
*/
public float getMaximumGain() {
if (hasGainControl()) {
return gainControl.getMaximum();
} else {
return 0.0F;
}
}
/**
* Gets min Gain value.
*/
public float getMinimumGain() {
if (hasGainControl()) {
return gainControl.getMinimum();
} else {
return 0.0F;
}
}
/**
* Returns true if Gain control is supported.
*/
public boolean hasGainControl() {
if (gainControl == null) {
// Try to get Gain control again (to support J2SE 1.5)
if ((sourceDataLine != null)
&& (sourceDataLine
.isControlSupported(FloatControl.Type.MASTER_GAIN)))
gainControl = (FloatControl) sourceDataLine
.getControl(FloatControl.Type.MASTER_GAIN);
}
return gainControl != null;
}
/**
* Sets the gain(volume) for the outputline
*
* @param gain [0.0 <-> 1.0]
* @throws IOException thrown when the soundcard does not support this
* operation
*/
public void setGain(double fGain) throws IOException {
if (hasGainControl()) {
double ampGainDB = ((10.0f / 20.0f) * getMaximumGain())
- getMinimumGain();
double cste = Math.log(10.0) / 20;
double valueDB = getMinimumGain() + (1 / cste)
* Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain);
gainControl.setValue((float) valueDB);
} else
throw new IOException("Volume error");
}
}