463 lines
12 KiB
Java
463 lines
12 KiB
Java
/*
|
|
* Copyright (c) 2013, Slick2D
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
* - Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
* - 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.
|
|
* - Neither the name of the Slick2D 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 OWNER 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 org.newdawn.slick.openal;
|
|
|
|
import java.io.IOException;
|
|
import java.net.URL;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.IntBuffer;
|
|
|
|
import org.lwjgl.BufferUtils;
|
|
import org.lwjgl.Sys;
|
|
import org.lwjgl.openal.AL10;
|
|
import org.lwjgl.openal.AL11;
|
|
import org.lwjgl.openal.OpenALException;
|
|
import org.newdawn.slick.util.Log;
|
|
import org.newdawn.slick.util.ResourceLoader;
|
|
|
|
/**
|
|
* A generic tool to work on a supplied stream, pulling out PCM data and buffered it to OpenAL
|
|
* as required.
|
|
*
|
|
* @author Kevin Glass
|
|
* @author Nathan Sweet <misc@n4te.com>
|
|
* @author Rockstar play and setPosition cleanup
|
|
*/
|
|
public class OpenALStreamPlayer {
|
|
/** The number of buffers to maintain */
|
|
public static final int BUFFER_COUNT = 20; // 3
|
|
/** The size of the sections to stream from the stream */
|
|
private static final int sectionSize = 4096; // 4096 * 20
|
|
|
|
/** The buffer read from the data stream */
|
|
private byte[] buffer = new byte[sectionSize];
|
|
/** Holds the OpenAL buffer names */
|
|
private IntBuffer bufferNames;
|
|
/** The byte buffer passed to OpenAL containing the section */
|
|
private ByteBuffer bufferData = BufferUtils.createByteBuffer(sectionSize);
|
|
/** The buffer holding the names of the OpenAL buffer thats been fully played back */
|
|
private IntBuffer unqueued = BufferUtils.createIntBuffer(1);
|
|
/** The source we're playing back on */
|
|
private int source;
|
|
/** The number of buffers remaining */
|
|
private int remainingBufferCount;
|
|
/** True if we should loop the track */
|
|
private boolean loop;
|
|
/** True if we've completed streaming to buffer (but may not be done playing) */
|
|
private boolean done = true;
|
|
/** The stream we're currently reading from */
|
|
private AudioInputStream audio;
|
|
/** The source of the data */
|
|
private String ref;
|
|
/** The source of the data */
|
|
private URL url;
|
|
/** The pitch of the music */
|
|
private float pitch;
|
|
/** Position in seconds of the previously played buffers */
|
|
// private float positionOffset;
|
|
|
|
/** The stream position. */
|
|
long streamPos = 0;
|
|
|
|
/** The sample rate. */
|
|
int sampleRate;
|
|
|
|
/** The sample size. */
|
|
int sampleSize;
|
|
|
|
/** The play position. */
|
|
long playedPos;
|
|
|
|
/** The music length. */
|
|
long musicLength = -1;
|
|
|
|
/** The assumed time of when the music position would be 0. */
|
|
long syncStartTime;
|
|
|
|
/** The last value that was returned for the music position. */
|
|
float lastUpdatePosition = 0;
|
|
|
|
/** The average difference between the sync time and the music position. */
|
|
float avgDiff;
|
|
|
|
/** The time when the music was paused. */
|
|
long pauseTime;
|
|
|
|
/**
|
|
* Create a new player to work on an audio stream
|
|
*
|
|
* @param source The source on which we'll play the audio
|
|
* @param ref A reference to the audio file to stream
|
|
*/
|
|
public OpenALStreamPlayer(int source, String ref) {
|
|
this.source = source;
|
|
this.ref = ref;
|
|
|
|
bufferNames = BufferUtils.createIntBuffer(BUFFER_COUNT);
|
|
AL10.alGenBuffers(bufferNames);
|
|
}
|
|
|
|
/**
|
|
* Create a new player to work on an audio stream
|
|
*
|
|
* @param source The source on which we'll play the audio
|
|
* @param url A reference to the audio file to stream
|
|
*/
|
|
public OpenALStreamPlayer(int source, URL url) {
|
|
this.source = source;
|
|
this.url = url;
|
|
|
|
bufferNames = BufferUtils.createIntBuffer(BUFFER_COUNT);
|
|
AL10.alGenBuffers(bufferNames);
|
|
}
|
|
|
|
/**
|
|
* Initialise our connection to the underlying resource
|
|
*
|
|
* @throws IOException Indicates a failure to open the underling resource
|
|
*/
|
|
private void initStreams() throws IOException {
|
|
if (audio != null) {
|
|
audio.close();
|
|
}
|
|
|
|
AudioInputStream audio;
|
|
|
|
if (url != null) {
|
|
audio = new OggInputStream(url.openStream());
|
|
} else {
|
|
if (ref.toLowerCase().endsWith(".mp3"))
|
|
audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref));
|
|
else
|
|
audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
|
|
|
|
if (audio.getRate() == 0 && audio.getChannels() == 0) {
|
|
if (ref.toLowerCase().endsWith(".mp3"))
|
|
audio = new OggInputStream(ResourceLoader.getResourceAsStream(ref));
|
|
else
|
|
audio = new Mp3InputStream(ResourceLoader.getResourceAsStream(ref));
|
|
}
|
|
}
|
|
|
|
this.audio = audio;
|
|
sampleRate = audio.getRate();
|
|
if (audio.getChannels() > 1)
|
|
sampleSize = 4; // AL10.AL_FORMAT_STEREO16
|
|
else
|
|
sampleSize = 2; // AL10.AL_FORMAT_MONO16
|
|
// positionOffset = 0;
|
|
streamPos = 0;
|
|
playedPos = 0;
|
|
|
|
}
|
|
|
|
/**
|
|
* Get the source of this stream
|
|
*
|
|
* @return The name of the source of string
|
|
*/
|
|
public String getSource() {
|
|
return (url == null) ? ref : url.toString();
|
|
}
|
|
|
|
/**
|
|
* Clean up the buffers applied to the sound source
|
|
*/
|
|
private synchronized void removeBuffers() {
|
|
AL10.alSourceStop(source);
|
|
IntBuffer buffer = BufferUtils.createIntBuffer(1);
|
|
|
|
while (AL10.alGetSourcei(source, AL10.AL_BUFFERS_QUEUED) > 0) {
|
|
AL10.alSourceUnqueueBuffers(source, buffer);
|
|
buffer.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start this stream playing
|
|
*
|
|
* @param loop True if the stream should loop
|
|
* @throws IOException Indicates a failure to read from the stream
|
|
*/
|
|
public synchronized void play(boolean loop) throws IOException {
|
|
this.loop = loop;
|
|
initStreams();
|
|
|
|
done = false;
|
|
|
|
AL10.alSourceStop(source);
|
|
|
|
startPlayback();
|
|
syncStartTime = getTime();
|
|
}
|
|
|
|
/**
|
|
* Setup the playback properties
|
|
*
|
|
* @param pitch The pitch to play back at
|
|
*/
|
|
public void setup(float pitch) {
|
|
this.pitch = pitch;
|
|
}
|
|
|
|
/**
|
|
* Check if the playback is complete. Note this will never
|
|
* return true if we're looping
|
|
*
|
|
* @return True if we're looping
|
|
*/
|
|
public boolean done() {
|
|
return done;
|
|
}
|
|
|
|
/**
|
|
* Poll the bufferNames - check if we need to fill the bufferNames with another
|
|
* section.
|
|
*
|
|
* Most of the time this should be reasonably quick
|
|
*/
|
|
public synchronized void update() {
|
|
if (done) {
|
|
return;
|
|
}
|
|
|
|
int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED);
|
|
while (processed > 0) {
|
|
unqueued.clear();
|
|
AL10.alSourceUnqueueBuffers(source, unqueued);
|
|
|
|
int bufferIndex = unqueued.get(0);
|
|
|
|
int bufferLength = AL10.alGetBufferi(bufferIndex, AL10.AL_SIZE);
|
|
|
|
playedPos += bufferLength;
|
|
|
|
if (musicLength > 0 && playedPos > musicLength)
|
|
playedPos -= musicLength;
|
|
|
|
if (stream(bufferIndex)) {
|
|
AL10.alSourceQueueBuffers(source, unqueued);
|
|
} else {
|
|
remainingBufferCount--;
|
|
if (remainingBufferCount == 0) {
|
|
done = true;
|
|
}
|
|
}
|
|
processed--;
|
|
}
|
|
|
|
int state = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE);
|
|
|
|
if (state != AL10.AL_PLAYING) {
|
|
AL10.alSourcePlay(source);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stream some data from the audio stream to the buffer indicates by the ID
|
|
*
|
|
* @param bufferId The ID of the buffer to fill
|
|
* @return True if another section was available
|
|
*/
|
|
public synchronized boolean stream(int bufferId) {
|
|
try {
|
|
int count = audio.read(buffer);
|
|
if (count != -1) {
|
|
streamPos += count;
|
|
|
|
bufferData.clear();
|
|
bufferData.put(buffer,0,count);
|
|
bufferData.flip();
|
|
|
|
int format = audio.getChannels() > 1 ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16;
|
|
try {
|
|
AL10.alBufferData(bufferId, format, bufferData, audio.getRate());
|
|
} catch (OpenALException e) {
|
|
Log.error("Failed to loop buffer: "+bufferId+" "+format+" "+count+" "+audio.getRate(), e);
|
|
return false;
|
|
}
|
|
} else {
|
|
if (loop) {
|
|
musicLength = streamPos;
|
|
initStreams();
|
|
stream(bufferId);
|
|
} else {
|
|
done = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (IOException e) {
|
|
Log.error(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seeks to a position in the music.
|
|
*
|
|
* @param position Position in seconds.
|
|
* @return True if the setting of the position was successful
|
|
*/
|
|
public synchronized boolean setPosition(float position) {
|
|
try {
|
|
long samplePos = (long) (position * sampleRate) * sampleSize;
|
|
|
|
if (streamPos > samplePos)
|
|
initStreams();
|
|
|
|
long skipped = audio.skip(samplePos - streamPos);
|
|
if (skipped >= 0)
|
|
streamPos += skipped;
|
|
else
|
|
Log.warn("OpenALStreamPlayer: setPosition: failed to skip.");
|
|
|
|
while (streamPos + buffer.length < samplePos) {
|
|
int count = audio.read(buffer);
|
|
if (count != -1) {
|
|
streamPos += count;
|
|
} else {
|
|
if (loop) {
|
|
initStreams();
|
|
} else {
|
|
done = true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
playedPos = streamPos;
|
|
syncStartTime = getTime() - playedPos * 1000 / sampleSize / sampleRate;
|
|
|
|
startPlayback();
|
|
|
|
return true;
|
|
} catch (IOException e) {
|
|
Log.error(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the streaming.
|
|
*/
|
|
private void startPlayback() {
|
|
removeBuffers();
|
|
AL10.alSourcei(source, AL10.AL_LOOPING, AL10.AL_FALSE);
|
|
AL10.alSourcef(source, AL10.AL_PITCH, pitch);
|
|
|
|
remainingBufferCount = BUFFER_COUNT;
|
|
|
|
for (int i = 0; i < BUFFER_COUNT; i++) {
|
|
stream(bufferNames.get(i));
|
|
}
|
|
|
|
AL10.alSourceQueueBuffers(source, bufferNames);
|
|
AL10.alSourcePlay(source);
|
|
}
|
|
|
|
/**
|
|
* Return the current playing position in the sound
|
|
*
|
|
* @return The current position in seconds.
|
|
*/
|
|
public float getALPosition() {
|
|
float playedTime = ((float) playedPos / (float) sampleSize) / sampleRate;
|
|
float timePosition = playedTime + AL10.alGetSourcef(source, AL11.AL_SEC_OFFSET);
|
|
return timePosition;
|
|
}
|
|
|
|
/**
|
|
* Return the current playing position in the sound
|
|
*
|
|
* @return The current position in seconds.
|
|
*/
|
|
public float getPosition() {
|
|
float thisPosition = getALPosition();
|
|
long thisTime = getTime();
|
|
float dxPosition = thisPosition - lastUpdatePosition;
|
|
long dxTime = thisTime - syncStartTime;
|
|
|
|
// hard reset
|
|
if (Math.abs(thisPosition - dxTime / 1000f) > 1 / 2f) {
|
|
syncStartTime = thisTime - ((long) (thisPosition * 1000));
|
|
dxTime = thisTime - syncStartTime;
|
|
avgDiff = 0;
|
|
}
|
|
if ((int) (dxPosition * 1000) != 0) { // lastPosition != thisPosition
|
|
float diff = thisPosition * 1000 - (dxTime);
|
|
avgDiff = (diff + avgDiff * 9) / 10;
|
|
syncStartTime -= (int) (avgDiff/2);
|
|
dxTime = thisTime - syncStartTime;
|
|
lastUpdatePosition = thisPosition;
|
|
}
|
|
|
|
return dxTime / 1000f;
|
|
}
|
|
|
|
/**
|
|
* Processes a track pause.
|
|
*/
|
|
public void pausing() {
|
|
pauseTime = getTime();
|
|
}
|
|
|
|
/**
|
|
* Processes a track resume.
|
|
*/
|
|
public void resuming() {
|
|
syncStartTime += getTime() - pauseTime;
|
|
}
|
|
|
|
/**
|
|
* http://wiki.lwjgl.org/index.php?title=LWJGL_Basics_4_%28Timing%29
|
|
* Get the time in milliseconds
|
|
*
|
|
* @return The system time in milliseconds
|
|
*/
|
|
public long getTime() {
|
|
return (Sys.getTime() * 1000) / Sys.getTimerResolution();
|
|
}
|
|
|
|
/**
|
|
* Closes the stream.
|
|
*/
|
|
public void close() {
|
|
if (audio != null) {
|
|
try {
|
|
audio.close();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|