Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 76

Implementing Basic 3D Positional Audio

Recommended resources

Introduction

Welcome to the next lesson on audio integration! In our previous session, you successfully implemented functions to control both per-source and global volume using miniaudio. This gave you fine-grained control over the loudness of sounds in your project.

However, our sounds still exist in a void; they have no position in the game world. To create an immersive 3D experience, we need sounds to emanate from specific locations. This lesson directly addresses the learning outcome: Implement basic 3D positional audio by updating a sound's position relative to a listener.

To achieve this, we will move from the high-level miniaudio API to a more powerful, industry-standard library called OpenAL (Open Audio Library). While miniaudio was excellent for getting started, OpenAL gives us the direct, low-level control needed to build a sophisticated 3D audio system, which aligns perfectly with your goal of building an engine from the ground up.

In this lesson, you will learn to:

  • Understand the core concepts of OpenAL: the Listener, Sources, and Buffers.
  • Refactor your audio module to use OpenAL for initialization and sound playback.
  • Position a sound source in 3D space and hear its direction change relative to a stationary listener.
  • Control how a sound's volume attenuates (fades) with distance.

1. The Core Concepts of 3D Audio in OpenAL

OpenAL represents 3D audio space using three fundamental objects. Your experience with chemical engineering concepts like fields, sources, and sensors may provide a helpful parallel here.

  • The Listener: This is a single object representing the "ears" of the player in the game world. It has a position, a velocity (for the Doppler effect), and an orientation (a "forward" vector and an "up" vector to know which way it's facing). All sounds are heard relative to the Listener.
  • Sources: These are the points in 3D space that emit sound. You can have many sources in a scene. Each source also has a position, velocity, and properties like pitch and gain (volume).
  • Buffers: These are memory containers for the actual audio data (e.g., the raw sound samples from a WAV file). To play a sound, you load the data into a Buffer and then attach that Buffer to a Source.
OpenAL Architecture for 3D Positional Audio
This diagram illustrates the relationship between Buffers, Sources, and the Listener. Audio data from memory (Buffers) is attached to sound emitters (Sources) in the 3D world, and everything is heard from the perspective of the single Listener.

Our goal is to update the position of a Source in real-time and have OpenAL automatically calculate how it should sound based on its position relative to the Listener. This includes both panning (left/right balance) and volume attenuation.

Let's watch a short video that walks through the essential code for setting up the Listener and Sources.

How to setup and use OpenAL for game audio in C++ (Using Visual Studio) tutorial

This video from enigma tutorials provides a clear, practical demonstration of setting up OpenAL. Focus on how the listener and source properties are configured.

Watch from 08:56 to 12:59. First, observe how the listener's position, velocity, and orientation are set. Then, see how a sound source is created and how its 3D position is configured using alSource3f.

As you saw, the process involves:

  1. Initializing OpenAL: Opening an audio device and creating a context.
  2. Configuring the Listener: Setting its position and orientation in the world. Usually, this is tied to the game's camera.
  3. Preparing Sound Data: Loading a WAV file into a buffer.
  4. Configuring a Source: Creating a source, setting its position, and attaching a buffer to it.

2. Refactoring the Audio Module for OpenAL

We will now refactor our audio module to use OpenAL. This involves replacing the miniaudio functions with their OpenAL equivalents.

For this lesson, we will simplify audio file loading by using dr_wav, a single-header WAV loading library. You can add it to your project by downloading dr_wav.h from its repository and placing it in your includes directory. We are switching from miniaudio's built-in decoding to give us more direct access to the raw PCM data that OpenAL needs.

2.1. Updating audio.h

Our new header will define a structure to hold a positional audio source and the functions to control it. Notice we are moving away from the fire-and-forget audio_play_sfx to a model where we manage a persistent audio_source object.

// src/audio.h




#ifndef AUDIO_H
#define AUDIO_H




#include <AL/al.h> // Include OpenAL headers
#include <AL/alc.h>

// A structure to hold a positional sound source
typedef struct {
    ALuint buffer;
    ALuint source;
} audio_source_t;

// Initializes the OpenAL audio system
int audio_init(void);

// Shuts down the OpenAL audio system
void audio_shutdown(void);

// Loads a WAV file into an audio_source_t
// This creates an OpenAL buffer and source.
int audio_source_load(audio_source_t* audio_src, const char* filepath);

// Unloads resources for an audio source
void audio_source_unload(audio_source_t* audio_src);

// Sets the 3D position of the audio source
void audio_source_set_position(audio_source_t* audio_src, float x, float y, float z);

// Plays the audio source
void audio_source_play(audio_source_t* audio_src);

// Sets the source to loop
void audio_source_set_looping(audio_source_t* audio_src, int should_loop);




#endif // AUDIO_H

2.2. Implementing audio.c

This is where the core logic resides. We'll initialize OpenAL, set up the listener, and implement the functions declared in the header.

Important Note: The following code omits detailed error checking for brevity, but in a real engine, you should check the return values of most OpenAL calls. The video you watched earlier demonstrated a good macro-based approach for this.

// src/audio.c




#include "audio.h"
#include <stdio.h>




#define DR_WAV_IMPLEMENTATION
#include "dr_wav.h" // Make sure this is in your include path

// OpenAL context and device handles
static ALCdevice* device;
static ALCcontext* context;

int audio_init(void) {
    device = alcOpenDevice(NULL); // Open the default audio device
    if (!device) {
        printf("Failed to open audio device.\n");
        return 0;
    }

    context = alcCreateContext(device, NULL);
    if (!context) {
        printf("Failed to create OpenAL context.\n");
        alcCloseDevice(device);
        return 0;
    }
    alcMakeContextCurrent(context);

    // Set listener position at the origin
    alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f);
    // Set listener velocity (for Doppler effect, not used in this lesson)
    alListener3f(AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    
    // Set listener orientation: at(0,0,-1) and up(0,1,0)
    ALfloat orientation[] = { 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f };
    alListenerfv(AL_ORIENTATION, orientation);

    printf("OpenAL Initialized.\n");
    return 1;
}

void audio_shutdown(void) {
    alcMakeContextCurrent(NULL);
    alcDestroyContext(context);
    alcCloseDevice(device);
}

int audio_source_load(audio_source_t* audio_src, const char* filepath) {
    // --- Load WAV file using dr_wav ---
    unsigned int channels;
    unsigned int sample_rate;
    drwav_uint64 total_pcm_frame_count;
    short* pcm_data = drwav_open_file_and_read_pcm_frames_s16(filepath, &channels, &sample_rate, &total_pcm_frame_count, NULL);
    if (pcm_data == NULL) {
        printf("Failed to load WAV file: %s\n", filepath);
        return 0;
    }

    // Determine OpenAL format
    ALenum format;
    if (channels == 1) {
        format = AL_FORMAT_MONO16;
    } else if (channels == 2) {
        format = AL_FORMAT_STEREO16;
    } else {
        printf("Unsupported channel count: %d\n", channels);
        drwav_free(pcm_data, NULL);
        return 0;
    }
    // Positional audio only works with mono sounds!
    if (channels > 1) {
        printf("Warning: Positional audio works best with MONO sounds. %s is stereo.\n", filepath);
    }

    // --- Create OpenAL Buffer ---
    alGenBuffers(1, &audio_src->buffer);
    alBufferData(audio_src->buffer, format, pcm_data, total_pcm_frame_count * channels * sizeof(short), sample_rate);
    drwav_free(pcm_data, NULL); // Data is now in the OpenAL buffer

    // --- Create OpenAL Source ---
    alGenSources(1, &audio_src->source);
    alSourcef(audio_src->source, AL_PITCH, 1.0f);
    alSourcef(audio_src->source, AL_GAIN, 1.0f);
    alSource3f(audio_src->source, AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(audio_src->source, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSourcei(audio_src->source, AL_LOOPING, AL_FALSE);

    // Attach buffer to source
    alSourcei(audio_src->source, AL_BUFFER, audio_src->buffer);
    
    return 1;
}

void audio_source_unload(audio_source_t* audio_src) {
    alDeleteSources(1, &audio_src->source);
    alDeleteBuffers(1, &audio_src->buffer);
}

void audio_source_set_position(audio_source_t* audio_src, float x, float y, float z) {
    alSource3f(audio_src->source, AL_POSITION, x, y, z);
}

void audio_source_play(audio_source_t* audio_src) {
    alSourcePlay(audio_src->source);
}

void audio_source_set_looping(audio_source_t* audio_src, int should_loop) {
    alSourcei(audio_src->source, AL_LOOPING, should_loop ? AL_TRUE : AL_FALSE);
}

3. Making Distance Matter: Attenuation

Setting the source's position handles directionality, but a real-world sound also gets quieter as it moves away. This effect is called attenuation. OpenAL allows you to choose a distance model that defines the mathematical formula for how gain changes with distance.

Let's explore this concept with a focused tutorial.

OpenAL 3D Audio Tutorial 4: Distance Attenuation

This tutorial by ThinMatrix is an excellent explanation of distance attenuation in OpenAL. It covers the different models and the parameters you can tweak.

Watch from the beginning to 05:22. Pay attention to: The purpose of alDistanceModel. The three key source parameters: AL_ROLLOFF_FACTOR, AL_REFERENCE_DISTANCE, and AL_MAX_DISTANCE. The difference between exponential, inverse, and linear models.

The most common and default model is AL_INVERSE_DISTANCE_CLAMPED. It provides a realistic-sounding falloff that is highly tunable.

  • AL_ROLLOFF_FACTOR: Controls how sharply the volume drops off. A higher value means the sound fades more quickly with distance.
  • AL_REFERENCE_DISTANCE: The distance at which the sound is at 100% volume. Within this distance, the volume is clamped and does not get any louder.
  • AL_MAX_DISTANCE: The distance after which the volume is clamped and does not get any quieter.

Let's read a bit more about the formula and see some graphs that illustrate how these parameters affect the attenuation curve.

The Complete Guide to OpenAL with C++ Part 3: Positioning Sounds

This article from IndieGameDev provides clear graphs and the formula for the default AL_INVERSE_DISTANCE_CLAMPED model, making it easy to understand the effect of each parameter.

Start reading from the section titled 'Parameters used in OpenAL Attenuation'. Study the formula and the three graphs showing how AL_ROLLOFF_FACTOR, AL_REFERENCE_DISTANCE, and AL_MAX_DISTANCE each influence the final gain.

To use these, we can set them on a source-by-source basis. While we could add another function to our audio module, for now, we can simply set them directly in audio_source_load for simplicity.

Let's add the following lines inside audio_source_load in audio.c, just after setting the looping property:

// Inside audio_source_load in audio.c, after alSourcei(..., AL_LOOPING, ...):

alSourcef(audio_src->source, AL_ROLLOFF_FACTOR, 1.0f);
alSourcef(audio_src->source, AL_REFERENCE_DISTANCE, 6.0f);
alSourcef(audio_src->source, AL_MAX_DISTANCE, 50.0f);

These values mean the sound will be at full volume up to 6 units away, then start fading, with the sharpness of the fade controlled by the rolloff factor.


4. Practical Application in main.c

Now, let's test our new positional audio system. We will create a simple scene where a looping sound source moves back and forth along the X-axis in front of the listener. Put on headphones for the best effect!

// src/main.c (replace previous main content)




#include <stdio.h>
#include <math.h>
#include <GLFW/glfw3.h>
#include "audio.h"

int main(void) {
    if (!glfwInit()) {
        return -1;
    }

    GLFWwindow* window = glfwCreateWindow(640, 480, "Positional Audio Test", NULL, NULL);
    if (!window) {
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    if (!audio_init()) {
        glfwTerminate();
        return -1;
    }

    // Load a MONO sound effect.
    // You can find free mono WAV files online (e.g., a simple engine idle or beep).
    audio_source_t moving_sound;
    if (!audio_source_load(&moving_sound, "assets/your_mono_sound.wav")) {
        audio_shutdown();
        glfwTerminate();
        return -1;
    }

    // Set the sound to loop and start playing
    audio_source_set_looping(&moving_sound, 1);
    audio_source_play(&moving_sound);

    printf("A sound source is moving left and right. Listen for the panning and volume changes.\n");
    printf("Listener is at (0,0,0).\n");

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        // Animate the sound source's position
        // It will move back and forth along the x-axis from -15 to 15
        float time = (float)glfwGetTime();
        float x_pos = 15.0f * sin(time * 0.5f);
        
        // Update the position of our sound source in the audio engine
        audio_source_set_position(&moving_sound, x_pos, 0.0f, -5.0f);
        
        printf("Sound source at: (%.2f, 0.00, -5.00)\r", x_pos);
        fflush(stdout);

        // --- Rendering would go here ---
        glClear(GL_COLOR_BUFFER_BIT);
        glfwSwapBuffers(window);
    }

    // --- Cleanup ---
    audio_source_unload(&moving_sound);
    audio_shutdown();
    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

Compile and run this code. You should hear the sound continuously playing. As it moves from left to right, you will hear it pan between your speakers. As it moves toward the center (closer to the listener's AL_REFERENCE_DISTANCE), it will get slightly louder, and as it moves to the extremes, it will get quieter due to attenuation.


Conclusion

Excellent work! You have successfully integrated OpenAL and implemented a true 3D positional audio system. You replaced your simpler miniaudio-based system with a more powerful, low-level library, bringing you one step closer to a feature-complete game engine.

Key Takeaways:

  • 3D audio is primarily modeled with a Listener (the ears) and multiple Sources (the sound emitters).
  • OpenAL is a powerful, low-level C API that provides the necessary functions to manage these objects.
  • Positional audio is a combination of directionality (panning), which OpenAL handles based on relative AL_POSITION, and attenuation (volume falloff), which is controlled by the active distance model and parameters like AL_ROLLOFF_FACTOR.
  • Positional audio requires mono sound files for OpenAL to spatialize them correctly.

Preview of the Next Lesson:
In this lesson, we loaded a single audio file. In a real game, you'll have hundreds of assets: 3D models, textures, sounds, and more. Loading them one by one isn't scalable. Our next topic is "Resource Management and Model Loading," where we will begin to tackle this problem by learning how to parse a standard 3D model format (.obj) and build a system to manage these assets efficiently.