OpenAL: an object-oriented approach

in Game Programming, OpenAL

We have already discussed several topics related to OpenAL. This week we will investigate how we can structure the concepts of OpenAL to fit nicely with the object oriented paradigm, so we can write elegant C# code to play sounds and manage sound effects.

In this post we will quickly go over the three main concepts of OpenAL as discussed in my introduction post: listeners, sources, and buffers. In addition, we will also introduce a type for sound data, which comes in handy for sound effect management.

Helper

Whenever we use OpenAL, we are actually working with a library in a different language. If something goes wrong with executing code there, our program won’t crash. As a matter of fact, we have to explicitly ask OpenAL if there have been any errors. While many errors do not cause critical errors for our own application, they might cause unexpected behaviour, especially if different sound drivers get involved. If we do not check for errors regularly, it can be very difficult to trace back to the source of the error. For this reason, it is very helpful to check for errors after every OpenAL call, so we can be exactly sure where the error occurs. The following helper class will allow us to do that easily:

public class ALHelper
{
    public static void Check()
    {
        ALError error;
        if ((error = AL.GetError()) == ALError.NoError)
            return;

        // Fail silently for now.
        Debug.Print(AL.GetErrorString(error));
    }

    public static void Call(Action function)
    {
        function();
        ALHelper.Check();
    }

    public static TReturn Eval<TReturn>(Func<TReturn> function)
    {
        var val = function();
        ALHelper.Check();
        return val;
    }
}

This is only a small portion of the class: we can define further overloads for the Call and Eval functions that allow us to send parameters. The most important part is the setup of this class: we have a Check method that checks for errors and does something with it. The Call and Eval functions are there to make our life easy, it allows us to call functions like so:

var handle = ALHelper.Eval(AL.GetSource)

This evaluates the expression, checks for OpenAL errors, and returns the result.

Sound data

With some helpers to get us on our way, let’s start with the simplest class: a wrapper for the data. While in essence the data consists of raw bytes. However, our data might be divided amongst multiple byte arrays, and the data is meaningless without also storing its format and sample rate. That’s why we can wrap it together nicely in a single class:

public class SoundBufferData
{
    private readonly IList<short[]> buffers;
    private readonly ALFormat format;
    private readonly int sampleRate;

    public IList<short[]> Buffers => this.buffers;
    public ALFormat Format => this.format;
    public int SampleRate => this.sampleRate;

    public SoundBufferData(IList<short[]> buffers, ALFormat format, int sampleRate)
    {
        this.buffers = buffers;
        this.format = format;
        this.sampleRate = sampleRate;
    }
}

This class is especially nice, because it can be completely constructed from the input files and then stored in the application memory until we need it.

Sound buffers

Before we can play any sound, we need to store it in a sound buffer. Storing sounds in a buffer consists of two steps: asking OpenAL to reserve the right amount of buffers and filling them. Let’s first set up a basic class that wraps the buffer handles for us:

public class SoundBuffer : IDisposable
{
    private readonly int[] handles;

    public bool Disposed { get; private set; }

    public SoundBuffer(int amount)
    {
        this.handles = ALHelper.Eval(AL.GenBuffers, amount);
    }

    public void Dispose()
    {
        if (this.Disposed)
            return;

        ALHelper.Call(AL.DeleteBuffers, this.handles);

        this.Disposed = true;
    }

    static public implicit operator int[] (SoundBuffer buffer)
    {
        return buffer.handles;
    }
}

The class itself has only very limited functionality: in the constructor we request the right amount from buffers from OpenAL and store them internally using an array. We also implement IDisposable to implement a method that releases the buffers once we are finished with them.

A nice addition, which I added based on an identical usage pattern from amulware.Graphics, is the implicit conversion to the handles. This means that in any OpenAL method that takes a set of buffer handles, the wrapper class can be passed as parameter (footnote: sadly the generic Call and Eval methods do not work as nicely, because the type parameters will be filled in wrongly).

Now that we have a class that manages our sound buffers, we want to actually be able to put some useful data in it. Since we also have a nice class that manages our data, we should make use of that. In the most basic form, these are the methods we will be needing:

