/******************************************************************************
JSoundSystem is a simple and easy sound API to use sound in your Java applications.
Copyright (c) 2014, Johan Jansen
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used
to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
************************************************************************/
package net.jsoundsystem;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import net.jsoundsystem.utils.Vector3f;
class AudioThread extends Thread {
//Effect mixer modifiers. They are used in a bitmap so each much be a power of two
private static final int MIX_VOLUME = 1 << 0;
private static final int MIX_PANNING = 1 << 1;
private static final int MIX_SPEED = 1 << 2;
//Thread sound effects
private boolean looping, paused, stopped;
private boolean killThread;
private float volume;
private float panning;
private float speed;
//3D sound simulation
private boolean simulate3DEffect;
private Vector3f source;
private float lastVolume;
//Playback data
private AudioFormat soundFormat;
private byte[] soundData;
private File filePath;
private SourceDataLine audioChannel;
/**
* Constructs a new SoundThread, should only be called by the SoundSystem
* @throws IOException
* @throws UnsupportedAudioFileException
*/
AudioThread( File path, byte[] data, AudioFormat format ) throws UnsupportedAudioFileException, IOException {
super( path.getName() );
filePath = path;
setPriority( Thread.MIN_PRIORITY ); //Sounds have low priority
setDaemon(true); //And run independently
//Set default values
volume = 1.00f;
speed = 1.00f;
panning = 0.00f;
//Get the audio format for this sound
soundFormat = format;
soundData = data;
}
public void enableSpatializedSound(){
source = new Vector3f();
simulate3DEffect = true;
}
protected void setLooped( boolean looping ){
this.looping = looping;
}
protected void setPanning( float panning ){
this.panning = panning;
mixSoundEffects( audioChannel, MIX_PANNING );
}
protected void setVolume( float volume ){
this.volume = volume;
mixSoundEffects( audioChannel, MIX_VOLUME );
}
protected void setSpeed( float speed ){
this.speed = speed;
mixSoundEffects( audioChannel, MIX_SPEED );
}
/**
* Internal run function inherited from the Thread class. This is where the actual sound playing
* happens. This function should not be run directly, but rather activated by the play() function.
*/
public void run() {
try {
//Keep data ready until we are disposed of
while( !killThread ){
//Open and reserve a sound channel
DataLine.Info info = new DataLine.Info(SourceDataLine.class, soundFormat);
audioChannel = (SourceDataLine) AudioSystem.getLine(info);
audioChannel.open( soundFormat );
JSoundSystem.channelsPlaying++;
//Ready the sound stream
InputStream stream = null;
//It's a sound loaded into memory
if( soundData != null ) {
stream = new ByteArrayInputStream(soundData);
//Use mark if supported
if( stream.markSupported() ) stream.mark( stream.available() );
}
//This might be do once or in infinity, depending on the loop variable
do{
//Reset sound and reopen a sound stream if needed
if( stream != null && stream.markSupported() ) stream.reset();
else {
//Need to reopen the sound to reset it
if( stream != null) stream.close();
stream = JSoundSystem.getAudioInputStream(filePath);
if( stream.markSupported() ) stream.mark( stream.available() );
}
//begin playing
audioChannel.start();
//Apply various sound effects
mixSoundEffects( audioChannel, MIX_PANNING | MIX_VOLUME | MIX_SPEED );
//This actually plays the sound
int len = 0;
int bytesPerFrame = soundFormat.getFrameSize();
// some audio formats may have unspecified frame size
// in that case we may read any amount of bytes
if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) bytesPerFrame = 1;
// Set an arbitrary buffer size of 1024 frames.
int numBytes = 1024 * bytesPerFrame;
byte[] audioBytes = new byte[numBytes];
//Keep playing as long as there is data left and sound has not been stopped
while ( !stopped && (len = stream.read(audioBytes) ) != -1 ) {
//Pause until we are told to continue
if( paused ) sleepThread();
//Update 3D sound effects
if( simulate3DEffect ) update3DSound();
audioChannel.write(audioBytes, 0, len);
}
//Finish the rest of the data
if( !stopped ) audioChannel.drain();
//Release resources
audioChannel.stop();
stream.close();
}
while( looping && !stopped );
audioChannel.close();
audioChannel = null;
JSoundSystem.channelsPlaying--;
//Pause until further notice
sleepThread();
}
} catch(LineUnavailableException e){
System.err.println("Could not play sound ("+ getName() +"): Audio Drivers doesnt support more than " + JSoundSystem.channelsPlaying + "sound channels.");
} catch (Exception e) {
System.err.println("Error playing sound ("+ getName() +"): " + e);
e.printStackTrace();
}
//This thread should be killed now
}
private void sleepThread(){
if( killThread ) return;
paused = true;
//This causes the thread to sleep until a notify() is called
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void resumeThread(){
//No need to wake up if we are not sleeping
if( !paused || killThread ) return;
synchronized (this) {
this.notifyAll(); //Wake up!
paused = false;
if( audioChannel != null ) audioChannel.start();
}
}
/**
* Begins playing the sound or resumes if it was paused
*/
protected void play(){
if( killThread ) return;
stopped = false;
//Begin playing for the first time
if( !this.isAlive() ){
start();
return;
}
//Already playing, try to unpause
resumeThread();
}
protected void pause() {
paused = true;
if( audioChannel != null ) audioChannel.stop();
}
protected void dispose(){
stopped = true;
killThread = true;
}
/**
* JJ> This updates sound mixer effects like volume and sound balance to an active audio channel
* @param line Which audio line to update
* @param effects A bitmask with the effects to update
*/
private void mixSoundEffects( Line line, int effects ) {
//No need to mix something that isn't playing
if( audioChannel == null ) return;
//Adjust sound speed
if( (effects & MIX_SPEED) != 0 && line.isControlSupported(FloatControl.Type.SAMPLE_RATE) ) {
FloatControl gainControl = (FloatControl)line.getControl(FloatControl.Type.SAMPLE_RATE);
float sampleRate = soundFormat.getFrameRate() * speed;
sampleRate = Math.max(gainControl.getMinimum(), Math.min(sampleRate, gainControl.getMaximum()));
gainControl.setValue(sampleRate);
}
//Adjust sound balance
if( (effects & MIX_PANNING) != 0 && line.isControlSupported(FloatControl.Type.PAN) ) {
FloatControl gainControl = (FloatControl)line.getControl(FloatControl.Type.PAN);
panning = Math.max(gainControl.getMinimum(), Math.min(panning, gainControl.getMaximum()));
gainControl.setValue(panning);
}
//Set sound volume
if( (effects & MIX_VOLUME) != 0 && line.isControlSupported(FloatControl.Type.MASTER_GAIN) ) {
FloatControl gainControl = (FloatControl)line.getControl(FloatControl.Type.MASTER_GAIN);
float gain = (float)(Math.log(volume)/Math.log(10.0f)*20.0f);
gain = Math.max(gainControl.getMinimum(), Math.min(gain, gainControl.getMaximum()));
gainControl.setValue(gain);
}
}
protected boolean isPlaying() {
return !paused && !stopped;
}
protected void stopPlaying(){
resumeThread();
stopped = true;
if( audioChannel != null ){
audioChannel.stop();
audioChannel.flush();
}
}
private void update3DSound() {
Vector3f listenerPosition = JSoundSystem.getListenerPosition();
float distance = listenerPosition.getDistance(source);
//Calculate how loud the sound is
float newVolume = (JSoundSystem.maxDistance - distance) / JSoundSystem.maxDistance;
if (newVolume <= 0) newVolume = 0;
//Calculate if the sound is left or right oriented
float newPanning = (2.00f/JSoundSystem.maxDistance) * -(listenerPosition.x - source.x);
//Now actually update the effects
setVolume( (newVolume + lastVolume) / 2);
setPanning( newPanning );
lastVolume = newVolume;
}
protected void setSourcePosition( Vector3f pos ){
source = pos;
}
public boolean isPaused() {
return paused;
}
/**
* This function makes an exact copy of this JSoundThread, also cloning the sound data, format, etc.
*/
public AudioThread clone() {
AudioThread copy;
//Try to clone the actual thread
try {
copy = new AudioThread( filePath, soundData, soundFormat );
} catch (Exception e) {
e.printStackTrace();
return null;
}
//Copy attributes
copy.volume = this.volume;
copy.looping = this.looping;
copy.speed = this.speed;
copy.source = new Vector3f(source);
//Finished cloning
return copy;
}
public AudioFormat getAudioFormat() {
return soundFormat;
}
public void invertSoundData(){
//Simply change reference to the byte array instead of actually modifying
//the array itself. Others might be using the old non-inversed array through
//the clone() method. Slower, but it's safe.
byte[] inverseData = new byte[soundData.length];
for( int i = 0; i < soundData.length; i++ ){
inverseData[soundData.length-i-1] = soundData[i];
}
soundData = inverseData;
}
}