OpenAL Lesson 8: OggVorbis Streaming Using The Source Queue （转载）
Hello again fellow coders. I'm back after a fairly long hiatus with another tutorial on the OpenAL api. And I think this will be a beefy one. I would first like to thank the community for their support thus far in the series. I want to put out some special thanks to DevMaster.net who is hosting the series on their website. This really got the ball rolling on the series which has now been ported to Visual C++ 6 by TheCell, and to Java by Athomas Goldberg for JOAL (Java Bindings for OpenAL). I have also heard they have been translated to Portuguese for the Brazilian GameDev. I would also like to give special thanks to Jorge Bernal for sending me some sample code which gave me enough of a kick in the pants to get me writing again. That was a big help (even though translating the code from Spanish was a chore :).
An Introduction to OggVorbis
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.
Designing Your OggVorbis Streaming API
Without further ado let's get to some code.
BUFFER_SIZE (4096 * 8)
This tutorial will be written in pure C++ code, so we will first include some of the C++ standard headers. We of course include the OpenAL api (as always), and we will also include 4 new headers. These new headers are part of a set of libraries written by the designers of OggVorbis. There are 4 in total: 'ogg.dll' (the format and decoder), 'vorbis.dll' (the coding scheme), 'vorbisenc.dll' (tools for encoding), and 'vorbisfile.dll' (tools for streaming and seeking). We won't be using vorbisenc but I've included it in the files for your use. Using these libraries will take care of the hardest 99% of the work (pretty much all of the decoding). There really is no reason not to use them. First of all we would be re-inventing the wheel if we did, and I doubt that we could write something better than the actual designers of the codec. As another plus: these libraries will be updated as the format evolves without any additional work on our part. But the biggest reason to use these libraries is consistency. If all Ogg files are encoded and decoded using these libraries then all Ogg's should be able to play in all Ogg players. As long as we use this standard library set then we can be sure we will support any Ogg file in existence.
We also create the macro 'BUFFER_SIZE' which 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.
open(string path); release(); display(); playback(); playing(); update();
stream(ALuint buffer); empty(); check(); string errorString( code);
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 (like error checking). I won't go over each function just yet. I believe my comments should give you an idea of what they're for.
FILE* oggFile; OggVorbis_File oggStream; vorbis_info* vorbisInfo; vorbis_comment* vorbisComment;
ALuint buffers; ALuint source; ALenum format; };
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.
I know your looking at 'FILE*' and are thinking why we would be using a vanilla C file handle when we are using C++. Well vorbisfile was designed around C rather than C++ so it's natural that they used the C file system. It is possible, and indeed quite easy, to get vorbisfile to work with fstream. But even though it is easy it's not any simpler than doing it this way.
(!(oggFile = fopen(path.c_str(), "rb")))
string("Could not open Ogg file.");
((result = ov_open(oggFile, &oggStream, NULL, 0)) < 0)
string("Could not open Ogg stream. ") + errorString(result);
See what I mean? If we were to use fstream we would have to create several new functions and register them through 'ov_open_callbacks'. This may be more useful for you if you need support for a virtual file system. The function 'ov_open' binds the file handle with the Ogg stream. The stream now 'owns' this file handle so don't go messing around with it yourself.
vorbisInfo = ov_info(&oggStream, -1);
vorbisComment = ov_comment(&oggStream, -1);
(vorbisInfo->channels == 1)
format = AL_FORMAT_MONO16;
format = AL_FORMAT_STEREO16;
This grabs some information on the file. We extract the OpenAL format enumerator based on how many channels are in the Ogg.
alSource3f(source, AL_POSITION, 0.0, 0.0, 0.0);
alSource3f(source, AL_VELOCITY, 0.0, 0.0, 0.0);
alSource3f(source, AL_DIRECTION, 0.0, 0.0, 0.0);
alSourcef (source, AL_ROLLOFF_FACTOR, 0.0 );
alSourcei (source, AL_SOURCE_RELATIVE, 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.
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. 'ov_clear' releases it's hold on the file stream and will close the handle for us as well.
<< "version " << vorbisInfo->version << "/n"
<< "channels " << vorbisInfo->channels << "/n"
<< "rate (hz) " << vorbisInfo->rate << "/n"
<< "bitrate upper " << vorbisInfo->bitrate_upper << "/n"
<< "bitrate nominal " << vorbisInfo->bitrate_nominal << "/n"
<< "bitrate lower " << vorbisInfo->bitrate_lower << "/n"
<< "bitrate window " << vorbisInfo->bitrate_window << "/n"
<< "vendor " << vorbisComment->vendor << "/n";
( i = 0; i < vorbisComment->comments; i++)
cout << " " << vorbisComment->user_comments[i] << "/n";
cout << endl;
We can use this to view additional information on the file.
alSourceQueueBuffers(source, 2, buffers);
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.
alGetSourcei(source, AL_SOURCE_STATE, &state);
(state == AL_PLAYING);
This simplifies the task of checking the state of the source.
active = ;
alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
alSourceUnqueueBuffers(source, 1, &buffer);
active = stream(buffer);
alSourceQueueBuffers(source, 1, &buffer);
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.
size = 0;
(size < BUFFER_SIZE)
result = ov_read(&oggStream, data + size, BUFFER_SIZE - size, 0, 2, 1, & section);
(result > 0)
size += result;
(result < 0)
(size == 0)
alBufferData(buffer, format, data, size, vorbisInfo->rate);
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. 'ov_read' does exactly what you may be thinking it does; it reads data from the Ogg bitstream. vorbisfile does all the decoding of the bitstream, so we don't have to worry about that. This function takes our 'oggStream' structure, a data buffer where it can write the decoded audio, and the size of the chunk you want to decode. The last 4 arguments you don't really have to worry about but I will explain anyway. Relatively: the first indicates little endian (0) or big endian (1), the second indicates the data size (in bytes) as 8 bit (1) or 16 bit (2), the third indicates whether the data is unsigned (0) or signed (1), and the last gives the number of the current bitstream.
The return value of 'ov_read' indicates several things. If the value of the result is positive then it represents how much data was read. This is important because 'ov_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 'ov_read' over 'BUFFER_SIZE' in any case. If the result of 'ov_read' happens to be negative then it indicates that there was an error in the bitstream. The value of the result is an error code in this case. If the result happens to equal zero then there is nothing left in the file to play.
What makes this code complicated is the while loop. This method was designed to be modular and modifiable. You can change 'BUFFER_SIZE' to whatever you would like and it will still work. But this requires us to make sure that we fill the entire size of the buffer with multiple calls to 'ov_read' and make sure that everything aligns properly. 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 'ov_read'. We employ the 'format' and 'vorbisInfo' data that we set up earlier
alGetSourcei(source, AL_BUFFERS_QUEUED, &queued);
alSourceUnqueueBuffers(source, 1, &buffer);
This method will will unqueue any buffers that are pending on the source.
error = alGetError();
(error != AL_NO_ERROR)
string("OpenAL error was raised.");
This saves us some typing for our error checks.
string ogg_stream::errorString( code)
string("Read from media.");
string("Not Vorbis data.");
string("Vorbis version mismatch.");
string("Invalid Vorbis header.");
string("Internal logic fault (bug or heap/stack corruption.");
string("Unknown Ogg error.");
This will 'stringify' an error message so it makes sense when you read it on a console or MessageBox or whatever.
Making Your Own OggVorbis Player
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.
main( argc, * argv)
This should be a no-brainer.
(argc < 2)
Since we are using C++ we will also be using the try/catch/throw keywords for exception handling. You have probably noticed that I've been throwing strings throughout the code in this article.
The first thing I have done here is check to make sure that the user has supplied us with a file path. If there is no arguments to the program then we can't really do anything, so we will simply show the user a little message indicating the Ogg extension. Not very informative but a pretty standard way of handling this in a console application. If there was an argument to the program then we can use it to open a file. We'll also display info on the Ogg file for completeness.
string("Ogg refused to play.");
string("Ogg abruptly stopped.");
cout << "Ogg stream was interrupted./n";
cout << "Program normal termination.";
I find a programs main loop is always the most fun part to write. We begin by playing the Ogg. An Ogg may refuse to play if there is not enough data to stream through the initial 2 buffers (in other words the Ogg is too small) or if it simply can not read the file.
The program 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. This may seem like it serves the same purpose as 'update', but it will also solve some other issues that have to do with the system. As a simple test I ran this program while also running a lot of other programs at the same time. Eating up as much cpu time as I could to see how oggplayer would react. You may be surprised to find that the interrupt message does get displayed. Streaming can be interrupted by external processes. This does not raise an error however. Keep that in mind.
If nothing else happens then the program will exit normally with a little message to let you know.
cout << error;
Will catch an error string if one was thrown and display some information on why the program had to terminate.
The end of our main is also a no-brainer.
Answers To Questions You May Be Asking
Can I use more than one buffer for the stream?
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.
How often should I call ogg.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.
Is it safe to stream more than one Ogg at a time?
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.
So what is with the name?
"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.
How come my console displays 'oggplayer *.ogg' when I run the example program?
You have to specify a filename. You can simply drag and drop an Ogg file over the program and it should work fine. I did not supply a sample Ogg to go with the example however. Partially due to the legality of using someone else's music and partially to reduce the file size. If someone wants to donate a piece (and I like it :) I may add it to the example files. But please do not submit copyrighted material or hip-hop :).