private void fillBufferRaw(int index, short[] data, ALFormat format, int sampleRate)
{
    if (index < 0 || index >= this.handles.Length)
        throw new ArgumentOutOfRangeException(nameof(index));

    ALHelper.Call(
        () => AL.BufferData(this.handles[index], format, data, data.Length * sizeof (short), sampleRate));
}

public void FillBuffer(SoundBufferData data)
{
    this.FillBuffer(0, data);
}

public void FillBuffer(int index, SoundBufferData data)
{
    if (index < 0 || index >= this.handles.Length)
        throw new ArgumentOutOfRangeException(nameof(index));
    if (data.Buffers.Count > this.handles.Length)
        throw new ArgumentException("This data does not fit in the buffer.", nameof(data));

    for (int i = 0; i < data.Buffers.Count; i++)
        this.fillBufferRaw((index + i) % this.handles.Length, data.Buffers[i], data.Format, data.SampleRate);
}

fillBufferRaw only forwards the fill buffer request to OpenAL. The remaining methods form a layer around it to allow us to use the SoundBufferData wrapper we wrote before.

And with that, we have all the functionality of the sound buffers as well.

Source

The source is the most complex entity we will discuss today. Sources do not only have functionality, but also have a state: they can have a position and velocity, volume and pitch. Sources can also be played, paused, stopped, and rewinded, which means they have a playing state as well. This means that in addition to some methods, we will also have to give it some properties. Let’s not get ahead of ourselves and start with the basics again.

public class Source : IDisposable
{
    private readonly int handle;

    public bool Disposed { get; private set; }

    public Source()
    {
        this.handle = ALHelper.Eval(AL.GenSource);
    }

    public void Dispose()
    {
        if (this.Disposed)
            return;

        ALHelper.Call(AL.DeleteSource, this.handle);
        this.Disposed = true;
    }
    #endregion

    static public implicit operator int (Source source)
    {
        return source.handle;
    }
}

The basics look pretty much identical to the SoundBuffer class, but instead of buffers we ask for a source, and since there will always be only one of them, we can do with a single integer handle, instead of an array of them.

Next we will be adding some controls to the source, since these are easy to implement.

public ALSourceState State => ALHelper.Eval(AL.GetSourceState, this.handle);

public void Play()
{
    ALHelper.Call(AL.SourcePlay, this.handle);
}

public void Pause()
{
    ALHelper.Call(AL.SourcePause, this.handle);
}

public void Stop()
{
    ALHelper.Call(AL.SourceStop, this.handle);
}

public void Rewind()
{
    ALHelper.Call(AL.SourceRewind, this.handle);
}

The four methods only forward the call to OpenAL. These forwarded calls in turn only change the state of the source, so that OpenAL knows what to do with them. We can retrieve the state again by using the AL.GetSourceState getter. In C# style, we write an accessor for this property.

A small detail we will have to add to our dispose method is stopping the source before disposing of it:

if (this.State != ALSourceState.Stopped)
    this.Stop();

Right now we have a source we can create, play, and stop when we want, but it isn’t very useful yet until we can actually tell the source what buffers it should play. Sources play buffers according to a queue. Queueing buffers is easy enough:

private void queueBuffersRaw(int bufferLength, int[] bufferIDs)
{
    ALHelper.Call(AL.SourceQueueBuffers, this.handle, bufferLength, bufferIDs);
}

public void QueueBuffer(SoundBuffer buffer)
{
    var handles = (int[]) buffer;
    this.queueBuffersRaw(handles.Length, handles);
}

We use the same pattern here as before: we have a method that forwards the call to the OpenAL, and then a wrapper method around it that allows us to use our own interface. Here we use an explicit conversion to an int[] to retrieve the actual handles of the buffers.

For our next task we require a bit more information: we also want to be able to unqueue buffers. This becomes important if we for example want to look at audio streaming: buffers we finished players can be removed from the front of the queue and we add new queues to the end of the queue to make sure we have sufficiently enough data to play from. OpenAL has accessors to get the amount of buffers currently processed and queued, so we will convert these into C# properties as well:

public int ProcessedBuffers
{
    get
    {
        int processedBuffers;
        AL.GetSource(this.handle, ALGetSourcei.BuffersProcessed, out processedBuffers);
        ALHelper.Check();
        return processedBuffers;
    }
}

