The Complete Guide to OpenAL with C++ – Part 2: Streaming Audio

Title of article: The Complete Guide to OpenAL with C++: Streaming Audio

In the previous part of this series, we learned about OpenAL, setting it up, and getting it to play a .WAV file. That was everything we needed for a small game, everything except the music. That’s because music files are usually pretty big, and so we need to stream them.

In this article, I’ll show you how to stream audio files through OpenAL with C++.

What is Streaming?

Streaming is the process of loading just enough data from the source, in our case a file on harddisk, to use it, and discard stuff we don’t need while simultaneously loading stuff we will soon need. Think of it like keeping the needle of our record player (or laser of the CD player) moving along the file.

First let’s recap playing a single data file:

Playing a Small Amount of Audio Data

Remember from the previous article how we created a buffer of data and provided it to OpenAL?

std::uint8_t channels;
std::int32_t sampleRate;
std::uint8_t bitsPerSample;
std::vector<char> soundData;
if(!load_wav("iamtheprotectorofthissystem.wav", channels, sampleRate, bitsPerSample, soundData))
{
    std::cerr << "ERROR: Could not load wav" << std::endl;
    return 0;
}
ALuint buffer;
alCall(alGenBuffers, 1, &buffer);
alCall(alBufferData, buffer, format, soundData.data(), soundData.size(), sampleRate);

The way it worked was we generated an OpenAL Buffer via our call to alGenBuffers which basically gives us a handle to the object within the OpenAL Context. We take the sound data we loaded from the .WAV file and placed it into that OpenAL buffer with the call to alBufferData.

That single buffer was the same size as the sound data. In other words, we filled that buffer on the OpenAL side with exactly the amount of data we had on the CPU side. Hopefully this illustrates it:

Diagram illustrating how OpenAL plays sound data from a source
Playing sound from a single buffer

Here’s some bullet points to follow along with the diagram:

  • You load an audio file from disk into some kind of buffer on the CPU side. We used an std::vector<char> for ours, but it could also be a raw pointer if that’s your thing.
  • You generate the buffer in OpenAL and request back a handle
  • You fill the buffer with a call to alBufferData
  • You generate a source (where the sound is played from) and get back a handle
  • You set the buffer for the source, i.e., where the source should get sound data from with a call to alSourcei
  • You call alSourcePlay and you got some sound

Playing Streaming Data

Streaming it requires us to have several buffers for the same sound. While you can do this for a large sound file, like above, but split into multiple buffers, it uses a lot of RAM. Instead, while OpenAL is making use of one buffer, we can load up data into another. The process looks like this below diagram, we’ll go over each of those calls in the next sections.

Streaming sound data into OpenAL
  1. First you generate multiple buffers, somewhere between 3 and 4 is usually enough
  2. You load some data up into these buffers
  3. Generate a single source
  4. Start executing an update_stream call that you will write. It will:
    1. Determine if there’s any buffers that OpenAL is finished with
    2. Unqueue those buffers so that we can fill them with the next bit of audio data
    3. Queue them back up into the buffer

The big important thing here is that OpenAL supports no direct method of streaming. What it does is allow you to queue and unqueue buffers for a source. The source will play through each buffer in it’s queue until it gets to the end, it doesn’t care what’s in those queues. It’s your job to fill that next buffer up with the next few seconds of sound data from that really cool song you wrote.

Streaming a Music Track

Okay; we’re going to need a music track. I’ll let you handle that. Go and find yourself something that is about 30 seconds to a minute long. If you can’t find something that short; then grab Audacity and cut the track. I’m going to use the following, I’ve converted it into an .ogg file just for the purposes of putting it up here on the website. You can download the original here.

30, 29, 28….

Now technically, you could take the code from last time, change the filename, and it will work. This file isn’t sooo big that you have to stream it, but we’re going to use it to stream, just because we can.

Same as last time, drop all of this code into your IDE. Make sure you’ve got the error handling code and the wav file loading code already in there.

const std::size_t NUM_BUFFERS = 4;
const std::size_t BUFFER_SIZE = 65536; // 32kb of data in each buffer

