Skip to main content

Welcome to a Grasp lesson

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

Lesson 72

Setting Up Audio Libraries in C

Recommended resources

Introduction

Welcome to Module 12: Audio Integration! In the last module, we put the finishing touches on our simple physics engine by implementing friction, adding a significant layer of realism to object interactions. Your game world now has a solid foundation for physical simulation.

Now, we're going to bring that world to life with sound. An immersive game is as much about what you hear as what you see. Over the next few lessons, we'll build a complete audio system from the ground up.

Today's lesson addresses our first audio-related learning outcome: Set up an audio library like OpenAL or miniaudio in the C project. We will be using miniaudio, a powerful and easy-to-use C library.

By the end of this lesson, you will have:

  1. Understood the concept of a single-file C library.
  2. Integrated miniaudio into your project structure.
  3. Initialized the miniaudio "engine," which will be the core of our audio system.
  4. Learned how to compile and link your project with the new audio library.

This will lay the essential groundwork for our next lesson, where we'll start loading and playing sounds.


Part 1: Choosing and Understanding miniaudio

There are many audio libraries available, such as OpenAL, FMOD, and others. For our project, we will use miniaudio. It's an excellent choice for several reasons, which align well with our goal of building an engine from scratch.

To get a quick overview of why miniaudio is a great fit, let's watch the beginning of this introductory video.

MINIAUDIO -- Audio Playback and Recording Library

This video from the Gamefromscratch channel provides a concise summary of miniaudio's features. Focus on why it's so convenient for a C project.

Watch from the beginning until 02:09. Pay attention to these key points: It's a single-file library written in C. It has no external dependencies (besides the C standard library). It is cross-platform (Windows, macOS, Linux, etc.). It has a simple, flexible API.

The most important feature for us is that miniaudio is a single-file header library. Since you're coming from Python and C#, where dependencies are often managed by package managers or by linking to pre-compiled binaries (.dll, .so), this concept might be new.

In C, a single-file library bundles both the function declarations (what functions exist) and their implementations (the actual code) into a single .h file. This makes integration incredibly simple: you just add one file to your project.

To control whether you get just the declarations or the full implementation, you use a special preprocessor macro. This is a common pattern in modern C libraries.

Building miniaudio

The following post on the Handmade Network forum shows the exact pattern for including and implementing miniaudio.

Read the short section titled "Building". It shows a two-line code snippet. This is the core mechanism for using single-file libraries.

Here's the pattern:




#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
  • #include "miniaudio.h": By default, this just includes the function and struct declarations, so your code knows about types like ma_engine and functions like ma_engine_init(). You can do this in any file that needs to use the audio API.
  • #define MINIAUDIO_IMPLEMENTATION: When you define this macro before the #include directive in one single C file, it tells the compiler to also include the entire implementation (the source code) for all those functions.

This prevents "multiple definition" errors from the linker, which would occur if you included the full implementation in more than one place.


Part 2: Integrating miniaudio into Your Project

Let's get miniaudio set up.

  1. Download the Library: Go to the official miniaudio documentation page linked below and download the miniaudio.h file. You can usually find a direct link on the homepage or on their GitHub repository.
  2. Add it to Your Project: Create a new folder in your project directory, perhaps named vendor or external, and place the miniaudio.h file inside it. This keeps third-party code separate from your own.

Now, we'll create a new module in our engine to handle audio, just as we did for physics and rendering. This promotes good organization.

Create two new files: src/audio.h and src/audio.c.

src/audio.h
This file will contain the public interface for our audio system. For now, it will be very simple.




#ifndef AUDIO_H
#define AUDIO_H




#include <stdbool.h>

// Initializes the audio engine. Returns true on success, false on failure.
bool audio_init(void);

// Shuts down the audio engine and cleans up resources.
void audio_shutdown(void);




#endif // AUDIO_H

src/audio.c
This is where we will provide the implementation for our audio functions. This is also the one and only file where we will define MINIAUDIO_IMPLEMENTATION.




#define MINIAUDIO_IMPLEMENTATION
#include "../vendor/miniaudio.h" // Adjust the path as needed




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

// We will define a global engine object here
static ma_engine engine;

bool audio_init(void) {
    // Implementation will go here in the next step
    printf("Audio system initialized.\n");
    return true;
}

void audio_shutdown(void) {
    // Implementation will go here in the next step
    printf("Audio system shut down.\n");
}

Part 3: The High-Level Engine API