public int QueuedBuffers
{
    get
    {
        int queuedBuffers;
        AL.GetSource(this.handle, ALGetSourcei.BuffersQueued, out queuedBuffers);
        ALHelper.Check();
        return queuedBuffers;
    }
}

public bool FinishedPlaying => this.ProcessedBuffers >= this.QueuedBuffers && !this.looping;

The FinishedPlaying property is an easy way to check if the source is still playing any sound. Our sound manager could then use that information to clean up the no longer necessary sources. Remark that the property includes a field (this.looping) we have not discussed yet, but we will come back to it later.

With these two properties we can implement methods that automatically unqueue any buffers finished playing, or just all the buffers if for some reason we want to start with a clean source.

public void UnqueueBuffers()
{
    if (this.QueuedBuffers == 0)
        return;

    ALHelper.Call(() => AL.SourceUnqueueBuffers(this.handle, this.QueuedBuffers));
}

public void UnqueueProcessedBuffers()
{
    ALHelper.Call(() => AL.SourceUnqueueBuffers(this.handle, this.ProcessedBuffers));
}

Due to the simplicity of these functions, we don’t have a raw unqueue method here.

Finally, we want to be able to influence the properties of a source. The pattern for each of the properties will be the same, so let’s look at the looping one, since we already saw that one before. The properties will look something like before:

private bool looping;

public bool Looping
{
    get { return this.looping; }
    set
    {
        ALHelper.Call(AL.Source, this.handle, ALSourceb.Looping, this.looping = value);
    }
}

We use a backing field to easily retrieve the value. We could instead use the AL.GetSource method, but this is a small speedup which might be especially important if you access the properties every frame.

Properties in OpenAL use an enum with all possible properties. This means that the pattern for the other properties is very similar. For more examples of these properties, check the full source in the Github repository of my audio library.

Since we are using a backing field and not checking the actual source value, we have to be careful about setting the right value on first initialisation. Most importatly, we should set the volume and pitch to 1 in the constructor.

All of this together gives us the basic functionality of sources. Sources are easily the most complex entity in OpenAL and there are some advanced features we won’t touch upon here, since they are very rarely used.

Listener

The listener is a complicated entitie when it comes to OpenAL: OpenAL uses the single listener model. This means OpenAL can only work with one listener at a time. We could make use of a singleton pattern to force a single listener instance existing. While this would be a valid approach and stay true to the original design of OpenAL, I decided to choose a slightly different approach. The fact that only a single listener can be used in OpenAL is often seen as a limitation of OpenAL. The approach I took is pretty much identical to the pattern we used for the sound data: we use a class to store all the data, but it won’t be of any use, unless we put it in OpenAL somehow.

In this approach, we could even limit ourselves to using an IListener interface, since there is no additional logic required:

public interface IListener
{
    Vector3 Position { get; }
    Vector3 Velocity { get; }
    float Gain { get; }
    Vector3 Up { get; }
    Vector3 At { get; }
}

In a 3D game, your player game object could be the listener. It is very likely that is has most of these properties already available to it, so using an interface makes using listeners very convenient.

Of course, the interface will have to be used to actually influence the results we are getting. We won’t go over it in this post, but this is where we would need an AudioManager. This manager could be responsible for updating the listener properties every frame, and we could even easily switch out listeners (let’s say we are an observer cycling through the players: this allows us to adapt the sound to that as well).

If the positioning of sounds is not important, which is almost always the case, unless you have some sort of first person 3D game, then the listener does not matter and we can use the default values as provided by OpenAL. Moreover, if we play stereo sound, the positional properties of every entity will be completely ignored. Hence, for most applications, the listener will be completely unnecessary.

Conclusion

We have stepped through the four main entities in OpenAL and wrote objects to represent all of them. While not implementing all the functionality – especially in sources there are many advanced features we did not touch upon – we now have a framework that we can use to access all OpenAL functionality, without having to use it’s procedural interface.

These classes provide an elegant basis for a more extensive audio library. The classes we have discussed have already been added to my audio library, and I will soon be using them to implement more advanced features. Progress on this can be found in the GitHub repository.

If you have any questions or comments, feel free to let me know, and I’ll see you next time.

Place comment

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