LWJGL streaming sound with OpenAL

Streaming might not necessarily mean playing sound from an internet source, you could for example be creating it with an algorithm, you probably don’t know the whole out come of the algorithm (it might depend on real time values) so you must stream the audio data to the output as you get it…

I’ll hold up my hand and admit at first I found OpenAL a little opaque, that said once you figure it out, its amazing how simple it looks!

OpenAL isolates different devices into context’s once you create one it becomes the current context which when created without parameters seems to do a good job of picking the systems default sound device.  Each context can have a number of sound sources, which are automagically™ mixed together, however we’re just using one as we are manually synthesising our sound! In order to hand off data to OpenAL we must provide it in an appropriate native buffer.

ALContext alContext = ALContext.create();

// a sound maker
int source = alGenSources();

// hold new data for transfer to AL
ByteBuffer pcm = BufferUtils.createByteBuffer(11025); // about 1/4 second at 44.1khz

 

Handily you can queue up buffers of sound data to play one after the other, here we’re using just 3, at the worst possible case we could be filling one while another is playing and the remaining buffer is sat ready to provide seamless play back…

// throw 3 chunks of sound into a queue
for (int i=0;i<3;i++) {
  ang = fillBuffer(ang, pcm);
  int b = alGenBuffers();
  alBufferData(b, AL_FORMAT_MONO8, pcm, 44100);
  alSourceQueueBuffers(source, b);
}

System.out.println("Playing sound ...");
alSourcePlay(source);

 

The main meat of the example is in this while loop, first of all we ask OpenAL how many sources have been processed, we then start another loop to reuse each processed buffer.

while (!done) {
  // buffers processed since last asked
  int p = alGetSourcei(source, AL_BUFFERS_PROCESSED);
  while(p!=0) {
    elapsed++;
    if (elapsed>15) break; // stop adding to queue

 

First we remove (unqueue) 1 processed buffer from the source, this returns the “handle” of the buffer we’ve removed from the queue.  Once we’ve filled up this spent buffer with new data we can transfer that data to OpenAL and requeue the buffer – finally we just need to check again to see if any more buffers were processed (there could be more than one)

    // at least 1 buffer had played so remove it from the queue
    int b = alSourceUnqueueBuffers(source);
    // refill the byte buffer with the next chunk of sound
    ang = fillBuffer(ang, pcm);
    // send the buffer to AL
    alBufferData(b, AL_FORMAT_MONO8, pcm, 44100);
    // requeue the buffer
    alSourceQueueBuffers(source, b);
    // check if any more buffers have played
    p = alGetSourcei(source, AL_BUFFERS_PROCESSED);
  }

 

between polling for spent buffers its good practice to sleep – more practically you might have the check in its own thread kicked off by a timer every 20ms or so.

In this example after filling the buffers up so many times I stop doing it, this leads the sound source to stop playing, once the queue is fully processed we fall out of the main loop…

 
  Thread.sleep(20); // lets not hog the cpu... (we're not some crappy AAA game!)
 
  // just to show if we stop filling buffers it stops!
  if (alGetSourcei(source, AL_SOURCE_STATE)==AL_STOPPED) { 
    done = true;
    System.out.println("source stopped ");
  }
}

 

Of no consequence to the technique of streaming to OpenAL but of some interest here is the routine the example is using to fill the buffers, you might be grabbing packets from the internet or modulating an existing waveform with some users input (like a mouse or joystick)

public static float fillBuffer(float ang, ByteBuffer buff) {
  int size = buff.capacity();
  trig++;
  for (int i=0;i<size;i++) {
 
    int source1 = (int)(Math.sin(ang)*127+128);
    int source2 = 0;
 
    if (trig>3) source2 = (int)(Math.sin(ang*3)*127+128);
    if (trig>4) trig=0;
 
    buff.put(i,(byte)((source1+source2)/2));

    ang+=0.1f;
  }
  return ang; 
}

ang (or angle) is returned so it can be passed back to the function later so as to have a continuous unbroken progression of values.  Every now and then we are mixing in another waveform – really just to prove its “live” date we’re hearing and not just a short loop

You can get the complete example here

Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *