Streaming audio data now works in a similar way to buffered data.

It is possible to loop streaming sources, as well as play
them again after they finished playing. It is possible to stop()
and then play() a streaming source to start playing from the
beginning. Essentially the lifecycle of a streaming audio
is now completely controlled by the user, in the same fashion
as buffered data.
In addition, AudioNode status updates (when an audio stops playing)
now occur every frame rendered, as opposed to every 50 milliseconds.
experimental
shadowislord 10 years ago
parent 3b384a7e58
commit a5699e9d82
  1. 110
      jme3-core/src/main/java/com/jme3/audio/AudioStream.java
  2. 293
      jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java

@ -39,36 +39,37 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* <code>AudioStream</code> is an implementation of AudioData that * <code>AudioStream</code> is an implementation of AudioData that acquires the
* acquires the audio from an InputStream. Audio can be streamed * audio from an InputStream. Audio can be streamed from network, hard drive
* from network, hard drive etc. It is assumed the data coming * etc. It is assumed the data coming from the input stream is uncompressed.
* from the input stream is uncompressed.
* *
* @author Kirill Vainer * @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()); private final static Logger logger = Logger.getLogger(AudioStream.class.getName());
protected InputStream in; protected InputStream in;
protected float duration = -1f; protected float duration = -1f;
protected boolean open = false; protected boolean open = false;
protected boolean eof = false;
protected int[] ids; protected int[] ids;
public AudioStream(){ public AudioStream() {
super(); super();
} }
protected AudioStream(int[] ids){ protected AudioStream(int[] ids) {
// Pass some dummy ID so handle // Pass some dummy ID so handle
// doesn't get created. // doesn't get created.
super(-1); super(-1);
// This is what gets destroyed in reality // This is what gets destroyed in reality
this.ids = ids; this.ids = ids;
} }
public void updateData(InputStream in, float duration){ public void updateData(InputStream in, float duration) {
if (id != -1 || this.in != null) if (id != -1 || this.in != null) {
throw new IllegalStateException("Data already set!"); throw new IllegalStateException("Data already set!");
}
this.in = in; this.in = in;
this.duration = duration; this.duration = duration;
@ -76,22 +77,27 @@ public class AudioStream extends AudioData implements Closeable{
} }
/** /**
* Reads samples from the stream. The format of the data * Reads samples from the stream. The format of the data depends on the
* depends on the getSampleRate(), getChannels(), getBitsPerSample() * getSampleRate(), getChannels(), getBitsPerSample() values.
* values.
* *
* @param buf Buffer where to read the samples * @param buf Buffer where to read the samples
* @param offset The offset in the buffer where to read samples * @param offset The offset in the buffer where to read samples
* @param length The length inside the buffer where to read samples * @param length The length inside the buffer where to read samples
* @return number of bytes read. * @return number of bytes read.
*/ */
public int readSamples(byte[] buf, int offset, int length){ public int readSamples(byte[] buf, int offset, int length) {
if (!open) if (!open || eof) {
return -1; return -1;
}
try{ try {
return in.read(buf, offset, length); int totalRead = in.read(buf, offset, length);
}catch (IOException ex){ if (totalRead < 0) {
eof = true;
}
return totalRead;
} catch (IOException ex) {
eof = true;
return -1; return -1;
} }
} }
@ -103,41 +109,41 @@ public class AudioStream extends AudioData implements Closeable{
* @param buf Buffer where to read the samples * @param buf Buffer where to read the samples
* @return number of bytes read. * @return number of bytes read.
*/ */
public int readSamples(byte[] buf){ public int readSamples(byte[] buf) {
return readSamples(buf, 0, buf.length); return readSamples(buf, 0, buf.length);
} }
public float getDuration(){ public float getDuration() {
return duration; return duration;
} }
@Override @Override
public int getId(){ public int getId() {
throw new RuntimeException("Don't use getId() on streams"); throw new RuntimeException("Don't use getId() on streams");
} }
@Override @Override
public void setId(int id){ public void setId(int id) {
throw new RuntimeException("Don't use setId() on streams"); throw new RuntimeException("Don't use setId() on streams");
} }
public void initIds(int count){ public void initIds(int count) {
ids = new int[count]; ids = new int[count];
} }
public int getId(int index){ public int getId(int index) {
return ids[index]; return ids[index];
} }
public void setId(int index, int id){ public void setId(int index, int id) {
ids[index] = id; ids[index] = id;
} }
public int[] getIds(){ public int[] getIds() {
return ids; return ids;
} }
public void setIds(int[] ids){ public void setIds(int[] ids) {
this.ids = ids; this.ids = ids;
} }
@ -155,9 +161,7 @@ public class AudioStream extends AudioData implements Closeable{
@Override @Override
public void deleteObject(Object rendererObject) { public void deleteObject(Object rendererObject) {
// It seems that the audio renderer is already doing a good ((AudioRenderer) rendererObject).deleteAudioData(this);
// job at deleting audio streams when they finish playing.
// ((AudioRenderer)rendererObject).deleteAudioData(this);
} }
@Override @Override
@ -165,42 +169,42 @@ public class AudioStream extends AudioData implements Closeable{
return new AudioStream(ids); return new AudioStream(ids);
} }
/** public boolean isEOF() {
* @return Whether the stream is open or not. Reading from a closed return eof;
* stream will always return eof.
*/
public boolean isOpen(){
return open;
} }
/** /**
* Closes the stream, releasing all data relating to it. Reading * Closes the stream, releasing all data relating to it.
* from the stream will return eof. * Reading from the stream will return eof.
*
* @throws IOException * @throws IOException
*/ */
public void close() { public void close() {
if (in != null && open){ if (in != null && open) {
try{ try {
in.close(); in.close();
}catch (IOException ex){ } catch (IOException ex) {
} }
open = false; open = false;
}else{ } else {
throw new RuntimeException("AudioStream is already closed!"); throw new RuntimeException("AudioStream is already closed!");
} }
} }
public void setTime(float time) {
public void setTime(float time){ if (in instanceof SeekableStream) {
if(in instanceof SeekableStream){ ((SeekableStream) in).setTime(time);
((SeekableStream)in).setTime(time); eof = false;
}else{ } 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"); 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 @Override
public long getUniqueId() { public long getUniqueId() {
return ((long)OBJTYPE_AUDIOSTREAM << 32) | ((long)ids[0]); return ((long) OBJTYPE_AUDIOSTREAM << 32) | ((long) ids[0]);
} }
} }

@ -72,9 +72,10 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
private int auxSends = 0; private int auxSends = 0;
private int reverbFx = -1; private int reverbFx = -1;
private int reverbFxSlot = -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 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 AtomicBoolean threadLock = new AtomicBoolean(false);
private final AL al; private final AL al;
@ -88,23 +89,26 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
} }
public void initialize() { public void initialize() {
if (!audioThread.isAlive()) { if (!decoderThread.isAlive()) {
audioThread.setDaemon(true); // Set high priority to avoid buffer starvation.
audioThread.setPriority(Thread.NORM_PRIORITY + 1); decoderThread.setDaemon(true);
audioThread.start(); decoderThread.setPriority(Thread.NORM_PRIORITY + 1);
decoderThread.start();
} else { } else {
throw new IllegalStateException("Initialize already called"); throw new IllegalStateException("Initialize already called");
} }
} }
private void checkDead() { private void checkDead() {
if (audioThread.getState() == Thread.State.TERMINATED) { if (decoderThread.getState() == Thread.State.TERMINATED) {
throw new IllegalStateException("Audio thread is terminated"); throw new IllegalStateException("Decoding thread is terminated");
} }
} }
public void run() { public void run() {
initInThread(); initInDecoderThread();
// Notify render thread that OAL context is available.
synchronized (threadLock) { synchronized (threadLock) {
threadLock.set(true); threadLock.set(true);
threadLock.notifyAll(); threadLock.notifyAll();
@ -120,7 +124,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
} }
synchronized (threadLock) { synchronized (threadLock) {
updateInThread(UPDATE_RATE); updateInDecoderThread(UPDATE_RATE);
} }
long endTime = System.nanoTime(); long endTime = System.nanoTime();
@ -139,11 +143,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
} }
synchronized (threadLock) { synchronized (threadLock) {
cleanupInThread(); cleanupInDecoderThread();
} }
} }
public void initInThread() { public void initInDecoderThread() {
try { try {
if (!alc.isCreated()) { if (!alc.isCreated()) {
alc.createALC(); alc.createALC();
@ -225,7 +229,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
} }
} }
public void cleanupInThread() { public void cleanupInDecoderThread() {
if (audioDisabled) { if (audioDisabled) {
alc.destroyALC(); alc.destroyALC();
return; return;
@ -264,10 +268,10 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
public void cleanup() { public void cleanup() {
// kill audio thread // kill audio thread
if (audioThread.isAlive()) { if (decoderThread.isAlive()) {
audioThread.interrupt(); decoderThread.interrupt();
try { try {
audioThread.join(); decoderThread.join();
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
} }
} }
@ -453,10 +457,8 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
} }
break; break;
case Looping: case Looping:
if (src.isLooping()) { if (src.isLooping() && !(src.getAudioData() instanceof AudioStream)) {
if (!(src.getAudioData() instanceof AudioStream)) { al.alSourcei(id, AL_LOOPING, AL_TRUE);
al.alSourcei(id, AL_LOOPING, AL_TRUE);
}
} else { } else {
al.alSourcei(id, AL_LOOPING, AL_FALSE); 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); al.alSourcei(id, AL_LOOPING, AL_FALSE);
} else { } else {
al.alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); al.alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE);
@ -661,45 +663,71 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return true; return true;
} }
private boolean fillStreamingSource(int sourceId, AudioStream stream) { private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) {
if (!stream.isOpen()) { boolean success = false;
return false;
}
boolean active = true;
int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED); int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED);
// while((processed--) != 0){ for (int i = 0; i < processed; i++) {
if (processed > 0) {
int buffer; int buffer;
ib.position(0).limit(1); ib.position(0).limit(1);
al.alSourceUnqueueBuffers(sourceId, 1, ib); al.alSourceUnqueueBuffers(sourceId, 1, ib);
buffer = ib.get(0); buffer = ib.get(0);
active = fillBuffer(stream, buffer); boolean active = fillBuffer(stream, buffer);
ib.position(0).limit(1); if (!active && !stream.isEOF()) {
ib.put(0, buffer); throw new AssertionError();
al.alSourceQueueBuffers(sourceId, 1, ib); }
}
if (!active && looping) {
if (!active && stream.isOpen()) { stream.setTime(0);
stream.close(); 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) { private boolean attachStreamToSource(int sourceId, AudioStream stream, boolean looping) {
boolean active = true; 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()) { for (int id : stream.getIds()) {
active = fillBuffer(stream, id); boolean active = fillBuffer(stream, id);
ib.position(0).limit(1); if (!active && !stream.isEOF()) {
ib.put(id).flip(); throw new AssertionError();
al.alSourceQueueBuffers(sourceId, 1, ib); }
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) { private boolean attachBufferToSource(int sourceId, AudioBuffer buffer) {
@ -707,11 +735,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return true; return true;
} }
private boolean attachAudioToSource(int sourceId, AudioData data) { private boolean attachAudioToSource(int sourceId, AudioData data, boolean looping) {
if (data instanceof AudioBuffer) { if (data instanceof AudioBuffer) {
return attachBufferToSource(sourceId, (AudioBuffer) data); return attachBufferToSource(sourceId, (AudioBuffer) data);
} else if (data instanceof AudioStream) { } else if (data instanceof AudioStream) {
return attachStreamToSource(sourceId, (AudioStream) data); return attachStreamToSource(sourceId, (AudioStream) data, looping);
} }
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -723,15 +751,9 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
int sourceId = channels[index]; int sourceId = channels[index];
al.alSourceStop(sourceId); al.alSourceStop(sourceId);
if (src.getAudioData() instanceof AudioStream) { // For streaming sources, this will clear all queued buffers.
AudioStream str = (AudioStream) src.getAudioData(); al.alSourcei(sourceId, AL_BUFFER, 0);
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);
}
if (src.getDryFilter() != null && supportEfx) { if (src.getDryFilter() != null && supportEfx) {
// detach filter // detach filter
@ -747,71 +769,118 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
chanSrcs[index] = null; 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) { public void update(float tpf) {
// does nothing synchronized (threadLock) {
updateInRenderThread(tpf);
}
} }
public void updateInThread(float tpf) { public void updateInRenderThread(float tpf) {
if (audioDisabled) { if (audioDisabled) {
return; return;
} }
for (int i = 0; i < channels.length; i++) { for (int i = 0; i < channels.length; i++) {
AudioSource src = chanSrcs[i]; AudioSource src = chanSrcs[i];
if (src == null) { if (src == null) {
continue; continue;
} }
int sourceId = channels[i]; int sourceId = channels[i];
// is the source bound to this channel
// if false, it's an instanced playback
boolean boundSource = i == src.getChannel(); boolean boundSource = i == src.getChannel();
boolean reclaimChannel = false;
// source's data is streaming
boolean streaming = src.getAudioData() instanceof AudioStream; Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
Status jmeStatus = src.getStatus();
// only buffered sources can be bound
assert (boundSource && streaming) || (!streaming); // Check if we need to sync JME status with OAL status.
if (oalStatus != jmeStatus) {
int state = al.alGetSourcei(sourceId, AL_SOURCE_STATE); if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) {
boolean wantPlaying = src.getStatus() == Status.Playing; // Maybe we need to reclaim the channel.
boolean stopped = state == AL_STOPPED; if (src.getAudioData() instanceof AudioStream) {
AudioStream stream = (AudioStream) src.getAudioData();
if (streaming && wantPlaying) {
AudioStream stream = (AudioStream) src.getAudioData(); if (stream.isEOF() && !src.isLooping()) {
if (stream.isOpen()) { // Stream finished playing
fillStreamingSource(sourceId, stream); reclaimChannel = true;
if (stopped) { } else {
al.alSourcePlay(sourceId); // 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) { if (reclaimChannel) {
// became inactive if (boundSource) {
src.setStatus(Status.Stopped); src.setStatus(Status.Stopped);
src.setChannel(-1); src.setChannel(-1);
}
clearChannel(i); clearChannel(i);
freeChannel(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) { } else {
boolean paused = state == AL_PAUSED; // 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 for (int i = 0; i < channels.length; i++) {
assert (src.getStatus() == Status.Paused && paused) || (!paused); AudioSource src = chanSrcs[i];
if (src == null || !(src.getAudioData() instanceof AudioStream)) {
continue;
}
if (stopped) { int sourceId = channels[i];
if (boundSource) { AudioStream stream = (AudioStream) src.getAudioData();
src.setStatus(Status.Stopped);
src.setChannel(-1); Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
} Status jmeStatus = src.getStatus();
clearChannel(i);
freeChannel(i); // 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) { if (src.getAudioData() instanceof AudioStream) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Cannot play instances " "Cannot play instances "
+ "of audio streams. Use playSource() instead."); + "of audio streams. Use play() instead.");
} }
if (src.getAudioData().isUpdateNeeded()) { if (src.getAudioData().isUpdateNeeded()) {
@ -896,7 +965,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
// set parameters, like position and max distance // set parameters, like position and max distance
setSourceParams(sourceId, src, true); setSourceParams(sourceId, src, true);
attachAudioToSource(sourceId, src.getAudioData()); attachAudioToSource(sourceId, src.getAudioData(), false);
chanSrcs[index] = src; chanSrcs[index] = src;
// play the channel // play the channel
@ -917,12 +986,11 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
return; return;
} }
//assert src.getStatus() == Status.Stopped || src.getChannel() == -1;
if (src.getStatus() == Status.Playing) { if (src.getStatus() == Status.Playing) {
return; return;
} else if (src.getStatus() == Status.Stopped) { } else if (src.getStatus() == Status.Stopped) {
assert src.getChannel() != -1;
// allocate channel to this source // allocate channel to this source
int index = newChannel(); int index = newChannel();
if (index == -1) { if (index == -1) {
@ -939,7 +1007,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
chanSrcs[index] = src; chanSrcs[index] = src;
setSourceParams(channels[index], src, false); setSourceParams(channels[index], src, false);
attachAudioToSource(channels[index], data); attachAudioToSource(channels[index], data, src.isLooping());
} }
al.alSourcePlay(channels[src.getChannel()]); al.alSourcePlay(channels[src.getChannel()]);
@ -989,16 +1057,9 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
src.setChannel(-1); src.setChannel(-1);
clearChannel(chan); clearChannel(chan);
freeChannel(chan); freeChannel(chan);
if (src.getAudioData() instanceof AudioStream) { if (src.getAudioData() instanceof AudioStream) {
AudioStream stream = (AudioStream) src.getAudioData(); ((AudioStream)src.getAudioData()).setTime(0);
if (stream.isOpen()) {
stream.close();
}
// And free the audio since it cannot be
// played again anyway.
deleteAudioData(src.getAudioData());
} }
} }
} }

Loading…
Cancel
Save