How to stream .ogg files with OpenAL in C++

When I first set out to do what the title says, I hit so many roadblocks and unforseen problems that it drove me bonkers. But eventually I figured it out.

So if you’ve given up trying to work out how to stream ogg files with OpenAL, or if you just need to get it done, you’ve come to the right place.

Create a struct to hold your streaming data

The first thing to do is create a kind of resource that will hold your buffers and file information.

const std::size_t NUM_BUFFERS = 4;
const ALsizei BUFFER_SIZE = 65536;

struct StreamingAudioData
{
    ALuint buffers[NUM_BUFFERS];
    std::string filename;
    std::ifstream file;
    std::uint8_t channels;
    std::int32_t sampleRate;
    std::uint8_t bitsPerSample;
    ALsizei size;
    ALuint source;
    ALsizei sizeConsumed = 0;
    ALenum format;
    OggVorbis_File oggVorbisFile;
    std::int_fast32_t oggCurrentSection = 0;
    std::size_t duration;
};

This is what you’re going to be passing around to play the sounds, so you’d likely want to setup some kind of system where you can reference this struct via an identifier.

The consts at the top will be used later as well. The buffer size is basically set to 32kB.

NUM_BUFFERS I’ve found that 4 buffers works well, but feel free to play around with this value.

buffers are loaded into as the previous one is playing. That’s how the streaming works, one buffer is handed off to OpenAL as the previous ones are having the next few milliseconds of audio provided.

filename is the filename of the original .ogg file. This is used in case the file handle is lost, to continue streaming we need to know which file to re-open.

file is the file handle.

channels, sampleRate, bitsPerSample are all populated when we load the .ogg file. These are needed by OpenAL in order to understand the data being passed to it in the buffers.

size this is the total size of the ogg file in bytes. Populated when the file is first loaded.

source this is the OpenAL Source that is generated to play the file. You set the gain and position of the file with this.

sizeConsumed is used by the callbacks to understand where in the audio data that playback is up to.

format is the OpenAL enum of either MONO or STEREO

oggVoribsFile is a handle needed by Ogg Vorbis library.

oggCurrentSection is the part of the audio that’s currently being played back.

duration is the total length of the audio data.

Some helpful error checking functions

The following functions will act as wrappers around any OpenAL function calls and automatically check for errors.

#define alCall(function, ...) alCallImpl(__FILE__, __LINE__, function, __VA_ARGS__)
#define alcCall(function, device, ...) alcCallImpl(__FILE__, __LINE__, function, device, __VA_ARGS__)

void check_al_errors(const std::string& filename, const std::uint_fast32_t line)
{
    ALCenum error = alGetError();
    if(error != AL_NO_ERROR)
    {
        std::cerr << "***ERROR*** (" << filename << ": " << line << ")\n" ;
        switch(error)
        {
        case AL_INVALID_NAME:
            std::cerr << "AL_INVALID_NAME: a bad name (ID) was passed to an OpenAL function";
            break;
        case AL_INVALID_ENUM:
            std::cerr << "AL_INVALID_ENUM: an invalid enum value was passed to an OpenAL function";
            break;
        case AL_INVALID_VALUE:
            std::cerr << "AL_INVALID_VALUE: an invalid value was passed to an OpenAL function";
            break;
        case AL_INVALID_OPERATION:
            std::cerr << "AL_INVALID_OPERATION: the requested operation is not valid";
            break;
        case AL_OUT_OF_MEMORY:
            std::cerr << "AL_OUT_OF_MEMORY: the requested operation resulted in OpenAL running out of memory";
            break;
        default:
            std::cerr << "UNKNOWN AL ERROR: " << error;
        }
        std::cerr << std::endl;
    }
}

template<typename alFunction, typename... Params>
auto alCallImpl(const char* filename, const std::uint_fast32_t line, alFunction function, Params... params)
->typename std::enable_if<std::is_same<void,decltype(function(params...))>::value,decltype(function(params...))>::type
{
    function(std::forward<Params>(params)...);
    check_al_errors(filename,line);
}

template<typename alFunction, typename... Params>
auto alCallImpl(const char* filename, const std::uint_fast32_t line, alFunction function, Params... params)
->typename std::enable_if<!std::is_same<void,decltype(function(params...))>::value,decltype(function(params...))>::type
{
    auto ret = function(std::forward<Params>(params)...);
    check_al_errors(filename,line);
    return ret;
}

You can then call OpenAL functions like:

