OpenAL Tutorials |
Ogg/Vorbis Streaming
Lesson 8
Launch the Demo via Java Web Start (not working yet)
This is a translation of OpenAL Lesson 8: OggVorbis Streaming Using The Source Queue tutorial from DevMaster.net to JOAL.
"This software is based on or using the J-Ogg library available from http://www.j-ogg.de and copyrighted by Tor-Einar Jarnbjo."
Ever heard of Ogg? There's more to it than a funny sounding name. It's the biggest thing to happen for audio compression since mp3's (also typically used for music). Hopefully, one day, it will replace mp3's as the mainstream standard for compressed audio. Is it better than mp3? That is a question that is a little more difficult to answer. It's a pretty strong debate in some crowds. There are various arguments about compression ratio vs. sound quality which can sometimes get really cumbersome to read through. I personally don't have any opinion on which is "better". I feel the evidence in either case is arguable and not worth mentioning. But for me the fact that Ogg is royalty free (which mp3 is not) wins the argument hands down. The mp3 licensing fee is by no means steep for developers with deep pockets, but as an independent working on a project in your spare time and on minimal resources, shelling out a few grand in fees is not an option. Ogg may be the answer to your prayers.
Without further ado let's get to some code.
This tutorial will be written in Java and has two main classes: OggDecoder and OggStreamer. The OggDecoder class is wrapper on the J-Ogg library to decode an Ogg/Vorbis stream and will not be described in this tutorial. OggStreamer which is the main class doing most of the working for streaming using OpenAL is explained below.
// The size of a chunk from the stream that we want to read for each update. private static int BUFFER_SIZE = 4096*8; // The number of buffers used in the audio pipeline private static int NUM_BUFFERS = 2;
'BUFFER_SIZE' defines how big a chunk we want to read from the stream on each update. You will find (with a little experimentation) that larger buffers usually produce better sound quality since they don't update as often, and will generally avoid any abrupt pauses or sound distortions. Of course making your buffer too big will also eat up more memory. Making a stream redundant. I beleive 4096 is the minimum buffer size one can have. I don't recommend using one that small. I tried, and it caused many clicks.
So why should we even bother with streaming? Why not load the whole file into a buffer and then play it? Well, that is a good question. The quick answer is that there is too much audio data. Even though the actual Ogg file size is quite small (usually around 1-3 MB) you must remember that is compressed audio data. You cannot play the compressed form of the data. It must be decompressed and formatted into a form OpenAL recognizes before it can be used in a buffer. That is why we stream the file.
/**
* The main loop to initialize and play the entire stream
*/
public boolean playstream() { ... }
/**
* Open the Ogg/Vorbis stream and initialize OpenAL based
* on the stream properties
*/
public boolean open() { ... }
/**
* OpenAL cleanup
*/
public void release() { ... }
/**
* Play the Ogg stream
*/
public boolean playback() { ... }
/**
* Check if the source is playing
*/
public boolean playing() { ... }
/**
* Update the stream if necessary
*/
public boolean update() { ... }
/**
* Reloads a buffer (reads in the next chunk)
*/
public boolean stream(int buffer) { ... }
/**
* Empties the queue
*/
protected void empty() { ... }
This will be the base of our Ogg streaming api. The public methods are everything that one needs to actually get the Ogg to play. Protected methods are more internal procedures. I won't go over each function just yet. I believe my comments should give you an idea of what they're for.
// Buffers hold sound data. There are two of them by default (front/back) private int[] buffers = new int[NUM_BUFFERS]; // Sources are points emitting sound. private int[] source = new int[1];
First thing that I want to point out is that we have 2 buffers dedicated to the stream rather than the 1 we have always used for wav files. This is important. To understand this I want you to think about how double buffering works in OpenGL/DirectX. There is a front buffer that is "on screen" at any given second, while a back buffer is being drawn to. Then they are swapped. The back buffer becomes the front and vice versa. Pretty much the same principle is applied here. There is a buffer being played and one waiting to be played. When the buffer being played has finished the next one starts. While the next buffer is being played, the first one is refilled with a new chunk of data from the stream and is set to play once the one playing is finished. Confused yet? I'll explain this in more detail later on.
public boolean open() {
oggDecoder = new OggDecoder(url);
if (!oggDecoder.initialize()) {
System.err.println("Error initializing ogg stream...");
return false;
}
if (oggDecoder.numChannels() == 1)
format = AL.AL_FORMAT_MONO16;
else
format = AL.AL_FORMAT_STEREO16;
rate = oggDecoder.sampleRate();
...
}
This creates a decoder for Ogg file, initializes it and grabs some information on the file. We extract the OpenAL format enumerator based on how many channels are in the Ogg and then make a not of the sample rate.
public boolean open() {
...
al.alGenBuffers(NUM_BUFFERS, buffers, 0); check();
al.alGenSources(1, source, 0); check();
al.alSourcefv(source[0], AL.AL_POSITION , sourcePos, 0);
al.alSourcefv(source[0], AL.AL_VELOCITY , sourceVel, 0);
al.alSourcefv(source[0], AL.AL_DIRECTION, sourceDir, 0);
al.alSourcef(source[0], AL.AL_ROLLOFF_FACTOR, 0.0f );
al.alSourcei(source[0], AL.AL_SOURCE_RELATIVE, AL.AL_TRUE);
...
}
You've seen most of this before. We set a bunch of default values, position, velocity, direction... But what is rolloff factor? This has to do with attenuation. I will cover attenuation in a later article so I won't go too in-depth, but I will explain it basically. Rolloff factor judges the strength of attenuation over distance. By setting it to 0 we will have turned it off. This means that no matter how far away the Listener is to the source of the Ogg they will still hear it. The same idea applies to source relativity.
public void release() { al.alSourceStop(source[0]); empty(); for (int i = 0; i < NUM_BUFFERS; i++) { al.alDeleteSources(i, source, 0); check(); } }
We can clean up after ourselves using this. We stop the source, empty out any buffers that are still in the queue, and destroy our objects.
public boolean playback() { if (playing()) return true; for (int i = 0; i < NUM_BUFFERS; i++) { if (!stream(buffers[i])) return false; } al.alSourceQueueBuffers(source[0], NUM_BUFFERS, buffers, 0); al.alSourcePlay(source[0]); return true; }
This will start playing the Ogg. If the Ogg is already playing then there is no reason to do it again. We must also initialize the buffers with their first data set. We then queue them and tell the source to play them. This is the first time we have used 'alSourceQueueBuffers'. What it does basically is give the source multiple buffers. These buffers will be played sequentially. I will explain more on this along with the source queue momentarily. One thing to make a note of though: if you are using a source for streaming never bind a buffer to it using 'alSourcei'. Always use 'alSourceQueueBuffers' consistently.
public boolean playing() {
int[] state = new int[1];
al.alGetSourcei(source[0], AL.AL_SOURCE_STATE, state, 0);
return (state[0] == AL.AL_PLAYING);
}
This simplifies the task of checking the state of the source.
public boolean update() {
int[] processed = new int[1];
boolean active = true;
al.alGetSourcei(source[0], AL.AL_BUFFERS_PROCESSED, processed, 0);
while (processed[0] > 0)
{
int[] buffer = new int[1];
al.alSourceUnqueueBuffers(source[0], 1, buffer, 0); check();
active = stream(buffer[0]);
al.alSourceQueueBuffers(source[0], 1, buffer, 0); check();
processed[0]--;
}
return active;
}
Here is how the queue works in a nutshell: There is a 'list' of buffers. When you unqueue a buffer it gets popped off of the front. When you queue a buffer it gets pushed to the back. That's it. Simple enough?
This is 1 of the 2 most important methods in the class. What we do in this bit of code is check if any buffers have already been played. If there is then we start popping each of them off the back of the queue, we refill the buffers with data from the stream, and then we push them back onto the queue so that they can be played. Hopefully the Listener will have no idea that we have done this. It should sound like one long continuous chain of music. The 'stream' function also tells us if the stream is finished playing. This flag is reported back when the function returns.
public boolean stream(int buffer) {
byte[] pcm = new byte[BUFFER_SIZE];
int size = 0;
try {
if ((size = oggDecoder.read(pcm)) <= 0)
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
ByteBuffer data = ByteBuffer.wrap(pcm, 0, size);
al.alBufferData(buffer, format, data, size, rate);
check();
return true;
}
This is another important method of the class. This part fills the buffers with data from the Ogg bitstream. It's a little harder to get a grip on because it's not explainable in a top down manner. 'oggDecoder.read' does exactly what you may be thinking it does; it reads data from the Ogg bitstream. The j-ogg library does all the decoding of the bitstream, so we don't have to worry about that. This function takes a byte array as an argument and will decode atmost the capacity of this array.
The return value of 'oggDecoder.read' indicates several things. If the value of the result is positive then it represents how much data was read. This is important because 'read' may not be able to read the entire size requested (usually because it's at the end of the file and there's nothing left to read). Use the result of 'read' over 'BUFFER_SIZE' in any case. If the result of 'read' happens to be negative then it indicates that there was an error in the bitstream. If the result happens to equal zero then there is nothing left in the file to play.
The last part of this method is the call to 'alBufferData' which fills the buffer id with the data that we streamed from the Ogg using 'read'. We employ the 'format' and 'rate' values that we set up earlier
protected void empty() {
int[] queued = new int[1];
al.alGetSourcei(source[0], AL.AL_BUFFERS_QUEUED, queued, 0);
while (queued[0] > 0)
{
int[] buffer = new int[1];
al.alSourceUnqueueBuffers(source[0], 1, buffer, 0);
check();
queued[0]--;
}
oggDecoder = null;
}
This method will will unqueue any buffers that are pending on the source.
protected void check() {
if (al.alGetError() != AL.AL_NO_ERROR)
throw new ALException("OpenAL error raised...");
}
This saves us some typing for our error checks.
If you're with me so far then you must be pretty serious about getting this to work for you. Don't worry! We are almost done. All that we need do now is use our newly designed class to play an Ogg file. It should be a relatively simple process from here on in. We have done the hardest part. I won't assume that you will be using this in a game loop, but I'll keep it in mind when designing the loop.
This should be a no-brainer.
public boolean playstream() { if (!open()) return false; oggDecoder.dump(); if (!playback()) return false; while (update()) { if (playing()) continue; if (!playback()) return false; } return true; }
The program opens the stream, dumps some stream information and then will continually loop as long as the 'update' method continues to return true, and it will continue to return true as long as it can successfully read and play the audio stream. Within the loop we will make sure that the Ogg is playing.
In short, yes. There can be any number of buffers queued on the source at a time. Doing this may actually give you better results too. As I said earlier, with just 2 buffers in the queue at any time and with the cpu being clocked out (or if the system hangs), the source may actually finish playing before the stream has decoded another chunk. Having 3 or even 4 buffers in the queue will keep you a little further ahead in case you miss the update.
This is going to vary depending on several things. If you want a quick answer I'll tell you to update as often as you can, but that is not really necessary. As long as you update before the source finishes playing to the end of the queue. The biggest factors that are going to affect this are the buffer size and the number of buffers dedicated to the queue. Obviously if you have more data ready to play to begin with less updates will be necessary.
It should be fine. I haven't performed any extreme testing but I don't see why not. Generally you will not have that many streams anyway. You may have one to play some background music, and the occasional character dialog for a game, but most sound effects are too short to bother with streaming. Most of your sources will only ever have one buffer attached to them.
"Ogg" is the name of Xiph.org's container format for audio, video, and metadata. "Vorbis" is the name of a specific audio compression scheme that's designed to be contained in Ogg. As for the specific meanings of the words... well, that's a little harder to tell. I think they involve some strange relationship to Terry Pratchett novels. Here is a little page that goes into the details.
© 2003 DevMaster.net. All rights reserved. |
Contact us if you want to write for us or for any comments, suggestions, or feedback. |