/**
* Xtreme Media Player a cross-platform media player.
* Copyright (C) 2005-2011 Besmir Beqiri
*
* This program 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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package xtrememp.player.audio;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tritonus.share.sampled.TAudioFormat;
import org.tritonus.share.sampled.file.TAudioFileFormat;
import javazoom.spi.PropertiesContainer;
import xtrememp.player.dsp.DigitalSignalSynchronizer;
/**
*
* @author Besmir Beqiri
*/
public class AudioPlayer implements Callable<Void> {
private final Logger logger = LoggerFactory.getLogger(AudioPlayer.class);
protected final int READ_BUFFER_SIZE = 4 * 1024;
protected final Lock lock = new ReentrantLock();
protected final Condition pauseCondition = lock.newCondition();
protected Object audioSource;
protected DigitalSignalSynchronizer dss;
protected AudioFileFormat audioFileFormat;
protected AudioInputStream audioInputStream;
protected SourceDataLine sourceDataLine;
protected String mixerName;
protected List<PlaybackListener> listeners;
protected ExecutorService execService;
protected Future<Void> future;
protected Map<String, Object> properties;
protected FloatControl gainControl;
protected FloatControl panControl;
protected BooleanControl muteControl;
protected int bufferSize = AudioSystem.NOT_SPECIFIED;
public static final int INIT = 0;
public static final int PLAY = 1;
public static final int PAUSE = 2;
public static final int SEEK = 3;
public static final int STOP = 4;
protected volatile int state = AudioSystem.NOT_SPECIFIED;
protected Map<String, Object> emptyMap = new HashMap<String, Object>();
protected long oldPosition = 0;
public AudioPlayer() {
execService = Executors.newFixedThreadPool(1);
dss = new DigitalSignalSynchronizer();
listeners = new ArrayList<PlaybackListener>();
reset();
}
public void addPlaybackListener(PlaybackListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removePlaybackListener(PlaybackListener listener) {
if (listener != null) {
listeners.remove(listener);
}
}
public List<PlaybackListener> getPlaybackListeners() {
return listeners;
}
protected void notifyEvent(Playback state) {
notifyEvent(state, emptyMap);
}
protected void notifyEvent(Playback state, Map properties) {
for (PlaybackListener listener : listeners) {
PlaybackEventLauncher launcher = new PlaybackEventLauncher(this,
state, getPosition() - oldPosition, properties, listener);
launcher.start();
}
logger.info("{}", state);
}
private void reset() {
if (sourceDataLine != null) {
sourceDataLine.flush();
sourceDataLine.close();
sourceDataLine = null;
}
audioFileFormat = null;
gainControl = null;
panControl = null;
muteControl = null;
future = null;
emptyMap.clear();
oldPosition = 0;
}
/**
* Open file to play.
* @param file
* @throws PlayerException
*/
public void open(File file) throws PlayerException {
if (file != null) {
audioSource = file;
init();
}
}
/**
* Open URL to play.
* @param url
* @throws PlayerException
*/
public void open(URL url) throws PlayerException {
if (url != null) {
audioSource = url;
init();
}
}
protected void init() throws PlayerException {
notifyEvent(Playback.BUFFERING);
int oldState = state;
state = AudioSystem.NOT_SPECIFIED;
if (oldState == INIT || oldState == PAUSE) {
lock.lock();
try {
pauseCondition.signal();
} finally {
lock.unlock();
}
}
awaitTermination();
lock.lock();
try {
reset();
initAudioInputStream();
initSourceDataLine();
} finally {
lock.unlock();
}
}
/**
* Inits AudioInputStream and AudioFileFormat from the data source.
* @throws PlayerException
*/
@SuppressWarnings("unchecked")
protected void initAudioInputStream() throws PlayerException {
// Close any previous opened audio stream before creating a new one.
closeStream();
if (audioInputStream == null) {
try {
logger.info("Data source: {}", audioSource);
if (audioSource instanceof File) {
initAudioInputStream((File) audioSource);
} else if (audioSource instanceof URL) {
initAudioInputStream((URL) audioSource);
}
AudioFormat sourceAudioFormat = audioInputStream.getFormat();
logger.info("Source format: {}", sourceAudioFormat);
int nSampleSizeInBits = sourceAudioFormat.getSampleSizeInBits();
if (nSampleSizeInBits <= 0) {
nSampleSizeInBits = 16;
}
if ((sourceAudioFormat.getEncoding() == AudioFormat.Encoding.ULAW) || (sourceAudioFormat.getEncoding() == AudioFormat.Encoding.ALAW)) {
nSampleSizeInBits = 16;
}
if (nSampleSizeInBits != 8) {
nSampleSizeInBits = 16;
}
AudioFormat targetAudioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceAudioFormat.getSampleRate(), nSampleSizeInBits, sourceAudioFormat.getChannels(), sourceAudioFormat.getChannels() * (nSampleSizeInBits / 8), sourceAudioFormat.getSampleRate(), false);
logger.info("Target format: {}", targetAudioFormat);
// Create decoded stream.
audioInputStream = AudioSystem.getAudioInputStream(targetAudioFormat, audioInputStream);
if (audioFileFormat instanceof TAudioFileFormat) {
// Tritonus SPI compliant audio file format.
properties = ((TAudioFileFormat) audioFileFormat).properties();
// Clone the Map because it is not mutable.
properties = deepCopy(properties);
} else {
properties = new HashMap<String, Object>();
}
// Add JavaSound properties.
if (audioFileFormat.getByteLength() > 0) {
properties.put("audio.length.bytes",
Integer.valueOf(audioFileFormat.getByteLength()));
}
if (audioFileFormat.getFrameLength() > 0) {
properties.put("audio.length.frames", Integer.valueOf(audioFileFormat.getFrameLength()));
}
if (audioFileFormat.getType() != null) {
properties.put("audio.type", audioFileFormat.getType().toString());
}
// Audio format.
AudioFormat audioFormat = audioFileFormat.getFormat();
if (audioFormat.getFrameRate() > 0) {
properties.put("audio.framerate.fps", new Float(audioFormat.getFrameRate()));
}
if (audioFormat.getFrameSize() > 0) {
properties.put("audio.framesize.bytes", Integer.valueOf(audioFormat.getFrameSize()));
}
if (audioFormat.getSampleRate() > 0) {
properties.put("audio.samplerate.hz", new Float(audioFormat.getSampleRate()));
}
if (audioFormat.getSampleSizeInBits() > 0) {
properties.put("audio.samplesize.bits", Integer.valueOf(audioFormat.getSampleSizeInBits()));
}
if (audioFormat.getChannels() > 0) {
properties.put("audio.channels", Integer.valueOf(audioFormat.getChannels()));
}
if (audioFormat instanceof TAudioFormat) {
// Tritonus SPI compliant audio format.
properties.putAll(((TAudioFormat) audioFormat).properties());
}
for (String key : properties.keySet()) {
logger.info("Audio Format Properties: {} = {}", key, properties.get(key));
}
} catch (UnsupportedAudioFileException ex) {
throw new PlayerException(ex);
} catch (IOException ex) {
throw new PlayerException(ex);
}
}
}
/**
* Inits Audio resources from file.
* @param file
* @throws javax.sound.sampled.UnsupportedAudioFileException
* @throws java.io.IOException
*/
protected void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException {
audioInputStream = AudioSystem.getAudioInputStream(file);
audioFileFormat = AudioSystem.getAudioFileFormat(file);
}
/**
* Inits Audio resources from URL.
* @param url
* @throws javax.sound.sampled.UnsupportedAudioFileException
* @throws java.io.IOException
*/
protected void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException {
audioInputStream = AudioSystem.getAudioInputStream(url);
audioFileFormat = AudioSystem.getAudioFileFormat(url);
}
/**
* Inits Audio resources from AudioSystem.
* @throws PlayerException
*/
protected void initSourceDataLine() throws PlayerException {
if (sourceDataLine == null) {
try {
logger.info("Create Source Data Line");
AudioFormat audioFormat = audioInputStream.getFormat();
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
if (!AudioSystem.isLineSupported(lineInfo)) {
throw new PlayerException(lineInfo + " is not supported");
}
if (mixerName == null) {
// Primary Sound Driver
mixerName = getMixers().get(0);
}
Mixer mixer = getMixer(mixerName);
if (mixer != null) {
logger.info("Mixer: {}", mixer.getMixerInfo().toString());
sourceDataLine = (SourceDataLine) mixer.getLine(lineInfo);
} else {
sourceDataLine = (SourceDataLine) AudioSystem.getLine(lineInfo);
mixerName = null;
}
sourceDataLine.addLineListener(dss);
logger.info("Line Info: {}", sourceDataLine.getLineInfo().toString());
logger.info("Line AudioFormat: {}", sourceDataLine.getFormat().toString());
if (bufferSize <= 0) {
bufferSize = sourceDataLine.getBufferSize();
}
sourceDataLine.open(audioFormat, bufferSize);
logger.info("Line BufferSize: {}", sourceDataLine.getBufferSize());
for (Control c : sourceDataLine.getControls()) {
logger.info("Line Controls: {}", c);
}
if (sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
gainControl = (FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
}
if (sourceDataLine.isControlSupported(FloatControl.Type.PAN)) {
panControl = (FloatControl) sourceDataLine.getControl(FloatControl.Type.PAN);
}
if (sourceDataLine.isControlSupported(BooleanControl.Type.MUTE)) {
muteControl = (BooleanControl) sourceDataLine.getControl(BooleanControl.Type.MUTE);
}
sourceDataLine.start();
state = INIT;
future = execService.submit(this);
notifyEvent(Playback.OPENED);
} catch (LineUnavailableException ex) {
throw new PlayerException(ex);
}
}
}
/**
* Set SourceDataLine buffer size. It affects audio latency
* (the delay between SourceDataLine.write(data) and real sound).
* @param bufferSize if equal to -1 (AudioSystem.NOT_SPECIFIED)
* means maximum buffer size available.
*/
public void setBufferSize(int bufferSize) {
if (bufferSize <= 0) {
this.bufferSize = AudioSystem.NOT_SPECIFIED;
} else {
this.bufferSize = bufferSize;
}
}
/**
* Return SourceDataLine buffer size.
* @return -1 (AudioSystem.NOT_SPECIFIED) for maximum buffer size.
*/
public int getBufferSize() {
return bufferSize;
}
/**
* Deep copy of a Map.
* @param src
* @return map
*/
protected Map<String, Object> deepCopy(Map<String, Object> src) {
Map<String, Object> map = new HashMap<String, Object>();
if (src != null) {
Set<String> keySet = src.keySet();
for (String key : keySet) {
Object value = src.get(key);
map.put(key, value);
// logger.info("key: {}, value: {}", key, value);
}
}
return map;
}
public List<String> getMixers() {
List<String> mixers = new ArrayList<String>();
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
if (mixerInfos != null) {
for (int i = 0, len = mixerInfos.length; i < len; i++) {
Line.Info lineInfo = new Line.Info(SourceDataLine.class);
Mixer _mixer = AudioSystem.getMixer(mixerInfos[i]);
if (_mixer.isLineSupported(lineInfo)) {
mixers.add(mixerInfos[i].getName());
}
}
}
return mixers;
}
public Mixer getMixer(String name) {
Mixer _mixer = null;
if (name != null) {
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
if (mixerInfos != null) {
for (int i = 0, len = mixerInfos.length; i < len; i++) {
if (mixerInfos[i].getName().equals(name)) {
_mixer = AudioSystem.getMixer(mixerInfos[i]);
break;
}
}
}
}
return _mixer;
}
public String getMixerName() {
return mixerName;
}
public void setMixerName(String name) {
mixerName = name;
}
public long getDuration() {
long duration = AudioSystem.NOT_SPECIFIED;
if (properties.containsKey("duration")) {
duration = ((Long) properties.get("duration")).longValue();
} else {
duration = getTimeLengthEstimation(properties);
}
return duration;
}
public int getByteLength() {
int bytesLength = AudioSystem.NOT_SPECIFIED;
if (properties != null) {
if (properties.containsKey("audio.length.bytes")) {
bytesLength = ((Integer) properties.get("audio.length.bytes")).intValue();
}
}
return bytesLength;
}
public int getPositionByte() {
int positionByte = AudioSystem.NOT_SPECIFIED;
if (properties != null) {
if (properties.containsKey("mp3.position.byte")) {
positionByte = ((Integer) properties.get("mp3.position.byte")).intValue();
return positionByte;
}
if (properties.containsKey("ogg.position.byte")) {
positionByte = ((Integer) properties.get("ogg.position.byte")).intValue();
return positionByte;
}
}
return positionByte;
}
protected long getTimeLengthEstimation(Map properties) {
long milliseconds = AudioSystem.NOT_SPECIFIED;
int byteslength = AudioSystem.NOT_SPECIFIED;
if (properties != null) {
if (properties.containsKey("audio.length.bytes")) {
byteslength = ((Integer) properties.get("audio.length.bytes")).intValue();
}
if (properties.containsKey("duration")) {
milliseconds = (int) (((Long) properties.get("duration")).longValue()) / 1000;
} else {
// Try to compute duration
int bitspersample = AudioSystem.NOT_SPECIFIED;
int channels = AudioSystem.NOT_SPECIFIED;
float samplerate = AudioSystem.NOT_SPECIFIED;
int framesize = AudioSystem.NOT_SPECIFIED;
if (properties.containsKey("audio.samplesize.bits")) {
bitspersample = ((Integer) properties.get("audio.samplesize.bits")).intValue();
}
if (properties.containsKey("audio.channels")) {
channels = ((Integer) properties.get("audio.channels")).intValue();
}
if (properties.containsKey("audio.samplerate.hz")) {
samplerate = ((Float) properties.get("audio.samplerate.hz")).floatValue();
}
if (properties.containsKey("audio.framesize.bytes")) {
framesize = ((Integer) properties.get("audio.framesize.bytes")).intValue();
}
if (bitspersample > 0) {
milliseconds = (long) (1000.0f * byteslength / (samplerate * channels * (bitspersample / 8.0F)));
} else {
milliseconds = (long) (1000.0f * byteslength / (samplerate * framesize));
}
}
}
return milliseconds * 1000;
}
/**
* Sets Gain value.
* @param gain a value bitween -1.0 and +1.0
* @throws PlayerException
*/
public void setGain(float gain) throws PlayerException {
if (gainControl != null) {
double minGain = gainControl.getMinimum();
double maxGain = gainControl.getMaximum();
double ampGain = ((10.0f / 20.0f) * maxGain) - minGain;
double cste = Math.log(10.0) / 20;
double value = minGain + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGain) - 1) * gain);
gainControl.setValue((float) value);
logger.info("{}", gainControl.toString());
} else {
throw new PlayerException("Gain control not supported");
}
}
public float getGain() {
float gain = 0.0f;
if (gainControl != null) {
gain = gainControl.getValue();
}
return gain;
}
/**
* Sets Pan value.
* @param pan a value bitween -1.0 and +1.0
* @throws PlayerException
*/
public void setPan(float pan) throws PlayerException {
if (panControl != null) {
panControl.setValue(pan);
logger.info("{}", panControl.toString());
} else {
throw new PlayerException("Pan control not supported");
}
}
public float getPan() {
float pan = 0.0f;
if (panControl != null) {
pan = panControl.getValue();
}
return pan;
}
/**
* Sets Mute value.
* @param mute a boolean value
* @throws PlayerException
*/
public void setMuted(boolean mute) throws PlayerException {
if (muteControl != null) {
muteControl.setValue(mute);
logger.info("{}", muteControl.toString());
} else {
throw new PlayerException("Mute control not supported");
}
}
public boolean isMuted() {
boolean muted = false;
if (muteControl != null) {
muted = muteControl.getValue();
}
return muted;
}
public long getPosition() {
long pos = 0;
if (sourceDataLine != null) {
pos = sourceDataLine.getMicrosecondPosition();
}
return pos;
}
public int getState() {
return state;
}
public DigitalSignalSynchronizer getDSS() {
return dss;
}
@Override
public Void call() throws PlayerException {
logger.info("Decoding thread started");
int nBytesRead = 0;
int audioDataLength = READ_BUFFER_SIZE;
ByteBuffer audioDataBuffer = ByteBuffer.allocate(audioDataLength);
audioDataBuffer.order(ByteOrder.LITTLE_ENDIAN);
lock.lock();
try {
while ((nBytesRead != -1) && (state != STOP) && (state != SEEK) && (state != AudioSystem.NOT_SPECIFIED)) {
try {
if (state == PLAY) {
int toRead = audioDataLength;
int totalRead = 0;
while (toRead > 0 && (nBytesRead = audioInputStream.read(audioDataBuffer.array(), totalRead, toRead)) != -1) {
totalRead += nBytesRead;
toRead -= nBytesRead;
}
if (totalRead > 0) {
byte[] trimBuffer = audioDataBuffer.array();
if (totalRead < trimBuffer.length) {
trimBuffer = new byte[totalRead];
System.arraycopy(audioDataBuffer.array(), 0, trimBuffer, 0, totalRead);
}
sourceDataLine.write(trimBuffer, 0, totalRead);
dss.writeAudioData(trimBuffer, 0, totalRead);
for (PlaybackListener pl : listeners) {
PlaybackEvent pe = new PlaybackEvent(this, Playback.PLAYING, getPosition() - oldPosition, emptyMap);
if (audioInputStream instanceof PropertiesContainer) {
// Pass audio parameters such as instant bitrate, ...
pe.setProperties(((PropertiesContainer) audioInputStream).properties());
}
pl.playbackProgress(pe);
}
}
} else if (state == INIT || state == PAUSE) {
if (sourceDataLine != null && sourceDataLine.isRunning()) {
sourceDataLine.flush();
sourceDataLine.stop();
}
pauseCondition.awaitUninterruptibly();
}
} catch (IOException ex) {
logger.error("Decoder Exception: ", ex);
state = STOP;
notifyEvent(Playback.STOPPED);
throw new PlayerException(ex);
}
}
if (sourceDataLine != null) {
sourceDataLine.flush();
sourceDataLine.stop();
sourceDataLine.close();
sourceDataLine = null;
}
closeStream();
if (nBytesRead == -1) {
notifyEvent(Playback.EOM);
}
} finally {
lock.unlock();
}
logger.info("Decoding thread completed");
return null;
}
private void awaitTermination() {
if (future != null && !future.isDone()) {
try {
future.get();
} catch (InterruptedException ex) {
logger.error(ex.getMessage(), ex);
} catch (ExecutionException ex) {
logger.error(ex.getMessage(), ex);
} finally {
// Harmless if task already completed
future.cancel(true); // interrupt if running
}
}
}
public void play() throws PlayerException {
lock.lock();
try {
switch (state) {
case STOP:
initAudioInputStream();
initSourceDataLine();
default:
if (sourceDataLine != null && !sourceDataLine.isRunning()) {
sourceDataLine.start();
}
state = PLAY;
pauseCondition.signal();
notifyEvent(Playback.PLAYING);
break;
}
} finally {
lock.unlock();
}
}
public void pause() {
if (sourceDataLine != null) {
if (state == PLAY) {
state = PAUSE;
notifyEvent(Playback.PAUSED);
}
}
}
public void stop() {
if (state != STOP) {
int oldState = state;
state = STOP;
if (oldState == INIT || oldState == PAUSE) {
lock.lock();
try {
pauseCondition.signal();
} finally {
lock.unlock();
}
}
awaitTermination();
notifyEvent(Playback.STOPPED);
}
}
public long seek(long bytes) throws PlayerException {
long totalSkipped = 0;
if (audioSource instanceof File) {
int bytesLength = getByteLength();
if ((bytesLength <= 0) || (bytes >= bytesLength)) {
notifyEvent(Playback.EOM);
return totalSkipped;
}
logger.info("Bytes to skip: {}", bytes);
oldPosition = getPosition();
int oldState = state;
if (state == PLAY) {
state = PAUSE;
}
lock.lock();
try {
notifyEvent(Playback.SEEKING);
initAudioInputStream();
if (audioInputStream != null) {
// long skipped = 0;
// while (totalSkipped < bytes) {
// skipped = audioInputStream.skip(bytes - totalSkipped);
// totalSkipped += skipped;
// if (skipped == 0) {
// break;
// }
// }
totalSkipped = audioInputStream.skip(bytes);
logger.info("Skipped bytes: {}/{}", totalSkipped, bytes);
if (totalSkipped == -1) {
throw new PlayerException("Seek not supported");
}
initSourceDataLine();
}
} catch (IOException ex) {
throw new PlayerException(ex);
} finally {
lock.unlock();
}
if (oldState == PLAY) {
play();
}
}
return totalSkipped;
}
protected void closeStream() {
if (audioInputStream != null) {
try {
audioInputStream.close();
audioInputStream = null;
logger.info("Stream closed");
} catch (IOException ex) {
logger.error("Cannot close stream", ex);
}
}
}
}