Chapter 5: OpenTK.Audio (OpenAL)

The OpenAL 1.1 Crashcourse will give an introduction how to use OpenAL in your applications.

OpenAL contains the following classes:

OpenAL 1.0 Extensions that were included into 1.1: Multi-Channel Buffer playback Extension, Audio Capture Extension, Enumeration Extension.

It is recommended using these book pages as a starting point, and visit the online resources from the OpenAL website's documentation page for in-depth information. Downloading the OpenAL SDK is not required, but will provide you with some .wav files to toy around with and a few .pdf files not available directly at the OpenAL site.

Regarding compatibility, the "Generic Software" and "Generic Hardware" implementations of the OpenAL driver support OpenAL 1.1 and a few EFX Extensions, namely the Reverb Effect and Lowpass Filter. If the used Device cannot handle EAX natively, the driver will attempt to emulate the missing features.

Note that some functions of the OpenAL API are not imported for safety reasons. Rather use .Net's Thread.Sleep than Alut.Sleep, and Alut.CreateBuffer* instead of Alut.LoadMemory*. If this is a Problem, please voice it in the forum.

1. Devices, Buffers and X-Ram

OpenTK.Audio.AudioContext handles Device and Context allocation through Alc.

Instantiating a new AudioContext with a parameterless constructor will initialize the default Device and Context and makes it current. Calling the instance's Dispose method will destroy the Device and Context.

Buffers
Buffers in OpenAL are merely the storage for audio data in raw format, a Buffer Name (often called Handle) must be generated using AL.GenBuffers(). This buffer can now be filled using AL.BufferData().

X-Ram
The X-Ram Extension allows to manually assign Buffers a storage space, it's use is optional and not required. To use the Extension, the XRam wrapper must be instantiated (per used Device), which will take care of most ugly things with Extensions for you. The instantiated object contains a bool that returns if the Extension is usable, which should be checked before calling one of the Extension's Methods.

Example code:

var AC = new AudioContext();
var XRam = new XRamExtension(); // must be instantiated per used Device if X-Ram is desired.
 
// reserve 2 Handles
int[] MyBuffers = AL.GenBuffers(2);
 
if (XRam.IsInitialized)
{
    XRam.SetBufferMode(ref MyBuffer[0], XRamExtension.XRamStorage.Hardware); // optional
}
 
// Load a .wav file from disk. See example code at:
// https://github.com/opentk/opentk/blob/develop/Source/Examples/OpenAL/1.1/Playback.cs#L21
int channels, bits_per_sample, sample_rate;
var sound_data = LoadWave(
    File.Open(filename, FileMode.Open),
    out channels,
    out bits_per_sample,
    out sample_rate);
var sound_format =
    channels == 1 && bits_per_sample == 8 ? ALFormat.Mono8 :
    channels == 1 && bits_per_sample == 16 ? ALFormat.Mono16 :
    channels == 2 && bits_per_sample == 8 ? ALFormat.Stereo8 :
    channels == 2 && bits_per_sample == 16 ? ALFormat.Stereo16 :
    (ALFormat)0; // unknown
 
AL.BufferData(MyBuffers[0], sound_format, sound_data, sound_data.Length, sample_rate);
if (AL.GetError() != ALError.NoError)
{
   // respond to load error etc.
}
 
// Create a sinus waveform through parameters, this currently requires Alut.dll in the application directory
if (XRam.IsInitialized)
{
    XRam.SetBufferMode( ref MyBuffer[1], XRamStorage.Hardware ); // optional
}
MyBuffers[1] = Alut.CreateBufferWaveform(AlutWaveform.Sine, 500f, 42f, 1.5f);
 
// See next book page how to connect the buffers to sources in order to play them.
 
// Cleanup on application shutdown
AL.DeleteBuffers(MyBuffers.Length, MyBuffers); // free previously reserved Handles
AC.Dispose();

A description of the sound data in the Buffer can be queried using AL.GetBuffer().

Now that the Buffer Handle has been assigned a sound, we need to attach it to a Source for playback.

2. Sources and EFX

Sources
Sources represent the parameters how a Buffer Object is played back. These parameters include the Source's Position, Velocity, Gain (Volume amplification) and more. The settings can be set/get by using AL.Source and AL.GetSource functions.

Continuation of the sourcecode from previous page:

uint[] MySources = new uint[2];
AL.GenSources( 2, out MySources ); // gen 2 Source Handles
 