void update_stream(const ALuint source,
                   const ALenum& format,
                   const std::int32_t& sampleRate,
                   const std::vector<char>& soundData,
                   std::size_t& cursor)
{
    ALint buffersProcessed = 0;
    alCall(alGetSourcei, source, AL_BUFFERS_PROCESSED, &buffersProcessed);

    if(buffersProcessed <= 0)
        return;

    while(buffersProcessed--)
    {
        ALuint buffer;
        alCall(alSourceUnqueueBuffers, source, 1, &buffer);

        ALsizei dataSize = BUFFER_SIZE;

        char* data = new char[dataSize];
        std::memset(data, 0, dataSize);

        std::size_t dataSizeToCopy = BUFFER_SIZE;
        if(cursor + BUFFER_SIZE > soundData.size())
            dataSizeToCopy = soundData.size() - cursor;

        std::memcpy(&data[0], &soundData[cursor], dataSizeToCopy);
        cursor += dataSizeToCopy;

        if(dataSizeToCopy < BUFFER_SIZE)
        {
            cursor = 0;
            std::memcpy(&data[dataSizeToCopy], &soundData[cursor], BUFFER_SIZE - dataSizeToCopy);
            cursor = BUFFER_SIZE - dataSizeToCopy;
        }

        alCall(alBufferData, buffer, format, data, BUFFER_SIZE, sampleRate);
        alCall(alSourceQueueBuffers, source, 1, &buffer);

        delete[] data;
    }
}

int main()
{
    ALCdevice* openALDevice = alcOpenDevice(nullptr);
    if(!openALDevice)
        return 0;

    ALCcontext* openALContext;
    if(!alcCall(alcCreateContext, openALContext, openALDevice, openALDevice, nullptr) || !openALContext)
    {
        std::cerr << "ERROR: Could not create audio context" << std::endl;
        return 0;
    }
    ALCboolean contextMadeCurrent = false;
    if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, openALContext)
       || contextMadeCurrent != ALC_TRUE)
    {
        std::cerr << "ERROR: Could not make audio context current" << std::endl;
        return 0;
    }

    std::uint8_t channels;
    std::int32_t sampleRate;
    std::uint8_t bitsPerSample;
    std::vector<char> soundData;
    if(!load_wav("407640__drotzruhn__countdown-30-seconds.wav", channels, sampleRate, bitsPerSample, soundData))
    {
        std::cerr << "ERROR: Could not load wav" << std::endl;
        return 0;
    }

    ALuint buffers[NUM_BUFFERS];

    alCall(alGenBuffers, NUM_BUFFERS, &buffers[0]);

    ALenum format;

    if(channels == 1 && bitsPerSample == 8)
        format = AL_FORMAT_MONO8;
    else if(channels == 1 && bitsPerSample == 16)
        format = AL_FORMAT_MONO16;
    else if(channels == 2 && bitsPerSample == 8)
        format = AL_FORMAT_STEREO8;
    else if(channels == 2 && bitsPerSample == 16)
        format = AL_FORMAT_STEREO16;
    else
    {
        std::cerr
            << "ERROR: unrecognised wave format: "
            << channels << " channels, "
            << bitsPerSample << " bps" << std::endl;
        return 0;
    }

    for(std::size_t i = 0; i < NUM_BUFFERS; ++i)
    {
        alCall(alBufferData, buffers[i], format, &soundData[i * BUFFER_SIZE], BUFFER_SIZE, sampleRate);
    }

    ALuint source;
    alCall(alGenSources, 1, &source);
    alCall(alSourcef, source, AL_PITCH, 1);
    alCall(alSourcef, source, AL_GAIN, 1.0f);
    alCall(alSource3f, source, AL_POSITION, 0, 0, 0);
    alCall(alSource3f, source, AL_VELOCITY, 0, 0, 0);
    alCall(alSourcei, source, AL_LOOPING, AL_FALSE);

    alCall(alSourceQueueBuffers, source, NUM_BUFFERS, &buffers[0]);

    alCall(alSourcePlay, source);

    ALint state = AL_PLAYING;

    std::size_t cursor = BUFFER_SIZE * NUM_BUFFERS;

    while(state == AL_PLAYING)
    {
        update_stream(source, format, sampleRate, soundData, cursor);
        alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
    }

    alCall(alDeleteSources, 1, &source);
    alCall(alDeleteBuffers, NUM_BUFFERS, &buffers[0]);

    alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, nullptr);
    alcCall(alcDestroyContext, openALDevice, openALContext);

    ALCboolean closed;
    alcCall(alcCloseDevice, closed, openALDevice, openALDevice);

    return 0;
}

That’s a bit longer than last time, right? Nah, it’s not too bad. Let’s go over it all just the same as we did previously.