alCall(alGenSources, 1, &audioData.source);

And if there’s a return value, it’s returned by alCall; all errors are logged in the standard error stream. I won’t walk through this code as it’s not required, just a helpful thing to have to make your quality of life a little bit better.

Ogg Callbacks

You need to define a couple of callback functions that you will later provide to the Ogg Vorbis library.

std::size_t read_ogg_callback(void* destination, std::size_t size1, std::size_t size2, void* fileHandle)
{
    StreamingAudioData* audioData = reinterpret_cast<StreamingAudioData*>(fileHandle);

    ALsizei length = size1 * size2;

    if(audioData->sizeConsumed + length > audioData->size)
        length = audioData->size - audioData->sizeConsumed;

    if(!audioData->file.is_open())
    {
        audioData->file.open(audioData->filename, std::ios::binary);
        if(!audioData->file.is_open())
        {
            std::cerr << "ERROR: Could not re-open streaming file \"" << audioData->filename << "\"" << std::endl;
            return 0;
        }
    }

    char* moreData = new char[length];

    audioData->file.clear();
    audioData->file.seekg(audioData->sizeConsumed);
    if(!audioData->file.read(&moreData[0],length))
    {
        if(audioData->file.eof())
        {
            audioData->file.clear(); // just clear the error, we will resolve it later
        }
        else if(audioData->file.fail())
        {
            std::cerr << "ERROR: OGG stream has fail bit set " << audioData->filename << std::endl;
            audioData->file.clear();
            return 0;
        }
        else if(audioData->file.bad())
        {
            perror(("ERROR: OGG stream has bad bit set " + audioData->filename).c_str());
            audioData->file.clear();
            return 0;
        }
    }
    audioData->sizeConsumed += length;

    std::memcpy(destination, &moreData[0], length);

    delete[] moreData;

    audioData->file.clear();

    return length;
}

Hopefully this part is straightforward. Basically the Ogg Vorbis library will call this function (you provide it to the library later) whenever it needs to read a bit more data. The OggVorbis library is going to be used to continuously stream data.

All the function does is loads the requested amount of data into the provided destination ptr. If the file has been closed, we re-open it and try to seek back to where we were previously.

std::int32_t seek_ogg_callback(void* fileHandle, ogg_int64_t to, std::int32_t type)
{
    StreamingAudioData* audioData = reinterpret_cast<StreamingAudioData*>(fileHandle);

    if(type == SEEK_CUR)
    {
        audioData->sizeConsumed += to;
    }
    else if(type == SEEK_END)
    {
        audioData->sizeConsumed = audioData->size - to;
    }
    else if(type == SEEK_SET)
    {
        audioData->sizeConsumed = to;
    }
    else
        return -1; // what are you trying to do vorbis?

    if(audioData->sizeConsumed < 0)
    {
        audioData->sizeConsumed = 0;
        return -1;
    }
    if(audioData->sizeConsumed > audioData->size)
    {
        audioData->sizeConsumed = audioData->size;
        return -1;
    }

    return 0;
}

Whenever Ogg Vorbis needs to seek to a particular part of the audio it calls this function. By setting the parameters of the StreamingAudioData struct that it provides, those setting get used by the Read Callback.

We set values of the struct rather than the std::ifstream because the seek position of the file can be lost if it gets closed.

long int tell_ogg_callback(void* fileHandle)
{
    StreamingAudioData* audioData = reinterpret_cast<StreamingAudioData*>(fileHandle);
    return audioData->sizeConsumed;
}

The last is called when Ogg Vorbis needs to know where in the file the cursor is. We just return the sizeConsumed that we’ve been manipulating in the previous two callbacks.

Load the .ogg file

The next function loads an .ogg file from disk into the Ogg Vorbis library components and creates an OpenAL source (you’ll need to have initialised OpenAL).