AL.Source( TestSources[0], ALSourcei.Buffer, (int)MyBuffers[0] ); // attach the buffer to a source
 
AL.SourcePlay( MySources[0]); // start playback
AL.Source( MySources[0], ALSourceb.Looping, true ); // source loops infinitely
 
AL.Source( MySources[1], ALSourcei.Buffer, (int)MyBuffers[1] );
Vector3 Position = new Vector3( 1f, 2f, 3f ); 
AL.Source( MySources[1], ALSource3f.Position, ref Position );
AL.Source( MySources[1], ALSourcef.Gain, 0.85f );
AL.SourcePlay( MySources[1] );
 
Console.ReadLine(); // wait for keystroke before exiting
 
AL.SourceStop( MySources[0] ); // halt playback
AL.SourceStop( MySources[1] );
 
AL.DeleteSources( 2, ref MySources ); // free Handles
// now delete Buffer Objects and dispose the AudioContext

EFX

I'm sorry to do this, but if you want to work with EFX there is no other way. All I can give here is a brief overview that might help you make the decision if EFX is what you need. You will have to download the OpenAL SDK to get a copy of "Effects Extension Guide.pdf" from Creative labs, for in-depth information about programming with DSPs.

My advice is ignoring EFX, unless your game project is in 1st Person 3D. Environmental effects might look nice as a "selling point" on paper, but do not add any gameplay value to a Strategy game, or a 2D platform game.

The addition to OpenAL with EFX Extension is the rerouting of output signals.

  • In vanilla OpenAL you load a Buffer, attach it to a Source and besides the Source's parameters that's all the influence you have about what ends up in the mixer.
  • With EFX you may reroute a source's output through Filters and/or into Auxiliary Effect Slots. This allows more fine control about how a Source sounds when played, which is useful to achieve the effect of obstruction, occlusion or exclusion of a sound due to environment features like walls, obstacles or doors.

The new OpenAL Objects that come with EFX are "Effect", "Auxiliary Effect Slot" and "Filter".

An Effect Object stores the type of effect and the values for parameters of that effect. Types of Effects are for example Echo, Distortion, Chorus, Flanger, etc.

Auxiliary Effect Slots are containers for Effect Objects, whose output goes directly into the final output mix. The Slots are only used if there is a valid Effect Object attached to them, binding the reserved Handle 0 to a Slot will detach the previously bound Effect Object from it.

A Filter can be attached to a source, and either filter the "dry signal" that goes directly into the output mixer, or filter the "wet signal" that is forwarded to an Auxiliary Effect Slot.

3. Context and Listener

Like in OpenGL, a Context can be understood as an instance of OpenAL State. You can create multiple Contexts per Device, but each Context has the restriction of 1 Listener it's own unique Sources. Buffer Objects on the other hand may be shared by Contexts, which use the same Device.

Note that in contrast to OpenGL, OpenAL does not have an equivalent to SwapBuffers(). A Sources are automatically played until the end of their attached Buffer is reached, or until the programmer manually stops the Source playback.

Listener
The Listener represents the position and orientation of the Camera in the environment, thus there can be only one per Context. The settings can be set/get by using AL.Source and AL.GetSource functions.

It makes sense to handle it together with your OpenGL camera, to make sure a Source is properly positioned. This is very similar to OpenGL's Projection Matrix, with the exception that there is no Frustum culling for audio (you may not see something behind you, but you can hear it).

A sample Camera, taken from the OpenAL manual:

void PlaceCamera(Vector3 ListenerPosition, float listenerAngle)
{
  // prepare some calculations
  float Sinus = (float)Math.Sin(listenerAngle);
  float Cosinus = (float)Math.Cos(listenerAngle);
  Vector3 ListenerTarget = new Vector3(ListenerPosition.X + Sinus, ListenerPosition.Y, ListenerPosition.Z - Cosinus);
  Vector3 ListenerDirection = new Vector3(Sinus, 0, Cosinus);
 
  // update OpenGL - camera position
  GL.MatrixMode(MatrixMode.Projection);
  GL.LoadIdentity();
  GL.Frustum(-0.1333, 0.1333, -0.1, 0.1, 0.2, 50.0);
  Glu.LookAt(ListenerPosition, ListenerTarget, Vector3.UnitY);
 
  // update OpenAL - place listener at camera
  AL.Listener(ALListener3f.Position, ref ListenerPosition);
  AL.Listener(ALListenerfv.Orientation, ref ListenerDirection, ref Vector3.UnitY);
}