/*
* Jajuk
* Copyright (C) The Jajuk Team
* http://jajuk.info
*
* 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
package org.jajuk.services.players;
import java.io.File;
import java.util.Locale;
import java.util.Map;
import javazoom.jlgui.basicplayer.BasicController;
import javazoom.jlgui.basicplayer.BasicPlayer;
import javazoom.jlgui.basicplayer.BasicPlayerEvent;
import javazoom.jlgui.basicplayer.BasicPlayerListener;
import org.jajuk.base.Track;
import org.jajuk.base.TrackManager;
import org.jajuk.base.Type;
import org.jajuk.base.TypeManager;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.webradio.WebRadio;
import org.jajuk.util.Conf;
import org.jajuk.util.Const;
import org.jajuk.util.Messages;
import org.jajuk.util.UtilFeatures;
import org.jajuk.util.UtilSystem;
import org.jajuk.util.log.Log;
/**
* Jajuk player implementation based on javazoom BasicPlayer.
*/
public class JavaLayerPlayerImpl implements IPlayerImpl, Const, BasicPlayerListener {
/** The Constant AUDIO_LENGTH_BYTES. */
private static final String AUDIO_LENGTH_BYTES = "audio.length.bytes";
/** Current player. */
private BasicPlayer player;
/** Time elapsed in ms. */
private long lTime = 0;
/** Actually played time */
private long actuallyPlayedTimeMillis = 0l;
private long lastPlayTimeUpdate = System.currentTimeMillis();
/** Date of last elapsed time update. */
private long lDateLastUpdate = System.currentTimeMillis();
/** current track info. */
private Map<String, Object> mPlayingData;
/** Current position in %. */
private float fPos;
/** Length to be played in secs. */
private long length;
/** Stored Volume. */
private float fVolume;
/** Current track estimated duration in ms. */
private long lDuration;
/** Cross fade duration in ms. */
int iFadeDuration = 0;
/** Fading state. */
boolean bFading = false;
/** Progress step in ms, do not set less than 300 or 400 to avoid using too much CPU. */
private static final int PROGRESS_STEP = 500;
/** Total play time is refreshed every TOTAL_PLAYTIME_UPDATE_INTERVAL times. */
private static final int TOTAL_PLAYTIME_UPDATE_INTERVAL = 2;
/** Volume when starting fade. */
private float fadingVolume;
/** current file. */
private org.jajuk.base.File fCurrent;
/** Inc rating flag. */
private boolean bHasBeenRated = false;
/** Used to compute total played time. */
private int comp = 0;
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#play(org.jajuk.base.File, float, long,
* float)
*/
@Override
public void play(org.jajuk.base.File file, float fPosition, long length, float fVolume)
throws Exception {
this.fPos = 0;
this.lTime = 0;
this.mPlayingData = null;
this.fVolume = fVolume;
this.length = length;
this.bFading = false;
this.fCurrent = file;
this.bHasBeenRated = false;
// Instantiate player is needed
if (player == null) {
BasicPlayer.EXTERNAL_BUFFER_SIZE = Conf.getInt(Const.CONF_BUFFER_SIZE);
player = new BasicPlayer();
player.setLineBufferSize(Conf.getInt(Const.CONF_AUDIO_BUFFER_SIZE));
player.addBasicPlayerListener(this); // set listener
}
// make sure to stop any current player
if (player.getStatus() != BasicPlayer.STOPPED) {
player.stop();
}
player.open(new File(file.getAbsolutePath()));
if ((fPosition > 0.0f) &&
// (position*fPosition(%))*1000(ms) /24 because 1 frame =24ms
// test if this is a audio format supporting seeking
// Note: fio.getName() is better here as it will do less and not create
// java.io.File in File
(TypeManager.getInstance().getTypeByExtension(UtilSystem.getExtension(file.getName()))
.getBooleanValue(Const.XML_TYPE_SEEK_SUPPORTED))) {
seek(fPosition);
}
player.play();
setVolume(fVolume);
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.IPlayerImpl#stop()
*/
@Override
public void stop() throws Exception {
bFading = false;
if (player != null) {
player.stop();
}
// Update track rate
fCurrent.getTrack().updateRate();
// Force immediate rating refresh (without using the rating manager)
ObservationManager.notify(new JajukEvent(JajukEvents.RATE_CHANGED));
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.IPlayerImpl#setVolume(float)
*/
@Override
public void setVolume(float fVolume) throws Exception {
this.fVolume = fVolume;
if (player.hasGainControl()) {
player.setGain(fVolume * 0.6);
} else {
Log.warn("Gain control not supported");
}
// limit gain to avoid saturation
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#getCurrentPosition()
*/
@Override
public float getCurrentPosition() {
return fPos;
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#getCurrentVolume()
*/
@Override
public float getCurrentVolume() {
return fVolume;
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#getElapsedTimeMillis()
*/
@Override
public long getElapsedTimeMillis() {
return lTime;
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#pause()
*/
@Override
public void pause() throws Exception {
player.pause();
}
/* (non-Javadoc)
* @see org.jajuk.services.players.IPlayerImpl#resume()
*/
@Override
public void resume() throws Exception {
player.resume();
lastPlayTimeUpdate = System.currentTimeMillis();
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#seek(float) Ogg vorbis seek not yet
* supported
*/
@Override
public void seek(float pPosValue) {
float posValue = pPosValue;
// if fading, ignore
if (bFading) {
return;
}
// Do not seek to a position too near from the end : it can cause
// freeze. MAX=98%
if (posValue > 0.98f) {
posValue = 0.98f;
}
// leave if already seeking
if (player != null && getState() == BasicPlayer.SEEKING) {
Log.warn("Already seeking, leaving");
return;
}
if (mPlayingData.containsKey("audio.type") && player != null) {
String audioType = (String) mPlayingData.get("audio.type");
audioType = audioType.toLowerCase(Locale.getDefault());
Type type = TypeManager.getInstance().getTypeByExtension(audioType);
// Seek support for MP3. and WAVE
if (type != null && type.getBooleanValue(Const.XML_TYPE_SEEK_SUPPORTED)
&& mPlayingData.containsKey(AUDIO_LENGTH_BYTES)) {
int iAudioLength = ((Integer) mPlayingData.get(AUDIO_LENGTH_BYTES)).intValue();
long skipBytes = Math.round(iAudioLength * posValue);
try {
player.seek(skipBytes);
setVolume(fVolume); // need this because a seek reset volume
} catch (Exception e) {
Log.error(e);
return;
}
} else {
Messages.showErrorMessage(126);
return;
}
}
}
/**
* Gets the state.
*
* @return player state, -1 if player is null.
*/
@Override
public int getState() {
if (bFading) {
return FADING_STATUS;
} else if (player != null) {
return player.getStatus();
} else {
return -1;
}
}
/**
* Opened listener implementation.
*
* @param arg0
* @param arg1
*/
@Override
@SuppressWarnings("unchecked")
public void opened(Object arg0, @SuppressWarnings("rawtypes")
Map arg1) {
this.mPlayingData = arg1;
this.lDuration = UtilFeatures.getTimeLengthEstimation(mPlayingData);
lastPlayTimeUpdate = System.currentTimeMillis();
}
/**
* Progress listener implementation. Called several times by sec
*
* @param iBytesread
* @param lMicroseconds
* @param bPcmdata
* @param mProperties
*/
@Override
public void progress(int iBytesread, long lMicroseconds, byte[] bPcmdata,
@SuppressWarnings("rawtypes")
Map mProperties) {
if ((System.currentTimeMillis() - lDateLastUpdate) > PROGRESS_STEP) {
lDateLastUpdate = System.currentTimeMillis();
this.iFadeDuration = 1000 * Conf.getInt(Const.CONF_FADE_DURATION);
if (bFading) {
// computes the volume we have to sub to reach zero at last
// progress()
float fVolumeStep = fadingVolume * ((float) 500 / iFadeDuration);
// divide step by two to make fade softer
float fNewVolume = fVolume - (fVolumeStep / 2);
// decrease volume by n% of initial volume
if (fNewVolume < 0) {
fNewVolume = 0;
}
try {
setVolume(fNewVolume);
} catch (Exception e) {
Log.error(e);
}
return;
}
// Update total played time
if (comp > 0 && comp % TOTAL_PLAYTIME_UPDATE_INTERVAL == 0) {
// Increase actual play time
// End of file: increase actual play time to the track
// Perf note : this full action takes less much than 1 ms
long trackPlaytime = fCurrent.getTrack().getLongValue(Const.XML_TRACK_TOTAL_PLAYTIME);
trackPlaytime += ((PROGRESS_STEP * TOTAL_PLAYTIME_UPDATE_INTERVAL) / 1000);
fCurrent.getTrack().setProperty(Const.XML_TRACK_TOTAL_PLAYTIME, trackPlaytime);
}
comp++;
// computes read time
if (mPlayingData.containsKey(AUDIO_LENGTH_BYTES)) {
int byteslength = ((Integer) mPlayingData.get(AUDIO_LENGTH_BYTES)).intValue();
fPos = (byteslength != 0) ? (float) iBytesread / (float) byteslength : 0;
UtilFeatures.storePersistedPlayingPosition(fPos);
lTime = (long) (lDuration * fPos);
// update actually played duration
if (lastPlayTimeUpdate > 0 && player.getStatus() != BasicPlayer.PAUSED) {
actuallyPlayedTimeMillis += (System.currentTimeMillis() - lastPlayTimeUpdate);
}
lastPlayTimeUpdate = System.currentTimeMillis();
}
// check if the track get rate increasing level (INC_RATE_TIME
// secs or intro length)
if (!bHasBeenRated
&& (lTime >= INC_RATE_TIME * 1000 || (length != TO_THE_END && lTime > length))) {
// inc rate by 1 if file is played at least INC_RATE_TIME secs
TrackManager.getInstance().changeTrackRate(fCurrent.getTrack(),
fCurrent.getTrack().getRate() + 1);
}
// Cross-Fade test
if (iFadeDuration > 0 && lTime > (lDuration - iFadeDuration)) {
// if memory is low, we force full gc to avoid blanck during
// fade
if (UtilSystem.needFullFC()) {
Log.debug("Need full gc, no cross fade");
} else {
bFading = true;
this.fadingVolume = fVolume;
// we have to launch the next file from another thread to
// avoid stopping current track (perceptible during
// player.open() for remote files)
new Thread("Fade Next File Thread") {
@Override
public void run() {
QueueModel.finished();
// Update track rate
fCurrent.getTrack().updateRate();
// Force immediate rating refresh (without using the rating manager)
ObservationManager.notify(new JajukEvent(JajukEvents.RATE_CHANGED));
}
}.start();
}
}
// Caution: lMicroseconds reset to zero after a seek
// test end of length for intro mode
else if (length != TO_THE_END && lTime > length) {
// length=-1 means there is no max length
new Thread("Player Progress Thread") {
@Override
public void run() {
QueueModel.finished();
fCurrent.getTrack().updateRate();
// Force immediate rating refresh (without using the rating manager)
ObservationManager.notify(new JajukEvent(JajukEvents.RATE_CHANGED));
}
}.start();
}
}
}
/**
* State updated implementation.
*
* @param bpe
*/
@Override
public void stateUpdated(BasicPlayerEvent bpe) {
if (bpe.getCode() != 10) { // do not trace volume changes
Log.debug("Player state changed: " + bpe);
}
switch (bpe.getCode()) {
case BasicPlayerEvent.EOM:
// inc rate by 1 if file is fully played
Track track = fCurrent.getTrack();
TrackManager.getInstance().changeTrackRate(track, track.getRate() + 1);
if (!bFading) { // if using crossfade, ignore end of file
System.gc();// Benefit from end of file to perform a full gc
QueueModel.finished();
}
bFading = false;
break;
case BasicPlayerEvent.STOPPED:
break;
case BasicPlayerEvent.PLAYING:
break;
}
}
/**
* Set controler implementation.
*
* @param arg0
*/
@Override
public void setController(BasicController arg0) {
// nothing to do here
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#getCurrentLength()
*/
@Override
public long getDurationSec() {
return lDuration;
}
/**
* Scrobble.
*
*
* @return the int
*/
public int scrobble() {
return 1;
}
/*
* (non-Javadoc)
*
* @see org.jajuk.players.IPlayerImpl#play(org.jajuk.base.WebRadio, float)
*/
@Override
public void play(WebRadio radio, float fVolume) throws Exception {
// not needed right now
}
/* (non-Javadoc)
* @see org.jajuk.services.players.IPlayerImpl#getActuallyPlayedTimeMillis()
*/
@Override
public long getActuallyPlayedTimeMillis() {
return actuallyPlayedTimeMillis;
}
}