package net.sf.jiga.xtended.impl;
import java.applet.Applet;
import java.applet.AudioClip;
import java.io.*;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.Player;
import net.sf.jiga.xtended.JXAException;
import net.sf.jiga.xtended.impl.game.RenderingScene;
import net.sf.jiga.xtended.impl.game.RenderingSceneComponent;
import net.sf.jiga.xtended.impl.game.gl.GLHandler;
import net.sf.jiga.xtended.impl.game.gl.MemScratch;
import net.sf.jiga.xtended.impl.game.gl.RenderingSceneGL;
import net.sf.jiga.xtended.impl.game.gl.SndScratch;
import net.sf.jiga.xtended.impl.system.BufferIO;
import net.sf.jiga.xtended.kernel.*;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.OpenALException;
import org.lwjgl.openal.Util;
import org.lwjgl.util.WaveData;
/**
* The SoundInput class implements the Sound interface for playing sounds of
* many supported formats. The MPEG-Layer 3 can be used to decode mp3 encoded
* sound files. The whole class is thread-safe. More information about JLayer,
* the Java MPEG-Layer 3 codec can be found at <a
* href="http://www.javazoom.net/javalayer/javalayer.html"/> JavaZoom.net</a>.
* OpenAL Soft is also supported. To change the sound-config, browse to the
* net/sf/jiga/impl/game/rendering.properties file !
*
* @author www.b23prodtm.info
*/
public class SoundInput implements Sound, Resource, RenderingSceneComponent {
/** link to all of the openAL scratch (only if openAL is enabled)*/
public final static SndScratch sTex = GLHandler.sSnd;
/**
* global mute sound
*
* @default false
*/
public static boolean _mute = false;
/**
* hash-code
*/
long hash = System.nanoTime();
/**
* returns the unique hash-code for this instance
*
* @return the unique hash-code for this instance
*/
@Override
public int hashCode() {
return (int) hash;
}
/**
* returns true or false, whether the specified Object is equal or not to
* this instance, resp.
*
* @param o the Object to compare this instance to
* @return true or false, whether this instance is equal or not to the
* specfied Object
* @see #hashCode()
*/
@Override
public boolean equals(Object o) {
return o == null ? false : o.hashCode() == hashCode();
}
/*
* Position of the sources sound.
*/
FloatBuffer sourcePos = BufferIO._wrapf(new float[]{0.0f, 0.0f, 0.0f});
/*
* Velocity of the sources sound.
*/
FloatBuffer sourceVel = BufferIO._wrapf(new float[]{0.0f, 0.0f, 0.0f});
/*
* Position of the listener.
*/
FloatBuffer listenerPos = BufferIO._wrapf(new float[]{0.0f, 0.0f, 0.0f});
/*
* Velocity of the listener.
*/
FloatBuffer listenerVel = BufferIO._wrapf(new float[]{0.0f, 0.0f, 0.0f});
/*
* Orientation of the listener. (first 3 elements are "at", second 3 are "up")
*/
FloatBuffer listenerOri = BufferIO._wrapf(new float[]{0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f});
/**
* this is the velocity to add to the AL source
*
* @return the velocity added to the AL source 3D-location
*/
public FloatBuffer getSourceVel() {
return sourceVel;
}
/**
* the position of the source
*
* @return the 3D position of the source
*/
public FloatBuffer getSourcePos() {
return sourcePos;
}
/**
* the listener's velocity
*
* @return the listener's velocity to add to the listener 3D location
*/
public FloatBuffer getListenerVel() {
return listenerVel;
}
/**
* the listener's position
*
* @return the listener's 3D-location
*/
public FloatBuffer getListenerPos() {
return listenerPos;
}
/**
* the listener orientation
*
* @return the listener's 3D orientation vector
*/
public FloatBuffer getListenerOri() {
return listenerOri;
}
/**
* sets up the listener's velocity
*
* @param listenerVel a float array with a 3D vector for the velocity
*/
public void setListenerVel(FloatBuffer listenerVel) {
this.listenerVel = listenerVel;
}
/**
* sets up the listener's position
*
* @param listenerPos a float array with a 3D point for the location
*/
public void setListenerPos(FloatBuffer listenerPos) {
this.listenerPos = listenerPos;
}
/**
* sets up the source velocity
*
* @param sourceVel a float array with the 3D vector for the velocity
*/
public void setSourceVel(FloatBuffer sourceVel) {
this.sourceVel = sourceVel;
}
/**
* sets up the listener orientation
*
* @param listenerOri a float array with a 3D vector for the orientation
*/
public void setListenerOri(FloatBuffer listenerOri) {
this.listenerOri = listenerOri;
}
/**
* sets up the source position
*
* @param sourcePos a float array with a 3D vector for the source location
*/
public void setSourcePos(FloatBuffer sourcePos) {
this.sourcePos = sourcePos;
}
/**
* is set true or false to enable or disable the openAL library (3D surround
* sound fx) in the {@linkplain RenderingScene#rb ResourceBundle}
*/
public static boolean openALEnabled = Boolean.parseBoolean(RenderingScene.rb.getString("openALEnabled"));
static {
if (openALEnabled) {
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (AL.isCreated()) {
_killAL();
}
}
}));
return null;
}
});
}
}
final static BitStack bits = new BitStack();
final static int SND_INTERFACE_BIT = bits._newBitRange();
final static int SND_INTERFACE_APPLET = bits._newBit(SND_INTERFACE_BIT);
final static int SND_INTERFACE_JLAYER = bits._newBit(SND_INTERFACE_BIT);
final static int SND_INTERFACE_OPENAL = bits._newBit(SND_INTERFACE_BIT);
final static int SND_STATE_BIT = bits._newBitRange();
final static int SND_STATE_STOPPED = bits._newBit(SND_STATE_BIT);
final static int SND_STATE_PLAYING = bits._newBit(SND_STATE_BIT);
final static int SND_STATE_CLEARED = bits._newBit(SND_STATE_BIT);
final static int SND_EXT_BIT = bits._newBitRange();
final static int SND_EXT_LOOP = bits._newBit(SND_EXT_BIT);
final static int SND_EXT_INRSRC = bits._newBit(SND_EXT_BIT);
final static int SND_EXT_MUTE = bits._newBit(SND_EXT_BIT);
private int soundState = SND_STATE_CLEARED | (openALEnabled ? SND_INTERFACE_OPENAL : SND_INTERFACE_APPLET) | SND_EXT_INRSRC;
/**
* the current AudioClip instance
*/
AudioClip bgSfx = null;
/**
* the current JLayer player instance
*/
Player player = null;
/**
* the current file resource name
*/
String fileRsrc = null;
/**
* returns true or false, whether this intance is playing a sound or not,
* resp.
*
* @return true or false, whether this instance is playing a sound or not,
* resp.
*/
public boolean isPlaying() {
return (soundState & bits._getMask(SND_STATE_BIT) & SND_STATE_PLAYING) != 0;
}
/**
* dis/enables the JLayer
*
* @param b dis/enables the JLayer
*/
public void setUseJLayerEnabled(boolean b) {
if (isUseJLayerEnabled() != b) {
clearResource();
}
switchState(SND_INTERFACE_BIT, b ? SND_INTERFACE_JLAYER : openALEnabled ? SND_INTERFACE_OPENAL : SND_INTERFACE_APPLET);
}
private void switchState(int optionRange, int state) {
soundState = soundState - (soundState & bits._getMask(optionRange)) | state;
}
private void addExtState(int ext) {
soundState |= ext;
}
private void removeExtState(int ext) {
soundState = soundState - (soundState & bits._getMask(SND_EXT_BIT) & ext);
}
/**
* the synch-monitor for this instance
*/
private ThreadGroup soundTG = new ThreadGroup("Sound Input-TG");
/**
* the cache for all loaded sound file datas
*/
protected final static SpritesCacheManager<String, Map<String, Serializable>> _ALBackBuffer = new SpritesCacheManager<String, Map<String, Serializable>>();
static boolean initAL = true;
private static void _loadAL() {
if (openALEnabled && initAL) {
_ALBackBuffer.setCompressionEnabled(true);
_ALBackBuffer.setSwapDiskEnabled(true);
initAL = false;
int scratchCapacity = Integer.parseInt(RenderingScene.rb.getString("_ALBufferCapacity"));
GLHandler.sSnd.b.genVRAMBuffersMap(scratchCapacity);
GLHandler.sSnd.s.genVRAMBuffersMap(scratchCapacity);
}
}
/**
* the cache of the loaded sound files
*/
public final static SortedMap<String, Map<String, Serializable>> _ALCache = Collections.synchronizedSortedMap(_ALBackBuffer);
private static JXAException _getALNoRsrcEx(String fileRsrc) {
return new JXAException(JXAException.LEVEL.APP, "No such buffer has been loaded before, cannot create openAL source : " + fileRsrc);
}
private Player newJLayer(String filename) throws FileNotFoundException, JavaLayerException {
return new Player(new BufferedInputStream(isInnerResourceModeEnabled() ? getClass().getResourceAsStream(filename) : new FileInputStream(filename)));
}
/**
*
*/
protected AccessControlContext acc = AccessController.getContext();
/**
* loads up the specified file resource (.WAV only for
* {@linkplain #setHardwareAccel(boolean) openAL}, .MP3 for
* {@linkplain #setUseJLayerEnabled(boolean) JLayer})
*
* @param filename the file resource
* @return true or false, whether the resource could be loaded or not, resp.
* @see Class#getResource(String)
* @see Applet#newAudioClip(URL)
*/
@Override
public boolean load(final String filename) {
return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return _load(filename);
}
}, acc);
}
private boolean _load(String filename) {
if (filename == null || new String("").equals(filename) || isPlaying()) {
return false;
}
if (isResourceLoaded()) {
clearResource();
}
boolean loaded = false;
try {
URL rsrc = getClass().getResource(filename);
File file = new File(filename);
if (rsrc == null && !file.exists()) {
loaded = false;
} else {
if (JXAenvUtils._debug) {
System.err.print("SoundInput loads " + filename);
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_APPLET) != 0) {
bgSfx = Applet.newAudioClip(isInnerResourceModeEnabled() ? rsrc : file.toURI().toURL());
loaded = true;
if (JXAenvUtils._debug) {
System.err.println(" applet sound");
}
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_JLAYER) != 0) {
player = newJLayer(filename);
loaded = true;
if (JXAenvUtils._debug) {
System.err.println(" jlayer sound");
}
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0) {
WaveData waveFile = null;
Map<String, Serializable> map = null;
if (!_ALCache.containsKey(filename)) {
waveFile = WaveData.create(new BufferedInputStream(isInnerResourceModeEnabled() ? getClass().getResourceAsStream(filename) : new FileInputStream(file)));
map = new HashMap<String, Serializable>();
if (waveFile != null) {
map.put("data", new BufferIO(waveFile.data));
map.put("format", waveFile.format);
map.put("samplerate", waveFile.samplerate);
waveFile.dispose();
loaded = true;
_ALCache.put(filename, map);
if (JXAenvUtils._debug) {
System.err.println(" openAL sound");
}
}
} else {
loaded = true;
}
}
}
} catch (Exception ex) {
loaded = false;
throw new JXAException(ex);
} finally { /* run even after a thrown exception !!*/
fileRsrc = filename;
if (loaded) {
switchState(SND_STATE_BIT, SND_STATE_STOPPED);
if (JXAenvUtils._debug) {
System.err.println(toString() + " has been loaded.");
}
} else {
if (JXAenvUtils._debug) {
System.err.println(toString() + " no data has been loaded.");
}
switchState(SND_STATE_BIT, SND_STATE_CLEARED);
}
return loaded;
}
}
@Override
public String toString() {
return "SoundInput " + fileRsrc + " (hash " + hashCode() + ")";
}
/**
* returns the file path resource of this sound
*
* @return the file path resource of this sound
*/
public String getFileRsrc() {
return fileRsrc;
}
private void loadALBuffer() {
if (_ALCache.containsKey(fileRsrc)) {
int buffer = GLHandler.sSnd.b.getVRAMFindVacant(), source = GLHandler.sSnd.s.getVRAMFindVacant();
Map<String, Serializable> waveFile = _ALCache.get(fileRsrc);
AL10.alBufferData(buffer, (Integer) waveFile.get("format"), (ByteBuffer) ((BufferIO) waveFile.get("data")).getData(), (Integer) waveFile.get("samplerate"));
Util.checkALError();
AL10.alSourcei(source, AL10.AL_BUFFER, buffer);
AL10.alSourcef(source, AL10.AL_PITCH, 1.0f);
AL10.alSourcef(source, AL10.AL_GAIN, 1.0f);
AL10.alSource(source, AL10.AL_POSITION, sourcePos);
AL10.alSource(source, AL10.AL_VELOCITY, sourceVel);
Util.checkALError();
AL10.alListener(AL10.AL_POSITION, listenerPos);
AL10.alListener(AL10.AL_VELOCITY, listenerVel);
AL10.alListener(AL10.AL_ORIENTATION, listenerOri);
Util.checkALError();
GLHandler.sSnd.b.addItem(hashCode(), new MemScratch.Item(buffer, BufferIO._new(0)));
GLHandler.sSnd.s.addItem(hashCode(), new MemScratch.Item(buffer, BufferIO._newf(0)));
} else {
throw _getALNoRsrcEx(fileRsrc);
}
}
/**
* starts playing the sound in a loop <u>in a separate Thread</u>.
*/
private void loop() {
switchState(SND_STATE_BIT, SND_STATE_PLAYING);
if (JXAenvUtils._debug) {
System.out.println(toString() + " loop playback");
}
getMuteThread().start();
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_JLAYER) != 0) {
Thread t_mp3 = new Thread(soundTG, new Runnable() {
@Override
public void run() {
try {
if (JXAenvUtils._debug) {
System.out.println(toString() + " mp3 playback");
}
while (isPlaying()) {
if (player instanceof Player) {
if (!isMute()) {
player.play();
player.close();
player = newJLayer(fileRsrc);
}
} else {
break;
}
}
} catch (Exception ex) {
throw new JXAException(ex);
}
}
}, "T-loop-mp3");
t_mp3.setPriority(Thread.MAX_PRIORITY);
t_mp3.start();
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_APPLET) != 0) {
if (bgSfx instanceof AudioClip) {
if (!isMute()) {
bgSfx.loop();
}
}
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0) {
AL10.alSourcei(GLHandler.sSnd.s.firstItemBufferName(SoundInput.this.hashCode()), AL10.AL_LOOPING, AL10.AL_TRUE);
if (!isMute()) {
AL10.alSourcePlay(GLHandler.sSnd.s.firstItemBufferName(SoundInput.this.hashCode()));
}
}
}
private Thread getMuteThread() {
Thread t = new Thread(soundTG, new Runnable() {
@Override
public void run() {
if (!isRepeatEnabled()) {
return;
}
while (isPlaying()) {
if (isMute()) {
stop();
}
Thread.yield();
}
if (isRepeatEnabled()) {
while (isMute() && !isPlaying()) {
Thread.yield();
}
if (!isPlaying()) {
loop();
}
}
}
}, toString() + "-muteMonitor");
t.setPriority(Math.max(Thread.MIN_PRIORITY, Thread.currentThread().getPriority() - 1));
return t;
}
/**
* starts playing te sound <u>in a separate Thread</u>.
*/
@Override
public void play() {
assert fileRsrc != null : "Sound : you must call load(String) once before to play the Sound";
if (!isResourceLoaded()) {
if (!load(fileRsrc)) {
return;
}
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0) {
try {
loadALBuffer();
} catch (OpenALException ex) {
throw new JXAException(ex);
}
}
if (isRepeatEnabled()) {
loop();
} else {
if (JXAenvUtils._debug) {
System.out.println(toString() + " playback");
}
final Thread tmute = getMuteThread();
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_JLAYER) != 0) {
Thread t_mp3 = new Thread(soundTG, new Runnable() {
@Override
public void run() {
try {
if (player instanceof Player) {
if (JXAenvUtils._debug) {
System.out.println(toString() + " mp3 playback");
}
switchState(SND_STATE_BIT, SND_STATE_PLAYING);
tmute.start();
if (!isMute()) {
player.play();
stop();
}
}
} catch (JavaLayerException ex) {
throw new JXAException(ex);
}
}
}, "T-play-mp3");
t_mp3.setPriority(Thread.MAX_PRIORITY);
t_mp3.start();
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_APPLET) != 0) {
if (bgSfx instanceof AudioClip) {
switchState(SND_STATE_BIT, SND_STATE_PLAYING);
tmute.start();
if (!isMute()) {
bgSfx.play();
}
}
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0) {
switchState(SND_STATE_BIT, SND_STATE_PLAYING);
AL10.alSourcei(GLHandler.sSnd.s.firstItemBufferName(SoundInput.this.hashCode()), AL10.AL_LOOPING, AL10.AL_FALSE);
tmute.start();
if (!isMute()) {
AL10.alSourcePlay(GLHandler.sSnd.s.firstItemBufferName(SoundInput.this.hashCode()));
}
}
}
}
/**
*
* @return
*/
public boolean isMute() {
return _mute || (soundState & bits._getMask(SND_EXT_BIT) & SND_EXT_MUTE) != 0;
}
/**
*
*/
public void mute() {
addExtState(SND_EXT_MUTE);
}
/**
*
*/
public void unmute() {
removeExtState(SND_EXT_MUTE);
}
/**
* stops the current playing sound
*/
@Override
public void stop() {
if ((soundState & bits._getMask(SND_STATE_BIT) & SND_STATE_PLAYING) != 0) {
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_JLAYER) != 0) {
if (player instanceof Player) {
player.close();
}
switchState(SND_STATE_BIT, SND_STATE_STOPPED);
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_APPLET) != 0) {
if (bgSfx instanceof AudioClip) {
bgSfx.stop();
}
switchState(SND_STATE_BIT, SND_STATE_STOPPED);
}
if ((soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0) {
AL10.alSourceStop(GLHandler.sSnd.s.firstItemBufferName(SoundInput.this.hashCode()));
switchState(SND_STATE_BIT, SND_STATE_STOPPED);
}
}
}
/**
* returns true or false, whether the JLayer is enabled or not, resp.
*
* @return true or false, whether the JLayer is enabled or not, resp.
*/
public boolean isUseJLayerEnabled() {
return (soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_JLAYER) != 0;
}
@Override
public void setRepeatEnabled(boolean b) {
if (b) {
addExtState(SND_EXT_LOOP);
} else {
removeExtState(SND_EXT_LOOP);
}
}
@Override
public boolean isRepeatEnabled() {
return (soundState & bits._getMask(SND_EXT_BIT) & SND_EXT_LOOP) != 0;
}
/**
* instances a new SoundInput
*/
public SoundInput() {
this("", true);
}
/**
* instances a new SoundInput
*
* @param filePath the file resource for the sound (.aif, .wav, .mp3--Jlayer
* must be enabled--is supported)
* @param innerResource tells whether to look for the file in the classpath
* or the absolute path
*/
public SoundInput(String filePath, boolean innerResource) {
ThreadWorks.prioritizeThreadGroups(ThreadWorks.securityTWKS.workTG, soundTG);
fileRsrc = filePath;
setInnerResourceModeEnabled(innerResource);
_loadAL();
}
/**
*
* @return
*/
@Override
public Object loadResource() {
return load(fileRsrc);
}
/**
* 1) Releases all sources. 2) Releases all buffers. 3) destroys openAL.
*/
private static void _killAL() {
if (initAL) {
GLHandler.sSnd.s.freeScratch();
GLHandler.sSnd.b.freeScratch();
AL.destroy();
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
clearResource();
}
/**
* clears the current SoundInput resources. NOTICE : it seems that openAL,
* unlike openGL, does NOT provide a perfect target-resource management, so
* that a deletedSource cannot be reused again at the same address. Avoiding
* to use this method is preferred.
*
* @return
*/
@Override
public Object clearResource() {
if (!isResourceLoaded()) {
return null;
}
stop();
if (player instanceof Player) {
player = null;
}
if (bgSfx instanceof AudioClip) {
bgSfx = null;
}
GLHandler.sSnd.s.discardItems((RenderingSceneGL)rsc, hashCode());
GLHandler.sSnd.b.discardItems((RenderingSceneGL)rsc, hashCode());
switchState(SND_STATE_BIT, SND_STATE_CLEARED);
return null;
}
/**
*
* @return
*/
@Override
public boolean isResourceLoaded() {
return (soundState & bits._getMask(SND_STATE_BIT) & SND_STATE_CLEARED) == 0;
}
/**
* @return
*/
public boolean isInnerResourceModeEnabled() {
return (soundState & bits._getMask(SND_EXT_BIT) & SND_EXT_INRSRC) != 0;
}
/**
* @param b
*/
public final void setInnerResourceModeEnabled(boolean b) {
if (b) {
addExtState(SND_EXT_INRSRC);
} else {
removeExtState(SND_EXT_INRSRC);
}
}
private RenderingScene rsc = null;
@Override
public RenderingScene getRenderingScene() {
return rsc;
}
@Override
public void setRenderingScene(RenderingScene renderingScene) {
rsc = renderingScene;
}
/**
* dis/enables OpenAL (kills/creates the AL context if needed)
*
* @param openALEnabled
*/
public static void _setOpenALEnabled(boolean openALEnabled) {
SoundInput.openALEnabled = openALEnabled;
if (!openALEnabled) {
_killAL();
} else {
_loadAL();
}
}
/**
* returns true or false, whether OpenAL is enabled or not, resp.
*
* @return
*/
public static boolean _isOpenALEnabled() {
return openALEnabled;
}
/**
* enables OpenAL output (.mp3 is not supported with OpenAL) if
* {@linkplain #_isOpenALEnabled()} returns false, this method does nothing
*
* @param b
*/
@Override
public void setHardwareAccel(boolean b) {
if (openALEnabled) {
switchState(SND_INTERFACE_BIT, b ? SND_INTERFACE_OPENAL : isUseJLayerEnabled() ? SND_INTERFACE_JLAYER : SND_INTERFACE_APPLET);
if (b) {
_loadAL();
}
} else {
if (JXAenvUtils._debug) {
System.err.println(JXAenvUtils.log("OpenAL is not enabled so hardware acceleration of sfx is not available. To enable : SoundInput.setOpenALEnabled(true)", JXAenvUtils.LVL.APP_WRN));
}
}
}
/**
* returns true or false, whether OpenAL output is enabled or disabled ,
* resp.
*
* @return
*/
@Override
public boolean isHardwareAccel() {
return (soundState & bits._getMask(SND_INTERFACE_BIT) & SND_INTERFACE_OPENAL) != 0;
}
}