Global Variables

const std::size_t NUM_BUFFERS = 4;
const std::size_t BUFFER_SIZE = 65536; // 32kb of data in each buffer

You know these don’t need to be globals, but here they are. We only need two pieces of constant data, of course, these could be parameters of whatever class or struct you construct around all this OpenAL code; but for me, thesevalues work pretty much universally.

The NUM_BUFFERS is how many sequential buffers of sound data we’ll be using. This is a per-source thing by the way, if you’ve got multiple sources trying to play streaming data from the same file, you might run into problems if you’re queuing and unqueuing buffers all over the place.

Why 4 of them? That’s what works for me. I’ve found two of them, which honestly should be enough, to occasionally have a problem if there’s a sudden frame-rate drop, OpenAL is done with one, starts playing the next one before I’ve had a chance to change the audio data in it, so the audio skips backwards. 3 buffers worked for the majority of the time, but 4 is an even number.

The BUFFER_SIZE can be changed. I’ve found that 4 buffers of 32kB to work just great. You could try 2 buffers of 64Kb; who knows, you might have better luck. But my learned experience is that these two numbers tend to work the best.

Generate the Buffers

This time we’re generating more than one buffer. Super simple to do.

ALuint buffers[NUM_BUFFERS];
alCall(alGenBuffers, NUM_BUFFERS, &buffers[0]);

These obviously don’t need to be on the stack, you could allocate them from the heap. In fact, you probably should if you’ve got a larger game.

The calls here are pretty much the same as you did before, there’s just more than 1 buffer now.

Fill the Buffers

We’ll skip the format stuff, it’s the same as previously. Filling the buffers is almost the same, there’s a couple changes:

for(std::size_t i = 0; i < NUM_BUFFERS; ++i)
{
    alCall(alBufferData, buffers[i], format, &soundData[i * BUFFER_SIZE], BUFFER_SIZE, sampleRate);
}

We’re going to fill each buffer, and we’re going to fill it from the soundData where the last buffer left off. This code makes a big assumption that your audio data is larger than BUFFER_SIZE * NUM_BUFFERS. I would expect this to always be the case, but it’s not very robust.

Setup the Source

Setting up the source is almost identical. But note that we do not provide a buffer.

Play the Music

Now we can’t just hit play. We didn’t provide a buffer! That’s because we’re not assigning a single buffer anymore, instead, we need to queue them up.

Queuing them is going to tell the OpenAL Source that it should play through the first one in the queue, then the second, without any gaps, and the third, etc etc, until it runs out of buffers.

alCall(alSourceQueueBuffers, source, NUM_BUFFERS, &buffers[0]);

alCall(alSourcePlay, source);

ALint state = AL_PLAYING;
std::size_t cursor = BUFFER_SIZE * NUM_BUFFERS;

while(state == AL_PLAYING)
{
    update_stream(source, format, sampleRate, soundData, cursor);
    alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
}

We queue up the number of buffers we have. They’ve been filled, in order, with sound data from our original file. We then start playing.

But here things get different. We keep track of a cursor. Think of it like the needle on a record, or laser on a disk. It keeps track of where in the audio file we’re up to.

Now, we sort of shift everything off to our update_stream function.

Updating the Stream

This needs to be called periodically. I call it every frame, but you don’t need to. You just need to make sure it’s called before OpenAL runs out of buffers, and you need to allow yourself enough wiggle room to populate the next buffer and throw it on the back.

