Introduction
Welcome back! In our previous lesson, you successfully integrated the miniaudio library into our project and initialized its high-level audio engine. Your game engine now has the foundational component for a complete audio system.
Today, we will build directly on that foundation to tackle our next learning outcome: Load sound data from files (e.g., WAV) into memory. An engine isn't much use if it can't load assets, and sound is a critical one.
To align with your goal of understanding how things work "from the ground up," this lesson is structured in two parts. First, we'll explore the anatomy of a WAV file and the manual process of parsing it in C. This will connect back to what you've learned about file I/O, structs, and memory management. Then, for our actual engine, we will use miniaudio's powerful and convenient functions to handle this for us, ensuring our code is robust and extensible.
By the end of this lesson, you will be able to:
- Describe the chunk-based structure of a WAV file.
- Understand the steps required to manually parse a WAV file header and load its sample data.
- Use
miniaudioto load a complete sound file into memory with a single function call. - Integrate sound loading into our
audiomodule.
Part 1: The Anatomy of a WAV File
Before we can load a WAV file, we need to understand its structure. WAV is a container format based on the Resource Interchange File Format (RIFF). A RIFF file is simply a collection of "chunks," where each chunk contains a piece of data.
For a standard WAV file, there are three main chunks we care about:
- The 'RIFF' Chunk: This is the master chunk that contains the entire file. Its header includes the identifier "RIFF" and "WAVE".
- The 'fmt ' Chunk: (Note the space in 'fmt '). This chunk contains the format metadata: sample rate, number of channels (mono/stereo), bit depth (e.g., 16-bit), etc.
- The 'data' Chunk: This chunk contains the actual raw audio samples (the PCM data).