bool create_stream_from_file(const std::string& filename, StreamingAudioData& audioData)
{
    audioData.filename = filename;
    audioData.file.open(filename, std::ios::binary);
    if(!audioData.file.is_open())
    {
        std::cerr << "ERROR: couldn't open file" << std::endl;
        return 0;
    }

    audioData.file.seekg(0, std::ios_base::beg);
    audioData.file.ignore(std::numeric_limits<std::streamsize>::max());
    audioData.size = audioData.file.gcount();
    audioData.file.clear();
    audioData.file.seekg(0,std::ios_base::beg);
    audioData.sizeConsumed = 0;

    ov_callbacks oggCallbacks;
    oggCallbacks.read_func = read_ogg_callback;
    oggCallbacks.close_func = nullptr;
    oggCallbacks.seek_func = seek_ogg_callback;
    oggCallbacks.tell_func = tell_ogg_callback;

    if(ov_open_callbacks(reinterpret_cast<void*>(&audioData), &audioData.oggVorbisFile, nullptr, -1, oggCallbacks) < 0)
    {
        std::cerr << "ERROR: Could not ov_open_callbacks" << std::endl;
        return false;
    }

    vorbis_info* vorbisInfo = ov_info(&audioData.oggVorbisFile, -1);

    audioData.channels = vorbisInfo->channels;
    audioData.bitsPerSample = 16;
    audioData.sampleRate = vorbisInfo->rate;
    audioData.duration = ov_time_total(&audioData.oggVorbisFile, -1);

    alCall(alGenSources, 1, &audioData.source);
    alCall(alSourcef, audioData.source, AL_PITCH, 1);
    alCall(alSourcef, audioData.source, AL_GAIN, DEFAULT_GAIN);
    alCall(alSource3f, audioData.source, AL_POSITION, 0, 0, 0);
    alCall(alSource3f, audioData.source, AL_VELOCITY, 0, 0, 0);
    alCall(alSourcei, audioData.source, AL_LOOPING, AL_FALSE);

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

    if(audioData.file.eof())
    {
        std::cerr << "ERROR: Already reached EOF without loading data" << std::endl;
        return false;
    }
    else if(audioData.file.fail())
    {
        std::cerr << "ERROR: Fail bit set" << std::endl;
        return false;
    }
    else if(!audioData.file)
    {
        std::cerr << "ERROR: file is false" << std::endl;
        return false;
    }

    char* data = new char[BUFFER_SIZE];

    for(std::uint8_t i = 0; i < NUM_BUFFERS; ++i)
    {
        std::int32_t dataSoFar = 0;
        while(dataSoFar < BUFFER_SIZE)
        {
            std::int32_t result = ov_read(&audioData.oggVorbisFile, &data[dataSoFar], BUFFER_SIZE - dataSoFar, 0, 2, 1, &audioData.oggCurrentSection);
            if(result == OV_HOLE)
            {
                std::cerr << "ERROR: OV_HOLE found in initial read of buffer " << i << std::endl;
                break;
            }
            else if(result == OV_EBADLINK)
            {
                std::cerr << "ERROR: OV_EBADLINK found in initial read of buffer " << i << std::endl;
                break;
            }
            else if(result == OV_EINVAL)
            {
                std::cerr << "ERROR: OV_EINVAL found in initial read of buffer " << i << std::endl;
                break;
            }
            else if(result == 0)
            {
                std::cerr << "ERROR: EOF found in initial read of buffer " << i << std::endl;
                break;
            }
            
            dataSoFar += result;
        }
        
        if(audioData.channels == 1 && audioData.bitsPerSample == 8)
            audioData.format = AL_FORMAT_MONO8;
        else if(audioData.channels == 1 && audioData.bitsPerSample == 16)
            audioData.format = AL_FORMAT_MONO16;
        else if(audioData.channels == 2 && audioData.bitsPerSample == 8)
            audioData.format = AL_FORMAT_STEREO8;
        else if(audioData.channels == 2 && audioData.bitsPerSample == 16)
            audioData.format = AL_FORMAT_STEREO16;
        else
        {
            std::cerr << "ERROR: unrecognised ogg format: " << audioData.channels << " channels, " << audioData.bitsPerSample << " bps" << std::endl;
            delete[] data;
            return false;
        }

        alCall(alBufferData, audioData.buffers[i], audioData.format, data, dataSoFar, audioData.sampleRate);
    }

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

    delete[] data;

    return true;
}

I hope it’s clear what’s happening. First we load the file and set some of the basic members of the StreamingAudioData struct.

Next we “open” the ogg file using the Ogg Vorbis library and the callbacks we created earlier. This allows us to pass around the StreamAudioData struct through to those functions, and because it contains everything they need, it’s all sort of self-contained.

Now we can set some more members of the struct from Ogg Vorbis.

After that we create the source for OpenAL and finally point create our OpenAL buffers. Next part is us populating the buffers with decoded ogg data, via the ov_read function (which in turn calls our Read Callback).

Finally, we send that decoded data to OpenAL and start queuing up the buffers, of which we have 4, each holding 32kb of data.