void update_stream(const ALuint source,
                   const ALenum& format,
                   const std::int32_t& sampleRate,
                   const std::vector<char>& soundData,
                   std::size_t& cursor)
{
    ALint buffersProcessed = 0;
    alCall(alGetSourcei, source, AL_BUFFERS_PROCESSED, &buffersProcessed);

    if(buffersProcessed <= 0)
        return;

    while(buffersProcessed--)
    {

The source is the OpenAL source we’re going to update. We don’t check that it’s a stream, though the stream related calls will fail for the most part if there’s not streamed buffers involved. format is what it’s always been, same as sampleRate, values derived from the audio file. soundData is the entire audio data, and that’s where things can change if you want to stream from file.

We’re only streaming from data we already have. But, I go into depth on streaming from the file directly in this article.

The cursor is our value from before. Notice that it’s passed by reference, we’re going to update it.

Now, we make a call to alGetSourcei requesting the AL_BUFFERS_PROCESSED. It will return the number of buffers the source is finished playing through. If we’re calling this often enough, and if the buffers are large enough, this will in general only be 0 or 1, it would be weird if OpenAL played through 2 buffers before we got a chance to update. But it can happen, which is why we have 4 buffers.

If there’s no buffers it’s finished with, we can exit early. Otherwise, we’re going to do the next stuff for every buffer it’s finished with.

        ALuint buffer;
        alCall(alSourceUnqueueBuffers, source, 1, &buffer);

        ALsizei dataSize = BUFFER_SIZE;

        char* data = new char[dataSize];
        std::memset(data, 0, dataSize);

        std::size_t dataSizeToCopy = BUFFER_SIZE;
        if(cursor + BUFFER_SIZE > soundData.size())
            dataSizeToCopy = soundData.size() - cursor;

        std::memcpy(&data[0], &soundData[cursor], dataSizeToCopy);
        cursor += dataSizeToCopy;

        if(dataSizeToCopy < BUFFER_SIZE)
        {
            cursor = 0;
            std::memcpy(&data[dataSizeToCopy], &soundData[cursor], BUFFER_SIZE - dataSizeToCopy);
            cursor = BUFFER_SIZE - dataSizeToCopy;
        }

        alCall(alBufferData, buffer, format, data, BUFFER_SIZE, sampleRate);
        alCall(alSourceQueueBuffers, source, 1, &buffer);

        delete[] data;

I hope this is self-explanatory. First, we have to unqueue the buffer with a call to alSourceUnqueueBuffers. This is because even though OpenAL has played through it, it will play through it again when it gets to the end of it’s buffers (starts looping). We need to take it out of the queue.

Next we create a new buffer on the heap. And then we fill it with either BUFFER_SIZE amount of data, or however much data is left in the audio file. Technically, we could just use the soundData parameter to feed into the alBufferData call, but I thought this made it more obvious.

We make a check, that if we only copied as much data as was left, then we need to additionally fill the remainder with the start of the audio file. If we didn’t do this, there’s would be no audio data at the end, and it would take that many seconds until it started playing again.

Think about if this wasn’t music, but the sound of a car-engine idling. You can’t have a gap, it needs to loop seamlessly.

Then we fill the buffer on the OpenAL side, and requeue the buffer. We’ve unqueued it before, so now we’re appending it to the end of the queue. As far as the OpenAL Source is concerned, it never makes it to the end of it’s queued buffers. It’s just going like man, this is a really long song. Internally, the OpenAL objects are unconcerned with all this detail.

Finally, we delete our local buffer and release it back to the heap.

What’s next?!

Hey, take a look at the article on stream .ogg files. This will show you how to stream from the file, rather than loading it all into memory. Remember, OpenAL doesn’t give two-shits about how the data gets there, just that it has data.

You would have noticed that the application never stops, that’s because we keep adding more buffers to the queue. It should already be clear to you how you can alStopSource and alPlaySource when needed, or just not append the beginning of the audio data when you reach the end. The higher-level playback controls are something specific to your game that you’ll be able to work out.

With this, and the last article, you’ve got everything you need for a 2D game. You can play sounds, and stream audio. That’s music and lasers covered!

In the next article, we’ll go over spatial positioning the audio. Making things occur, behind, in front, to the left, underneath us and such. Exciting 3D audio!

4 thoughts on “The Complete Guide to OpenAL with C++ – Part 2: Streaming Audio”

  1. Hey that’s me again.Thank you I’ve got short sounds working perfectly. Now I want to make few looping sounds which should stop on some events. Like acceleration sound which is looping while I press w button and stops when I release it. Breaking sound. possibly car moving sound while car is moving. Music in background. Can you share some wisdom on how to make different looping sounds which should stop on some event?

    1. Hi Mckl, firstly, you should be able to have your sounds loop, and start and stop as neccessary with the above guide. For the actual production of a sound effect suitable for looping, take a look at this guide (it’s what I’ve used very recently to make a looping rumbling sound) https://gamedevbeginner.com/create-looping-sound-effects-for-games-for-free-with-audacity/

      What this won’t cover is the start of the sound and end of the sound, as in, this only works as if the sound effect was always playing and will play forever. The easiest way around this is to gradually fade the sound in and out, which is as simple as changing the gain of the source over time. I plan to cover this, which I think of as Higher-Level kind of code-structures, in a later part of the guide.

      If you jump onto the forum, I can share some code with you if you like though.

Leave a Comment