diff --git a/jme3-core/src/main/java/com/jme3/audio/AudioStream.java b/jme3-core/src/main/java/com/jme3/audio/AudioStream.java
index d528e01fe..61c5ff744 100644
--- a/jme3-core/src/main/java/com/jme3/audio/AudioStream.java
+++ b/jme3-core/src/main/java/com/jme3/audio/AudioStream.java
@@ -39,36 +39,37 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/**
- * AudioStream
is an implementation of AudioData that
- * acquires the audio from an InputStream. Audio can be streamed
- * from network, hard drive etc. It is assumed the data coming
- * from the input stream is uncompressed.
+ * AudioStream
is an implementation of AudioData that acquires the
+ * audio from an InputStream. Audio can be streamed from network, hard drive
+ * etc. It is assumed the data coming from the input stream is uncompressed.
*
* @author Kirill Vainer
*/
-public class AudioStream extends AudioData implements Closeable{
+public class AudioStream extends AudioData implements Closeable {
private final static Logger logger = Logger.getLogger(AudioStream.class.getName());
protected InputStream in;
protected float duration = -1f;
protected boolean open = false;
+ protected boolean eof = false;
protected int[] ids;
-
- public AudioStream(){
- super();
+
+ public AudioStream() {
+ super();
}
-
- protected AudioStream(int[] ids){
+
+ protected AudioStream(int[] ids) {
// Pass some dummy ID so handle
// doesn't get created.
- super(-1);
+ super(-1);
// This is what gets destroyed in reality
this.ids = ids;
}
- public void updateData(InputStream in, float duration){
- if (id != -1 || this.in != null)
+ public void updateData(InputStream in, float duration) {
+ if (id != -1 || this.in != null) {
throw new IllegalStateException("Data already set!");
+ }
this.in = in;
this.duration = duration;
@@ -76,22 +77,27 @@ public class AudioStream extends AudioData implements Closeable{
}
/**
- * Reads samples from the stream. The format of the data
- * depends on the getSampleRate(), getChannels(), getBitsPerSample()
- * values.
+ * Reads samples from the stream. The format of the data depends on the
+ * getSampleRate(), getChannels(), getBitsPerSample() values.
*
* @param buf Buffer where to read the samples
* @param offset The offset in the buffer where to read samples
* @param length The length inside the buffer where to read samples
* @return number of bytes read.
*/
- public int readSamples(byte[] buf, int offset, int length){
- if (!open)
+ public int readSamples(byte[] buf, int offset, int length) {
+ if (!open || eof) {
return -1;
+ }
- try{
- return in.read(buf, offset, length);
- }catch (IOException ex){
+ try {
+ int totalRead = in.read(buf, offset, length);
+ if (totalRead < 0) {
+ eof = true;
+ }
+ return totalRead;
+ } catch (IOException ex) {
+ eof = true;
return -1;
}
}
@@ -103,41 +109,41 @@ public class AudioStream extends AudioData implements Closeable{
* @param buf Buffer where to read the samples
* @return number of bytes read.
*/
- public int readSamples(byte[] buf){
+ public int readSamples(byte[] buf) {
return readSamples(buf, 0, buf.length);
}
- public float getDuration(){
+ public float getDuration() {
return duration;
}
@Override
- public int getId(){
+ public int getId() {
throw new RuntimeException("Don't use getId() on streams");
}
@Override
- public void setId(int id){
+ public void setId(int id) {
throw new RuntimeException("Don't use setId() on streams");
}
- public void initIds(int count){
+ public void initIds(int count) {
ids = new int[count];
}
- public int getId(int index){
+ public int getId(int index) {
return ids[index];
}
- public void setId(int index, int id){
+ public void setId(int index, int id) {
ids[index] = id;
}
- public int[] getIds(){
+ public int[] getIds() {
return ids;
}
- public void setIds(int[] ids){
+ public void setIds(int[] ids) {
this.ids = ids;
}
@@ -155,9 +161,7 @@ public class AudioStream extends AudioData implements Closeable{
@Override
public void deleteObject(Object rendererObject) {
- // It seems that the audio renderer is already doing a good
- // job at deleting audio streams when they finish playing.
-// ((AudioRenderer)rendererObject).deleteAudioData(this);
+ ((AudioRenderer) rendererObject).deleteAudioData(this);
}
@Override
@@ -165,42 +169,42 @@ public class AudioStream extends AudioData implements Closeable{
return new AudioStream(ids);
}
- /**
- * @return Whether the stream is open or not. Reading from a closed
- * stream will always return eof.
- */
- public boolean isOpen(){
- return open;
+ public boolean isEOF() {
+ return eof;
}
-
+
/**
- * Closes the stream, releasing all data relating to it. Reading
- * from the stream will return eof.
+ * Closes the stream, releasing all data relating to it.
+ * Reading from the stream will return eof.
+ *
* @throws IOException
*/
public void close() {
- if (in != null && open){
- try{
+ if (in != null && open) {
+ try {
in.close();
- }catch (IOException ex){
+ } catch (IOException ex) {
}
open = false;
- }else{
+ } else {
throw new RuntimeException("AudioStream is already closed!");
}
}
-
- public void setTime(float time){
- if(in instanceof SeekableStream){
- ((SeekableStream)in).setTime(time);
- }else{
- logger.log(Level.WARNING,"Cannot use setTime on a stream that is not seekable. You must load the file with the streamCache option set to true");
+ public void setTime(float time) {
+ if (in instanceof SeekableStream) {
+ ((SeekableStream) in).setTime(time);
+ eof = false;
+ } else {
+ logger.log(Level.WARNING,
+ "Cannot use setTime on a stream that "
+ + "is not seekable. You must load the file "
+ + "with the streamCache option set to true");
}
}
@Override
public long getUniqueId() {
- return ((long)OBJTYPE_AUDIOSTREAM << 32) | ((long)ids[0]);
+ return ((long) OBJTYPE_AUDIOSTREAM << 32) | ((long) ids[0]);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
index 61b0393dd..3b3c7fc35 100644
--- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
+++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
@@ -72,9 +72,10 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
private int auxSends = 0;
private int reverbFx = -1;
private int reverbFxSlot = -1;
- // Update audio 20 times per second
+
+ // Fill streaming sources every 50 ms
private static final float UPDATE_RATE = 0.05f;
- private final Thread audioThread = new Thread(this, "jME3 Audio Thread");
+ private final Thread decoderThread = new Thread(this, "jME3 Audio Decoding Thread");
private final AtomicBoolean threadLock = new AtomicBoolean(false);
private final AL al;
@@ -88,23 +89,26 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
public void initialize() {
- if (!audioThread.isAlive()) {
- audioThread.setDaemon(true);
- audioThread.setPriority(Thread.NORM_PRIORITY + 1);
- audioThread.start();
+ if (!decoderThread.isAlive()) {
+ // Set high priority to avoid buffer starvation.
+ decoderThread.setDaemon(true);
+ decoderThread.setPriority(Thread.NORM_PRIORITY + 1);
+ decoderThread.start();
} else {
throw new IllegalStateException("Initialize already called");
}
}
private void checkDead() {
- if (audioThread.getState() == Thread.State.TERMINATED) {
- throw new IllegalStateException("Audio thread is terminated");
+ if (decoderThread.getState() == Thread.State.TERMINATED) {
+ throw new IllegalStateException("Decoding thread is terminated");
}
}
public void run() {
- initInThread();
+ initInDecoderThread();
+
+ // Notify render thread that OAL context is available.
synchronized (threadLock) {
threadLock.set(true);
threadLock.notifyAll();
@@ -120,7 +124,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
synchronized (threadLock) {
- updateInThread(UPDATE_RATE);
+ updateInDecoderThread(UPDATE_RATE);
}
long endTime = System.nanoTime();
@@ -139,11 +143,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
synchronized (threadLock) {
- cleanupInThread();
+ cleanupInDecoderThread();
}
}
- public void initInThread() {
+ public void initInDecoderThread() {
try {
if (!alc.isCreated()) {
alc.createALC();
@@ -225,7 +229,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
}
- public void cleanupInThread() {
+ public void cleanupInDecoderThread() {
if (audioDisabled) {
alc.destroyALC();
return;
@@ -264,10 +268,10 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
public void cleanup() {
// kill audio thread
- if (audioThread.isAlive()) {
- audioThread.interrupt();
+ if (decoderThread.isAlive()) {
+ decoderThread.interrupt();
try {
- audioThread.join();
+ decoderThread.join();
} catch (InterruptedException ex) {
}
}
@@ -453,10 +457,8 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
break;
case Looping:
- if (src.isLooping()) {
- if (!(src.getAudioData() instanceof AudioStream)) {
- al.alSourcei(id, AL_LOOPING, AL_TRUE);
- }
+ if (src.isLooping() && !(src.getAudioData() instanceof AudioStream)) {
+ al.alSourcei(id, AL_LOOPING, AL_TRUE);
} else {
al.alSourcei(id, AL_LOOPING, AL_FALSE);
}
@@ -509,7 +511,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
}
}
- if (forceNonLoop) {
+ if (forceNonLoop || src.getAudioData() instanceof AudioStream) {
al.alSourcei(id, AL_LOOPING, AL_FALSE);
} else {
al.alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE);
@@ -661,45 +663,71 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return true;
}
- private boolean fillStreamingSource(int sourceId, AudioStream stream) {
- if (!stream.isOpen()) {
- return false;
- }
-
- boolean active = true;
+ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) {
+ boolean success = false;
int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED);
-
-// while((processed--) != 0){
- if (processed > 0) {
+
+ for (int i = 0; i < processed; i++) {
int buffer;
ib.position(0).limit(1);
al.alSourceUnqueueBuffers(sourceId, 1, ib);
buffer = ib.get(0);
- active = fillBuffer(stream, buffer);
-
- ib.position(0).limit(1);
- ib.put(0, buffer);
- al.alSourceQueueBuffers(sourceId, 1, ib);
- }
-
- if (!active && stream.isOpen()) {
- stream.close();
+ boolean active = fillBuffer(stream, buffer);
+
+ if (!active && !stream.isEOF()) {
+ throw new AssertionError();
+ }
+
+ if (!active && looping) {
+ stream.setTime(0);
+ active = fillBuffer(stream, buffer);
+ }
+
+ if (active) {
+ ib.position(0).limit(1);
+ ib.put(0, buffer);
+ al.alSourceQueueBuffers(sourceId, 1, ib);
+ // At least one buffer enqueued = success.
+ success = true;
+ } else {
+ // No more data left to process.
+ break;
+ }
}
- return active;
+ return success;
}
- private boolean attachStreamToSource(int sourceId, AudioStream stream) {
- boolean active = true;
+ private boolean attachStreamToSource(int sourceId, AudioStream stream, boolean looping) {
+ boolean success = false;
+
+ // Reset the stream. Typically happens if it finished playing on
+ // its own and got reclaimed.
+ // Note that AudioNode.stop() already resets the stream
+ // since it might not be in EOF when stopped.
+ if (stream.isEOF()) {
+ stream.setTime(0);
+ }
+
for (int id : stream.getIds()) {
- active = fillBuffer(stream, id);
- ib.position(0).limit(1);
- ib.put(id).flip();
- al.alSourceQueueBuffers(sourceId, 1, ib);
+ boolean active = fillBuffer(stream, id);
+ if (!active && !stream.isEOF()) {
+ throw new AssertionError();
+ }
+ if (!active && looping) {
+ stream.setTime(0);
+ active = fillBuffer(stream, id);
+ }
+ if (active) {
+ ib.position(0).limit(1);
+ ib.put(id).flip();
+ al.alSourceQueueBuffers(sourceId, 1, ib);
+ success = true;
+ }
}
- return active;
+ return success;
}
private boolean attachBufferToSource(int sourceId, AudioBuffer buffer) {
@@ -707,11 +735,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return true;
}
- private boolean attachAudioToSource(int sourceId, AudioData data) {
+ private boolean attachAudioToSource(int sourceId, AudioData data, boolean looping) {
if (data instanceof AudioBuffer) {
return attachBufferToSource(sourceId, (AudioBuffer) data);
} else if (data instanceof AudioStream) {
- return attachStreamToSource(sourceId, (AudioStream) data);
+ return attachStreamToSource(sourceId, (AudioStream) data, looping);
}
throw new UnsupportedOperationException();
}
@@ -723,15 +751,9 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
int sourceId = channels[index];
al.alSourceStop(sourceId);
-
- if (src.getAudioData() instanceof AudioStream) {
- AudioStream str = (AudioStream) src.getAudioData();
- ib.position(0).limit(STREAMING_BUFFER_COUNT);
- ib.put(str.getIds()).flip();
- al.alSourceUnqueueBuffers(sourceId, STREAMING_BUFFER_COUNT, ib);
- } else if (src.getAudioData() instanceof AudioBuffer) {
- al.alSourcei(sourceId, AL_BUFFER, 0);
- }
+
+ // For streaming sources, this will clear all queued buffers.
+ al.alSourcei(sourceId, AL_BUFFER, 0);
if (src.getDryFilter() != null && supportEfx) {
// detach filter
@@ -747,71 +769,118 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
chanSrcs[index] = null;
}
}
+
+ private AudioSource.Status convertStatus(int oalStatus) {
+ switch (oalStatus) {
+ case AL_INITIAL:
+ case AL_STOPPED:
+ return Status.Stopped;
+ case AL_PAUSED:
+ return Status.Paused;
+ case AL_PLAYING:
+ return Status.Playing;
+ default:
+ throw new UnsupportedOperationException("Unrecognized OAL state: " + oalStatus);
+ }
+ }
public void update(float tpf) {
- // does nothing
+ synchronized (threadLock) {
+ updateInRenderThread(tpf);
+ }
}
- public void updateInThread(float tpf) {
+ public void updateInRenderThread(float tpf) {
if (audioDisabled) {
return;
}
-
+
for (int i = 0; i < channels.length; i++) {
AudioSource src = chanSrcs[i];
+
if (src == null) {
continue;
}
int sourceId = channels[i];
-
- // is the source bound to this channel
- // if false, it's an instanced playback
boolean boundSource = i == src.getChannel();
-
- // source's data is streaming
- boolean streaming = src.getAudioData() instanceof AudioStream;
-
- // only buffered sources can be bound
- assert (boundSource && streaming) || (!streaming);
-
- int state = al.alGetSourcei(sourceId, AL_SOURCE_STATE);
- boolean wantPlaying = src.getStatus() == Status.Playing;
- boolean stopped = state == AL_STOPPED;
-
- if (streaming && wantPlaying) {
- AudioStream stream = (AudioStream) src.getAudioData();
- if (stream.isOpen()) {
- fillStreamingSource(sourceId, stream);
- if (stopped) {
- al.alSourcePlay(sourceId);
+ boolean reclaimChannel = false;
+
+ Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
+ Status jmeStatus = src.getStatus();
+
+ // Check if we need to sync JME status with OAL status.
+ if (oalStatus != jmeStatus) {
+ if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) {
+ // Maybe we need to reclaim the channel.
+ if (src.getAudioData() instanceof AudioStream) {
+ AudioStream stream = (AudioStream) src.getAudioData();
+
+ if (stream.isEOF() && !src.isLooping()) {
+ // Stream finished playing
+ reclaimChannel = true;
+ } else {
+ // Stream still has data.
+ // Buffer starvation occured.
+ // Audio decoder thread will fill the data
+ // and start the channel again.
+ }
+ } else {
+ // Buffer finished playing.
+ reclaimChannel = true;
}
- } else {
- if (stopped) {
- // became inactive
- src.setStatus(Status.Stopped);
- src.setChannel(-1);
+
+ if (reclaimChannel) {
+ if (boundSource) {
+ src.setStatus(Status.Stopped);
+ src.setChannel(-1);
+ }
clearChannel(i);
freeChannel(i);
-
- // And free the audio since it cannot be
- // played again anyway.
- deleteAudioData(stream);
}
+ } else {
+ // jME3 state does not match OAL state.
+ throw new AssertionError();
}
- } else if (!streaming) {
- boolean paused = state == AL_PAUSED;
+ } else {
+ // Stopped channel was not cleared correctly.
+ if (oalStatus == Status.Stopped) {
+ throw new AssertionError();
+ }
+ }
+ }
+ }
+
+ public void updateInDecoderThread(float tpf) {
+ if (audioDisabled) {
+ return;
+ }
- // make sure OAL pause state & source state coincide
- assert (src.getStatus() == Status.Paused && paused) || (!paused);
+ for (int i = 0; i < channels.length; i++) {
+ AudioSource src = chanSrcs[i];
+
+ if (src == null || !(src.getAudioData() instanceof AudioStream)) {
+ continue;
+ }
- if (stopped) {
- if (boundSource) {
- src.setStatus(Status.Stopped);
- src.setChannel(-1);
- }
- clearChannel(i);
- freeChannel(i);
+ int sourceId = channels[i];
+ AudioStream stream = (AudioStream) src.getAudioData();
+
+ Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
+ Status jmeStatus = src.getStatus();
+
+ // Keep filling data (even if we are stopped / paused)
+ boolean buffersWereFilled = fillStreamingSource(sourceId, stream, src.isLooping());
+
+ if (buffersWereFilled) {
+ if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) {
+ // The source got stopped due to buffer starvation.
+ // Start it again.
+ logger.log(Level.WARNING, "Buffer starvation "
+ + "occurred while playing stream");
+ al.alSourcePlay(sourceId);
+ } else {
+ // Buffers were filled, stream continues to play.
}
}
}
@@ -877,7 +946,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
if (src.getAudioData() instanceof AudioStream) {
throw new UnsupportedOperationException(
"Cannot play instances "
- + "of audio streams. Use playSource() instead.");
+ + "of audio streams. Use play() instead.");
}
if (src.getAudioData().isUpdateNeeded()) {
@@ -896,7 +965,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
// set parameters, like position and max distance
setSourceParams(sourceId, src, true);
- attachAudioToSource(sourceId, src.getAudioData());
+ attachAudioToSource(sourceId, src.getAudioData(), false);
chanSrcs[index] = src;
// play the channel
@@ -917,12 +986,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return;
}
- //assert src.getStatus() == Status.Stopped || src.getChannel() == -1;
-
if (src.getStatus() == Status.Playing) {
return;
} else if (src.getStatus() == Status.Stopped) {
-
+ assert src.getChannel() != -1;
+
// allocate channel to this source
int index = newChannel();
if (index == -1) {
@@ -939,7 +1007,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
chanSrcs[index] = src;
setSourceParams(channels[index], src, false);
- attachAudioToSource(channels[index], data);
+ attachAudioToSource(channels[index], data, src.isLooping());
}
al.alSourcePlay(channels[src.getChannel()]);
@@ -989,16 +1057,9 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
src.setChannel(-1);
clearChannel(chan);
freeChannel(chan);
-
+
if (src.getAudioData() instanceof AudioStream) {
- AudioStream stream = (AudioStream) src.getAudioData();
- if (stream.isOpen()) {
- stream.close();
- }
-
- // And free the audio since it cannot be
- // played again anyway.
- deleteAudioData(src.getAudioData());
+ ((AudioStream)src.getAudioData()).setTime(0);
}
}
}