Playing and Updating the stream

To play the stream you do the usual OpenAL calls:

play_stream(const StreamingAudioData& audioData)
{
    alCall(alSourceStop, audioData.source);
    alCall(alSourcePlay, audioData.source);
}

Now that we’ve got the hard part done, we just need to update the stream. This should occur constantly, or in the context of a game engine, it should occur every frame.

update_stream(StreamingAudioData& audioData)
{
    ALint buffersProcessed = 0;
    alCall(alGetSourcei, audioData.source, AL_BUFFERS_PROCESSED, &buffersProcessed);
    if(buffersProcessed <= 0)
    {
        return;
    }
    while(buffersProcessed--)
    {
        ALuint buffer;
        alCall(alSourceUnqueueBuffers, audioData.source, 1, &buffer);

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

        ALsizei dataSizeToBuffer = 0;
        std::int32_t sizeRead = 0;
            
        while(sizeRead < BUFFER_SIZE)
        {
            std::int32_t result = ov_read(&audioData.oggVorbisFile, &data[sizeRead], BUFFER_SIZE - sizeRead, 0, 2, 1, &audioData.oggCurrentSection);
            if(result == OV_HOLE)
            {
                std::cerr << "ERROR: OV_HOLE found in update of buffer " << std::endl;
                break;
            }
            else if(result == OV_EBADLINK)
            {
                std::cerr << "ERROR: OV_EBADLINK found in update of buffer " << std::endl;
                break;
            }
            else if(result == OV_EINVAL)
            {
                std::cerr << "ERROR: OV_EINVAL found in update of buffer " << std::endl;
                break;
            }
            else if(result == 0)
            {
               std::int32_t seekResult = ov_raw_seek(&audioData.oggVorbisFile, 0);
                if(seekResult == OV_ENOSEEK)
                    std::cerr << "ERROR: OV_ENOSEEK found when trying to loop" << std::endl;
                else if(seekResult == OV_EINVAL)
                    std::cerr << "ERROR: OV_EINVAL found when trying to loop" << std::endl;
                else if(seekResult == OV_EREAD)
                    std::cerr << "ERROR: OV_EREAD found when trying to loop" << std::endl;
                else if(seekResult == OV_EFAULT)
                    std::cerr << "ERROR: OV_EFAULT found when trying to loop" << std::endl;
                else if(seekResult == OV_EOF)
                    std::cerr << "ERROR: OV_EOF found when trying to loop" << std::endl;
                else if(seekResult == OV_EBADLINK)
                    std::cerr << "ERROR: OV_EBADLINK found when trying to loop" << std::endl;

                if(seekResult != 0)
                {
                    std::cerr << "ERROR: Unknown error in ov_raw_seek" << std::endl;
                    return;
                }
            }
            sizeRead += result;
        }
        dataSizeToBuffer = sizeRead;

        if(dataSizeToBuffer > 0)
        {
            alCall(alBufferData, buffer, audioData.format, data, dataSizeToBuffer, audioData.sampleRate);
            alCall(alSourceQueueBuffers, audioData.source, 1, &buffer);
        }

        if(dataSizeToBuffer < BUFFER_SIZE)
        {
            std::cout << "Data missing" << std::endl;
        }

        ALint state;
        alCall(alGetSourcei, audioData.source, AL_SOURCE_STATE, &state);
        if(state != AL_PLAYING)
        {
            alCall(alSourceStop, audioData.source);
            alCall(alSourcePlay, audioData.source);
        }

        delete[] data;
    }
}

Some of this function should look familiar, and to be honest, can probably be broken off into a reading function that the load and update both call.

All that’s happening is we ask OpenAL for which buffers it’s finished with. If it has finished with any, we use Ogg Vorbis library (the oggVorbisFile member) to fill the finished buffers with more decoded .ogg audio data. In this way, we’re just cycling through our four 32kB buffers over and over again.

When we get to the end of the audio, we wrap around so that one end of the audio ends as the beginning starts again. I’ve tested this and get a smooth transition.

When you’re done listening, you can just stop the source like you would for any other OpenAL source.

stop_stream(const StreamingAudioData& audioData)
{
    alCall(alSourceStop, audioData.source);
}

That’s all there is to it.


I hope someone finds this helpful. I know I spent a pretty long time trying to work this out when I first started looking into OpenAL and streaming .ogg files for my game engine. You’ll probably want to create some music to stream in your game as well.

Leave a Comment