miniaudio offers two ways to work with it: a low-level API and a high-level API.

  • Low-Level API: Gives you direct control over the audio device. You manage buffers and send raw audio data through callbacks. It's powerful but requires more setup.
  • High-Level API (The "Engine"): Provides a simpler, higher-level abstraction. It encapsulates resource management, mixing, and effects into an ma_engine object. For game development, this is often the most convenient starting point.

We will use the high-level Engine API. It's easy to use and powerful enough for our needs.

miniaudio Programming Manual - High Level API

Let's read the official documentation's introduction to the high-level API to understand what the ma_engine does for us.

Read the first few paragraphs of the section "1.2. High Level API". Focus on understanding that the engine wraps a resource manager and a node graph, and how to initialize it with ma_engine_init() and ma_engine_uninit().

As the documentation describes, we use a config/init pattern. We initialize a configuration object, pass it to an init function, and then uninit when we're done.

Let's implement this in our audio.c file.

Updated src/audio.c




#define MINIAUDIO_IMPLEMENTATION
#include "../vendor/miniaudio.h" // Adjust path if needed




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

// A global engine object for our audio system
static ma_engine engine;

bool audio_init(void) {
    ma_result result;

    // The `ma_engine_config` is used to customize the engine.
    // `ma_engine_config_init()` gives us a default configuration.
    ma_engine_config config = ma_engine_config_init();

    // Initialize the engine with the default config.
    // The first parameter is the config, the second is a pointer to our engine object.
    result = ma_engine_init(&config, &engine);
    if (result != MA_SUCCESS) {
        printf("Failed to initialize audio engine: %s\n", ma_result_description(result));
        return false;
    }

    printf("Audio engine initialized successfully.\n");
    return true;
}

void audio_shutdown(void) {
    // Uninitialize the engine to free resources.
    ma_engine_uninit(&engine);
    printf("Audio engine shut down.\n");
}

Part 4: Compiling and Final Integration

The last step is to call our new functions from main() and update our build process.

src/main.c
Make sure to include your new header and call the init/shutdown functions.




#include "audio.h" // Add this include

// ... other includes ...

int main(void) {
    // ... window creation, etc. ...
    
    // Initialize audio system
    if (!audio_init()) {
        // Handle initialization failure
        //...
        return -1;
    }

    // ... main game loop ...
    while (!glfwWindowShouldClose(window)) {
        // ... input, physics, rendering ...
    }

    // Shutdown audio system
    audio_shutdown();
    
    // ... other cleanup ...
    
    return 0;
}

Compilation and Linking

This is a critical step. miniaudio uses system-level libraries for audio I/O, and we need to tell our compiler to link against them. You will need to modify your Makefile or build script.

The exact flags depend on your operating system.

miniaudio Programming Manual - Building

The miniaudio manual provides a clear guide on the required build flags for each platform. This is essential for a successful compilation.

Read sections 2.1 (Windows), 2.2 (macOS), and 2.3 (Linux). Find the section for your operating system and identify the required linker flags.

Here's a summary of the required linker flags:

  • Windows (MinGW/GCC): Usually, no extra flags are needed. miniaudio handles it.
  • Linux: You must add -ldl -lpthread -lm to your linker command.
  • macOS: You must add -lpthread -lm to your linker command.

For example, if you are on Linux and using GCC, your compile command might look like this:

gcc -o game main.c audio.c ... -lglfw -lglad -lm -ldl -lpthread

Important: Make sure you add audio.c to the list of source files you are compiling!

After updating your build script, compile and run your program. If everything is set up correctly, you should see your "Audio engine initialized successfully" message printed to the console when the program starts, and the shutdown message when it closes.


Conclusion

Excellent work! You have successfully integrated a third-party audio library into your C project. This is a foundational skill in game engine development, as no engine is built entirely from scratch without leveraging existing libraries for complex tasks like audio, physics, or font rendering.

Key Takeaways:

  • miniaudio is a powerful, cross-platform, single-file C library for audio.
  • Single-file libraries use a #define ..._IMPLEMENTATION macro in one C file to include the library's source code.
  • It's good practice to wrap third-party libraries in your own engine module (e.g., audio.c and audio.h) for better organization and control.
  • The miniaudio high-level "engine" API (ma_engine) is a simple and effective way to manage audio in a game.
  • Integrating libraries often requires adding new source files to your build and updating linker flags for your platform.

Preview of the Next Lesson:
With the audio engine initialized and running, we're ready for the fun part. In our next lesson, we will tackle the next outcome: Load sound data from files (e.g., WAV) into memory. We'll learn how to use miniaudio's decoding capabilities and create functions to play one-shot sound effects.