package ibxm; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.IntBuffer; import org.lwjgl.BufferUtils; import org.lwjgl.LWJGLException; import org.lwjgl.openal.AL; import org.lwjgl.openal.AL10; /** * A streaming mod/xm play back system * * @author Kevin Glass */ public class OpenALMODPlayer { /** The size of the sections to stream from the mod file */ private static final int sectionSize = 4096 * 10; /** Holds the OpenAL buffer names */ private IntBuffer bufferNames; /** The IBXM reference */ private IBXM ibxm; /** The length of the track in frames */ private int songDuration; /** The data read for this section */ private byte[] data = new byte[sectionSize * 4]; /** The byte buffer passed to OpenAL containing the section */ private ByteBuffer bufferData = BufferUtils.createByteBuffer(sectionSize * 4); /** 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; /** True if sound works */ private boolean soundWorks = true; /** The module being played */ private Module module; /** True if we should loop the track */ private boolean loop; /** True if we've completed play back */ private boolean done = true; /** The number of buffers remaining to be played back */ private int remainingBufferCount; /** * Initialise OpenAL LWJGL styley */ public void init() { try { AL.create(); soundWorks = true; } catch (LWJGLException e) { System.err.println("Failed to initialise LWJGL OpenAL"); soundWorks = false; return; } if (soundWorks) { IntBuffer sources = BufferUtils.createIntBuffer(1); AL10.alGenSources(sources); if (AL10.alGetError() != AL10.AL_NO_ERROR) { System.err.println("Failed to create sources"); soundWorks = false; } else { source = sources.get(0); } } } /** * Play a mod or xm track streamed from the specified location * * @param in The input stream to read the music from * @param loop True if the track should be looped * @param start True if the music should be started * @throws IOException The input stream to read the music from */ public void play(InputStream in, boolean loop, boolean start) throws IOException { play(source, in, loop, start); } /** * Play a mod or xm track streamed from the specified location * * @param source The OpenAL source to play the music on * @param in The input stream to read the music from * @param loop True if the track should be looped * @param start True if the music should be started * @throws IOException The input stream to read the music from */ public void play(int source, InputStream in, boolean loop, boolean start) throws IOException { if (!soundWorks) { return; } done = false; this.loop = loop; this.source = source; module = loadModule(in); play(module, source, loop, start); } /** * Play a mod or xm track streamed from the specified location * * @param module The moudle to play back * @param source The OpenAL source to play the music on * @param start True if the music should be started * @param loop True if the track should be looped */ public void play(Module module, int source, boolean loop, boolean start) { this.source = source; this.loop = loop; this.module = module; done = false; ibxm = new IBXM(48000); ibxm.set_module(module); songDuration = ibxm.calculate_song_duration(); if (bufferNames != null) { AL10.alSourceStop(source); bufferNames.flip(); AL10.alDeleteBuffers(bufferNames); } bufferNames = BufferUtils.createIntBuffer(2); AL10.alGenBuffers(bufferNames); remainingBufferCount = 2; for (int i=0;i<2;i++) { stream(bufferNames.get(i)); } AL10.alSourceQueueBuffers(source, bufferNames); AL10.alSourcef(source, AL10.AL_PITCH, 1.0f); AL10.alSourcef(source, AL10.AL_GAIN, 1.0f); if (start) { AL10.alSourcePlay(source); } } /** * Setup the playback properties * * @param pitch The pitch to play back at * @param gain The volume to play back at */ public void setup(float pitch, float gain) { AL10.alSourcef(source, AL10.AL_PITCH, pitch); AL10.alSourcef(source, AL10.AL_GAIN, gain); } /** * 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; } /** * Load a module using the IBXM * * @param in The input stream to read the module from * @return The module loaded * @throws IOException Indicates a failure to access the module */ public static Module loadModule(InputStream in) throws IOException { Module module; DataInputStream din; byte[] xm_header, s3m_header, mod_header, output_buffer; int frames; din = new DataInputStream(in); module = null; xm_header = new byte[ 60 ]; din.readFully( xm_header ); if( FastTracker2.is_xm( xm_header ) ) { module = FastTracker2.load_xm( xm_header, din ); } else { s3m_header = new byte[ 96 ]; System.arraycopy( xm_header, 0, s3m_header, 0, 60 ); din.readFully( s3m_header, 60, 36 ); if( ScreamTracker3.is_s3m( s3m_header ) ) { module = ScreamTracker3.load_s3m( s3m_header, din ); } else { mod_header = new byte[ 1084 ]; System.arraycopy( s3m_header, 0, mod_header, 0, 96 ); din.readFully( mod_header, 96, 988 ); module = ProTracker.load_mod( mod_header, din ); } } din.close(); return module; } /** * Poll the bufferNames - check if we need to fill the bufferNames with another * section. * * Most of the time this should be reasonably quick */ public void update() { if (done) { return; } int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED); while (processed > 0) { unqueued.clear(); AL10.alSourceUnqueueBuffers(source, unqueued); if (stream(unqueued.get(0))) { 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 one section from the mod/xm into an OpenAL buffer * * @param bufferId The ID of the buffer to fill * @return True if another section was available */ public boolean stream(int bufferId) { int frames = sectionSize; boolean reset = false; boolean more = true; if (frames > songDuration) { frames = songDuration; reset = true; } ibxm.get_audio(data, frames); bufferData.clear(); bufferData.put(data); bufferData.limit(frames * 4); if (reset) { if (loop) { ibxm.seek(0); ibxm.set_module(module); songDuration = ibxm.calculate_song_duration(); } else { more = false; songDuration -= frames; } } else { songDuration -= frames; } bufferData.flip(); AL10.alBufferData(bufferId, AL10.AL_FORMAT_STEREO16, bufferData, 48000); return more; } }