package de.lessvoid.nifty.sound.openal.slick; import de.lessvoid.nifty.tools.resourceloader.NiftyResourceLoader; import org.lwjgl.BufferUtils; import org.lwjgl.openal.AL10; import org.lwjgl.openal.AL11; import org.lwjgl.openal.OpenALException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.logging.Logger; /** * 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 { private final Logger log = Logger.getLogger(OpenALStreamPlayer.class.getName()); /** * The number of buffers to maintain */ public static final int BUFFER_COUNT = 3; /** * The size of the sections to stream from the stream */ private static final int sectionSize = 4096 * 20; /** * The buffer read from the data stream */ @Nonnull private final byte[] buffer = new byte[sectionSize]; /** * Holds the OpenAL buffer names */ private final IntBuffer bufferNames; /** * The byte buffer passed to OpenAL containing the section */ private final ByteBuffer bufferData = BufferUtils.createByteBuffer(sectionSize); /** * The buffer holding the names of the OpenAL buffer thats been fully played back */ private final IntBuffer unqueued = BufferUtils.createIntBuffer(1); /** * The source we're playing back on */ private final 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 play back */ private boolean done = true; /** * The stream we're currently reading from */ @Nullable 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; private final NiftyResourceLoader resourceLoader; /** * 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, NiftyResourceLoader resourceLoader) { this.source = source; this.ref = ref; this.resourceLoader = resourceLoader; 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, NiftyResourceLoader resourceLoader) { this.source = source; this.url = url; this.resourceLoader = resourceLoader; 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(); } OggInputStream audio; InputStream in; if (url != null) { in = url.openStream(); } else { in = resourceLoader.getResourceAsStream(ref); } if (in != null) { audio = new OggInputStream(in); this.audio = audio; positionOffset = 0; } else { throw new IOException("Input not found."); } } /** * 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 void removeBuffers() { IntBuffer buffer = BufferUtils.createIntBuffer(1); int queued = AL10.alGetSourcei(source, AL10.AL_BUFFERS_QUEUED); while (queued > 0) { AL10.alSourceUnqueueBuffers(source, buffer); queued--; } } /** * Start this stream playing * * @param loop True if the stream should loop * @throws IOException Indicates a failure to read from the stream */ public void play(boolean loop) throws IOException { this.loop = loop; initStreams(); done = false; AL10.alSourceStop(source); removeBuffers(); startPlayback(); } /** * 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. * <p/> * Most of the time this should be reasonably quick */ public void update() { if (done || audio == null) { return; } float sampleRate = audio.getRate(); float sampleSize; if (audio.getChannels() > 1) { sampleSize = 4; // AL10.AL_FORMAT_STEREO16 } else { sampleSize = 2; // AL10.AL_FORMAT_MONO16 } int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED); while (processed > 0) { unqueued.clear(); AL10.alSourceUnqueueBuffers(source, unqueued); int bufferIndex = unqueued.get(0); float bufferLength = (AL10.alGetBufferi(bufferIndex, AL10.AL_SIZE) / sampleSize) / sampleRate; positionOffset += bufferLength; 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 boolean stream(int bufferId) { if (audio == null) { return false; } try { int count = audio.read(buffer); if (count != -1) { 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.warning("Failed to loop buffer: " + bufferId + " " + format + " " + count + " " + audio.getRate() + e .toString()); return false; } } else { if (loop) { initStreams(); stream(bufferId); } else { done = true; return false; } } return true; } catch (IOException e) { log.warning(e.toString()); 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 boolean setPosition(float position) { if (audio == null) { return false; } try { if (getPosition() > position) { initStreams(); } float sampleRate = audio.getRate(); float sampleSize; if (audio.getChannels() > 1) { sampleSize = 4; // AL10.AL_FORMAT_STEREO16 } else { sampleSize = 2; // AL10.AL_FORMAT_MONO16 } while (positionOffset < position) { int count = audio.read(buffer); if (count != -1) { float bufferLength = (count / sampleSize) / sampleRate; positionOffset += bufferLength; } else { if (loop) { initStreams(); } else { done = true; } return false; } } startPlayback(); return true; } catch (IOException e) { log.warning(e.toString()); return false; } } /** * Starts the streaming. */ private void startPlayback() { 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 getPosition() { return positionOffset + AL10.alGetSourcef(source, AL11.AL_SEC_OFFSET); } }