To parse this in C, we would define structs that match the layout of these chunks in the file.
Game Audio Programming Tutorial: Part 3 - Loading WAV Files
This article, while written for C++, provides an excellent breakdown of the WAV header structures and the logic for parsing them. The struct definitions are very similar to what we would use in C.
Read the section starting from "The WAV format is fairly standard..." down to the C++ code block defining the headers (RIFFHeader, WAVEFormatHeader, etc.). Focus on how these structs map to the file's binary layout.
The C equivalents of those C++ structs would look like this:
// Note: This requires specific integer types from <stdint.h>
#include <stdint.h>
// To ensure C structs have no padding, you might use a compiler-specific directive.
// For GCC/Clang, it's __attribute__((packed)). The C++ #pragma pack(1) does the same.
// This forces the compiler to lay out the struct members sequentially in memory,
// matching the file format exactly.
struct RIFFHeader {
char headerID[4]; // Contains "RIFF"
int32_t totalFileSize;
char fileType[4]; // Contains "WAVE"
} __attribute__((packed));
struct WAVEFormatHeader {
char headerID[4]; // Contains "fmt "
int32_t sectionSize;
int16_t audioFormat; // 1 for PCM
int16_t channelCount;
int32_t sampleRate;
int32_t byteRate;
int16_t blockAlign;
int16_t bitsPerSample;
} __attribute__((packed));
struct WAVEDataHeader {
char headerID[4]; // Contains "data"
int32_t sectionSize; // Size of the raw audio data
} __attribute__((packed));
Part 2: The Manual Approach to Loading WAV Data
Now that you know the structure, how would you load it manually? This is a great exercise that reinforces your C programming skills. The process involves opening the file in binary mode and reading its contents sequentially into your structs.
This short video demonstrates the entire process concisely using standard C library functions.
Watch this video by Nick Walton to see a practical, step-by-step implementation of a WAV loader in C. This will solidify the theoretical structure we just discussed.
Watch the full video (it's short). Pay close attention to the sequence of operations: fopen the file in "rb" (read binary) mode. fread the header chunks and check for the correct identifiers ("RIFF", "WAVE", "fmt ", "data"). Get the data size from the data chunk's header. Allocate memory for the samples using malloc. fread the sample data into your newly allocated buffer. fclose the file.
One complexity the video simplifies is the possibility of extra, optional chunks (like for metadata) appearing between the fmt and data chunks. A more robust parser wouldn't assume the data chunk immediately follows the fmt chunk. Instead, it would loop through chunks until it finds the one named "data".
For a more detailed look at that specific problem, the following video segment explains why you might need to search for the data chunk and how to implement that search.
EP0024 - Loading wav files from disk and playing sound - Making a video game from scratch in C
This segment from a video by Ryan Ries provides a great explanation for why the 'data' chunk's position isn't always fixed and demonstrates a loop to find it.
Watch from 28:00 to 32:20. You don't need to worry about the Windows-specific API calls; focus on the logic of reading 4-byte chunk IDs in a loop until the "data" ID is found.
Manually parsing files like this gives you a deep appreciation for what libraries do behind the scenes. However, for our engine, we will use a more powerful and less error-prone method.
Part 3: The miniaudio Way: Simple and Robust Loading
While manual parsing is a fantastic learning experience, writing a parser that can robustly handle all variations of WAV files—not to mention other formats like MP3 or FLAC—is a significant project in itself.
This is exactly why we integrated miniaudio. It includes high-quality, pre-built decoders for many popular audio formats. We can leverage these to load a sound with a single, simple function call.
We will use the ma_sound object, which is part of miniaudio's high-level API. An ma_sound represents a sound that can be played, paused, and manipulated. The function ma_sound_init_from_file performs all the steps we discussed—opening the file, parsing headers, decoding, and allocating memory—and prepares the sound for playback with our existing audio engine.
Part 4: Implementing Sound Loading in Our Module
Let's integrate this into our audio module. We will define a new type for our sounds and create functions to load and unload them.
First, you'll need a WAV file to test with. A great tool for generating simple, public-domain sound effects is bfxr.net. Create a sound (e.g., a "pickup/coin" sound), export it as a .wav file, and place it in a new assets folder in your project directory.
1. Update audio.h
We'll add a type definition for our sound object and declare our new load and unload functions.
// src/audio.h
#ifndef AUDIO_H
#define AUDIO_H
#include <stdbool.h>
#include "../vendor/miniaudio.h" // We need the miniaudio types like ma_sound
// Define our own sound type that is just a ma_sound object
typedef ma_sound audio_sound_t;
// Initializes the audio engine.
bool audio_init(void);
// Shuts down the audio engine.
void audio_shutdown(void);
// Loads a sound from a file and prepares it for playback.
bool audio_sound_load(audio_sound_t* sound, const char* file_path);
// Unloads a sound and frees its resources.
void audio_sound_unload(audio_sound_t* sound);
#endif // AUDIO_H
2. Implement the Functions in audio.c
Now, we'll write the implementation. The ma_sound_init_from_file function needs a pointer to our engine, which is conveniently available as a static global variable inside audio.c.
// src/audio.c
#define MINIAUDIO_IMPLEMENTATION
#include "../vendor/miniaudio.h"
#include "audio.h"
#include <stdio.h>
#include <assert.h> // For checking if the engine is running
static ma_engine engine;
// ... audio_init() and audio_shutdown() implementations from the previous lesson ...
bool audio_sound_load(audio_sound_t* sound, const char* file_path) {
// Sanity check to ensure the audio engine was initialized before we try to use it.
// The ma_engine_is_started() function is provided by miniaudio.
assert(ma_engine_is_started(&engine));
// The flags parameter allows for customization, like streaming.
// 0 means default behavior: decode the entire file and store it in memory.
// The group and pFence parameters are for advanced use cases, so we pass NULL.
ma_result result = ma_sound_init_from_file(&engine, file_path, 0, NULL, NULL, sound);
if (result != MA_SUCCESS) {
printf("Failed to load sound file '%s': %s\n", file_path, ma_result_description(result));
return false;
}
printf("Successfully loaded sound: %s\n", file_path);
return true;
}
void audio_sound_unload(audio_sound_t* sound) {
// ma_sound_uninit() handles freeing memory and other cleanup.
ma_sound_uninit(sound);
}
3. Test in main.c
Finally, let's update main.c to test our new functionality. We'll load the sound after initializing the audio system and unload it before shutting down.
// src/main.c
#include <stdio.h>
// ... other includes ...
#include "audio.h"
int main(void) {
// ... GLFW init, window creation ...
if (!audio_init()) {
printf("Failed to initialize audio system.\n");
// ... cleanup and exit ...
return -1;
}
// --- Test Sound Loading ---
audio_sound_t my_sound;
if (!audio_sound_load(&my_sound, "assets/your_sound_file.wav")) {
printf("Failed to load test sound.\n");
audio_shutdown();
// ... other cleanup ...
return -1;
}
// --------------------------
while (!glfwWindowShouldClose(window)) {
// ... game loop ...
}
// --- Test Sound Unloading ---
audio_sound_unload(&my_sound);
// ----------------------------
audio_shutdown();
// ... other cleanup ...
return 0;
}
Recompile and run your program. If everything is correct, you should see your "Successfully loaded sound" message in the console.
Conclusion
Fantastic work! You've now implemented a crucial piece of your engine's infrastructure. You not only learned the practical way to load sounds using a library but also dug into the underlying theory of how a WAV file is structured, satisfying your goal of building from the ground up.
Key Takeaways:
- WAV files use a RIFF structure composed of chunks (
fmt,data, etc.). - Manual parsing involves reading file headers into C
structs, allocating memory withmalloc, and reading sample data withfread. - Compiler directives like
__attribute__((packed))or#pragma pack(1)are crucial for ensuringstructmemory layout matches the file format. - For practical engine development, using a library's decoder like
miniaudio's is far more robust and efficient. - The
ma_sound_init_from_filefunction handles all the complexities of decoding and memory management, loading a sound, and preparing it for playback.
Preview of the Next Lesson:
Our sound is now loaded into memory, but it's silent. In the next lesson, "Create functions to play one-shot sound effects and looping background music," we will make some noise! We'll add functions to our audio module to trigger the sounds we've loaded, bringing your game world to life.