/**
*
*/
package com.soundlooper.audio.player;
import java.io.File;
import java.util.HashMap;
import java.util.function.Consumer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jouvieje.fmodex.FmodEx;
import org.jouvieje.fmodex.System;
import org.jouvieje.fmodex.enumerations.FMOD_RESULT;
import com.soundlooper.exception.PlayerException;
import com.soundlooper.exception.PlayerNotInitializedException;
import com.soundlooper.model.SoundLooperPlayer;
import com.soundlooper.system.util.MessagingUtil;
/**
* AudioEngine is an audio engine based on FMOD Copyright (C) 2014 Alexandre
* NEDJARI
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*
* @author ANEDJARI
*
*/
public class Player {
/**
* The current state of the player
*/
private IntegerProperty state = new SimpleIntegerProperty(PlayerState.STATE_PLAYER_UNINITIALIZED);
/**
* Save the current volume to apply it to the new songs
*/
private IntegerProperty volume = new SimpleIntegerProperty(100);
private BooleanProperty mute = new SimpleBooleanProperty(false);
/**
* Save the current timestretsh to apply it to the new songs
*/
private IntegerProperty timeStretch = new SimpleIntegerProperty(100);
/**
* The FMOD system
*/
private static System system = new System();
/**
* The current player (corresponding to the current song)
*/
private SoundFile sound = null;
/**
* The state
*/
private PlayerState playerState = new PlayerState();
/**
* True if there is an error on a state change
*/
private boolean changeStateError;
/**
* Logger for this class
*/
private static Logger LOGGER = LogManager.getLogger(Player.class);
/**
* Udes to generate song representation
*/
private ThreadImageGenerator threadGenerationImage;
protected static final int MINIMAL_MS_LOOP = 100;
/**
* Private constructor
*/
protected Player() {
volume.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
try {
applyVolume(newValue.intValue());
mute.set(false);
} catch (PlayerException e) {
MessagingUtil.displayError("Impossible de modifier le volume", e);
}
}
});
timeStretch.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
try {
applyTimeStretch(oldValue.intValue(), newValue.intValue());
} catch (PlayerException e) {
MessagingUtil.displayError("Impossible de modifier le timestrech", e);
}
}
});
mute.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
try {
if (newValue) {
applyVolume(0);
} else {
applyVolume(volume.intValue());
}
} catch (PlayerException e) {
MessagingUtil.displayError("Impossible de modifier le volume", e);
}
}
});
}
/**
* Set the state value
*
* @param newState
* the new state value
*/
public void setState(int newState) {
this.state.set(newState);
}
/**
* get the current state of the player
*
* @return the current state of the player
*/
public int getState() {
return this.state.get();
}
public IntegerProperty stateProperty() {
return state;
}
/**
* Initialize the player
*
* @throws PlayerException
*/
public void initialize() throws PlayerException {
new PlayerActionInit(this).run();
}
/**
* Load a song in the player
*
* @param fileToLoad
* the file to load
* @throws PlayerException
*/
protected void loadSong(File fileToLoad) throws PlayerException {
new PlayerActionLoad(this, fileToLoad).run();
}
/**
* Play the song
*
* @throws PlayerException
*/
public void play() throws PlayerException {
new PlayerActionStart(this).run();
}
/**
* Stop the play and set time to zero
*
* @throws PlayerException
*/
public void stop() throws PlayerException {
new PlayerActionStop(this).run();
}
/**
* Pause the song
*
* @throws PlayerException
*/
public void pause() throws PlayerException {
new PlayerActionPause(this).run();
}
/**
* Deallocate the song
*
* @throws PlayerException
*/
public void desallocate() throws PlayerException {
new PlayerActionDesallocateSong(this).run();
}
/**
* Check that the sound is initialized
*
* @throws PlayerException
* if the sound is not initialized (= null)
*/
void checkCurrentSoundInitialized() throws PlayerNotInitializedException {
if (!this.isSoundInitialized()) {
throw new PlayerNotInitializedException("Le lecteur n'est pas initialis�");
}
}
/**
* Check that the system is initialized
*
* @throws PlayerException
* if the system is not initialized (= null)
*/
void checkSystemInitialized() throws PlayerException {
if (!this.isSystemInitialized()) {
throw new PlayerException("Le syst�me n'est pas initialis�");
}
}
/**
* Check if the system is initialized
*
* @return true if the system is initialized or initializing
*/
public boolean isSystemInitialized() {
if (this.getState() == PlayerState.STATE_PLAYER_UNINITIALIZED) {
return false;
}
return true;
}
/**
* Check that the sound is initialized
*
* @return true if the sound is initialized
*/
public boolean isSoundInitialized() {
if (this.sound == null) {
LOGGER.info("Sound is not initialized");
return false;
}
return true;
}
/**
* get the current sound
*
* @return the sound
* @throws PlayerException
* if the sound is not initialized
*/
public SoundFile getCurrentSound() throws PlayerNotInitializedException {
this.checkCurrentSoundInitialized();
return this.sound;
}
/**
* Return the FMod system
*
* @return the FMod system
*/
public static System getSystem() {
return Player.system;
}
/**
* set the current sound
*
* @param newSound
* the new sound
*/
void setCurrentSound(SoundFile newSound) {
this.sound = newSound;
}
/**
* Change the stateError
*
* @param newChangeStateError
* the new StateError value
*/
public void setChangeStateError(boolean newChangeStateError) {
this.changeStateError = newChangeStateError;
}
/**
* Get the changeStateError value
*
* @return the changeStateValue
*/
public boolean isChangeStateError() {
return this.changeStateError;
}
/**
* Check if the result is corresponding to an error
*
* @param result
* the result to check
* @throws PlayerException
* if the result is an error
*/
public static void errorCheck(FMOD_RESULT result) throws PlayerException {
if (result != FMOD_RESULT.FMOD_OK) {
LOGGER.info("JNI : Searching string error corresponding to " + result.asInt());
throw new PlayerException("FMOD error! (" + result.asInt() + ") " + FmodEx.FMOD_ErrorString(result));
}
}
/**
* get the state label from state
*
* @param state
* the state
* @return the state label
*/
public String getStateLabel(int state) {
return this.playerState.getStateLabel(state);
}
// TODO supprimer cette sous classe
/**
* The state of the player
*
* @author ANEDJARI
*/
public class PlayerState {
/**
* The state for unitialized players (no song loaded)
*/
public static final int STATE_PLAYER_UNINITIALIZED = 0;
/**
* The state used during the player initialization
*/
public static final int STATE_PLAYER_INITIALIZING = 1;
/**
* No song loaded, but the player is ready
*/
public static final int STATE_PLAYER_INITIALIZED = 2;
/**
* State when the song is loading
*/
public static final int STATE_LOADING_SONG = 3;
/**
* State when the song is loaded
*/
// public static final int STATE_SONG_LOADED = 4;
/**
* State when preparing play
*/
public static final int STATE_PREPARING_PLAY = 5;
/**
* State when playing
*/
public static final int STATE_PLAYING = 6;
/**
* State when preparing pause
*/
public static final int STATE_PREPARING_PAUSE = 7;
/**
* State when it's paused
*/
public static final int STATE_PAUSED = 8;
/**
* State when preparing stop
*/
public static final int STATE_PREPARING_STOP = 9;
/**
* State when it's paused
*/
public static final int STATE_STOPPED = 10;
/**
* The state when unload current song
*/
public static final int STATE_UNLOAD_SONG = 11;
/**
* The state when unload the player
*/
public static final int STATE_UNLOAD_PLAYER = 12;
/**
* The label map for states
*/
private HashMap<Integer, String> stateLabel = new HashMap<Integer, String>();
/**
* Constructor
*/
public PlayerState() {
super();
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PLAYER_UNINITIALIZED), "STATE_PLAYER_UNINITIALIZED");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PLAYER_INITIALIZING), "STATE_PLAYER_INITIALIZING");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PLAYER_INITIALIZED), "STATE_PLAYER_INITIALIZED");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_LOADING_SONG), "STATE_LOADING_SONG");
// this.stateLabel.put(Integer.valueOf(PlayerState.STATE_SONG_LOADED),
// "STATE_SONG_LOADED");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PREPARING_PLAY), "STATE_PREPARING_PLAY");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PLAYING), "STATE_PLAYING");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PREPARING_PAUSE), "STATE_PREPARING_PAUSE");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PAUSED), "STATE_PAUSED");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_PREPARING_STOP), "STATE_PREPARING_STOP");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_STOPPED), "STATE_STOPPED");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_UNLOAD_SONG), "STATE_UNLOAD_SONG");
this.stateLabel.put(Integer.valueOf(PlayerState.STATE_UNLOAD_PLAYER), "STATE_UNLOAD_PLAYER");
}
/**
* get the label of a state
*
* @param stateToFind
* the state
* @return the label of a state
*/
public String getStateLabel(int stateToFind) {
String label = this.stateLabel.get(Integer.valueOf(stateToFind));
if (label == null) {
label = "UNKNOW STATE";
}
return label;
}
}
/**
* Set the media time
*
* @param beginMsValue
* the bebin MS value
* @throws PlayerException
*/
public void setMediaTime(int beginMsValue) throws PlayerException {
new PlayerActionSetPosition(this, beginMsValue).run();
}
public void moveMediaTime(int millisecondTime) throws PlayerException {
if (!isSoundInitialized()) {
return;
}
int newMediaTime = getMediaTime() + millisecondTime;
new PlayerActionSetPosition(this, newMediaTime).run();
}
/**
* Get the media time
*
* @return the media time
* @throws PlayerException
*/
public int getMediaTime() {
if (!this.isSoundInitialized()) {
return 0;
}
try {
return this.getCurrentSound().getMediaTime();
} catch (PlayerException e) {
LOGGER.error(e);
return 0;
}
}
/**
* Generate image for current song in a different thread When the image is
* generated, listeners are notified This task is not added to the queue for
* allow others player task to be excuted during the generation
*/
public void generateImage(Consumer<File> onSuccessConsumer) {
if (this.threadGenerationImage != null && this.threadGenerationImage.isAlive()) {
this.threadGenerationImage.interrupt();
}
try {
this.threadGenerationImage = new ThreadImageGenerator(this.getCurrentSound(), onSuccessConsumer);
this.threadGenerationImage.start();
} catch (PlayerException e) {
MessagingUtil.displayError("Impossible de g�n�rer l'image audio", e);
}
}
// /////////////////VOLUME MANAGEMENT////////////////////
public IntegerProperty volumeProperty() {
return volume;
}
public int getVolume() {
return volume.get();
}
public void setVolume(int percent) {
volume.set(percent);
}
/**
* Set the volume
*
* @param percent
* the volume in percent
*/
private void applyVolume(int percent) throws PlayerException {
LOGGER.info("Set the volume percent : " + percent);
if (this.isSoundInitialized()) {
this.sound.setVolume(new Float(percent / 100.0).floatValue());
// volume.set(percent);
}
}
public void incrementVolume(int percent) {
int newVolume = getVolume() + percent;
if (newVolume > 100) {
newVolume = 100;
}
if (newVolume < 0) {
newVolume = 0;
}
setVolume(newVolume);
}
// /////////////////END VOLUME MANAGEMENT////////////////////
// /////////////////TIMESTRETCH MANAGEMENT////////////////////
public IntegerProperty timeStretchProperty() {
return timeStretch;
}
public int getTimeStretch() {
return timeStretch.get();
}
public void setTimeStretch(int percent) {
timeStretch.set(percent);
}
public void incrementTimeStretch(int percent) {
int newPercent = getTimeStretch() + percent;
if (newPercent > 200) {
newPercent = 200;
}
if (newPercent < 50) {
newPercent = 50;
}
setTimeStretch(newPercent);
}
private void applyTimeStretch(int oldPercent, int newPercent) throws PlayerException {
if (oldPercent == newPercent) {
return;
}
if (newPercent > 200) {
setTimeStretch(200);
return;
}
if (newPercent < 50) {
setTimeStretch(50);
return;
}
LOGGER.info("Update timestrech from " + oldPercent + " to " + newPercent);
if (this.isSoundInitialized()) {
new PlayerActionApplyTimestretch(this, newPercent).run();
}
}
/**
* Set the loop points of the song
*
* @param beginTime
* the begin time in milliseconds
* @param endTime
* the end time in milliseconds
* @throws PlayerException
*/
protected void applyLoopPoints(final int beginTime, final int endTime) throws PlayerException {
PlayerActionApplyLoopPoint action = new PlayerActionApplyLoopPoint(SoundLooperPlayer.getInstance(), beginTime,
endTime);
action.run();
}
public PlayerState getPlayerState() {
return playerState;
}
public BooleanProperty getMute() {
return mute;
}
public void setMute(BooleanProperty mute) {
this.mute = mute;
}
public BooleanProperty muteProperty() {
return mute;
}
// /////////////////END LOOP POINTS MANAGEMENT////////////////////
}