Introduction
Welcome back! In our last lesson, you successfully created the functions to load sound data from a file into memory using miniaudio. Your audio_sound_load function can take a file path and populate an audio_sound_t object, but right now, our engine remains silent.
Today, we'll bring your game world to life with sound. We'll focus on the learning outcome: Create functions to play one-shot sound effects and looping background music. These are the two most fundamental audio playback patterns in game development. We will explore:
- The conceptual difference between short, event-driven sound effects and long, atmospheric background music.
- How to play "fire and forget" sounds that the engine manages automatically.
- How to play, loop, and stop a continuous music track that is streamed from disk to conserve memory.
By the end of this lesson, you will have a robust set of functions in your audio module to handle these essential tasks.
One-Shots vs. Streaming: Why and How
In a game, not all sounds are treated equally. A quick sound effect for a button click has very different requirements than a five-minute music track for a level.
This document, while based on the OpenAL library, provides excellent conceptual explanations for these two types of sounds. We'll use its ideas but implement them with miniaudio.
Please read the following two sections: 'Triggering Audio & Global Audio': This section explains the need for 'one-off' sound events, which we'll call one-shot effects. It describes sounds that are triggered by an action and don't need complex management. 'Streaming Audio': This section explains why it's not feasible to load large audio files, like music, completely into memory and introduces the concept of streaming.
As you've read, the two main categories are:
- One-Shot Sounds: Short, triggered effects (e.g., footsteps, laser blasts, UI feedback). These are small enough to be fully loaded into memory. We want a simple way to "fire and forget" them.
- Streaming Sounds (Music): Long audio tracks. Loading an entire song into memory is inefficient. Instead, we stream it: the engine loads and plays the audio in small chunks, discarding old chunks and loading new ones as it goes. This keeps the memory footprint low.
Now, let's see how miniaudio's high-level engine API helps us implement both.
Part 1: Playing One-Shot Sound Effects
miniaudio provides a wonderfully simple way to play a sound that you don't need to control after it starts. This is perfect for our one-shot effects.
The "Fire and Forget" Method
The function ma_engine_play_sound is the key here. You give it the path to a sound file, and it handles everything: loading, playing, and cleaning up the resources once the sound has finished.
Behind the scenes, the engine maintains a pool of ma_sound objects. When you call this function, it grabs an available sound object from the pool, loads your audio file into it, plays it, and returns the object to the pool when it's done.

Let's create a function in our audio module for this.
1. Update audio.h
Declare a new function to play a sound effect.
// src/audio.h
// ... (previous declarations) ...
// Plays a sound effect one time. "Fire and forget".
void audio_play_sfx(const char* file_path);
2. Implement in audio.c
The implementation is a straightforward wrapper around the miniaudio function.
// src/audio.c
// ... (static engine, init, shutdown, and load/unload functions) ...
void audio_play_sfx(const char* file_path) {
assert(ma_engine_is_started(&engine));
// The last parameter is for grouping sounds, which we aren't using yet.
ma_engine_play_sound(&engine, file_path, NULL);
}
This is the simplest way to get sound playing, ideal for UI sounds or simple effects. For effects that you might want to stop mid-play (like a machine gun burst), you would use the audio_sound_load function from the last lesson and then start it with ma_sound_start, but we'll focus on the "fire and forget" pattern for now.
Part 2: Playing Looping Background Music
For background music, we need three things:
- Streaming: Load the sound with a flag that tells
miniaudionot to load the whole file into RAM. - Looping: Tell the sound to start over when it reaches the end.
- Control: Start and stop the music on command.
We will create a set of functions specifically for handling a music track.
The miniaudio documentation provides all the functions we need. Let's look at the relevant parts.
Skim through these sections to find the key functions: In the first section, find how to initialize an ma_sound and the functions to start and stop it. In the second section, find the function ma_sound_set_looping(). In the third section, find the flag MA_SOUND_FLAG_STREAM which is used when initializing a sound.
As you saw, the key functions are:
ma_sound_init_from_filewith theMA_SOUND_FLAG_STREAMflag.ma_sound_set_loopingto enable looping.ma_sound_startto begin playback.ma_sound_stopto halt playback.
Let's build this into our audio module.
1. Update audio.h
We'll add a separate set of functions for music streams. We will use the same audio_sound_t type, but our functions will treat it as a music track.
// src/audio.h
// ... (previous declarations) ...
// Loads a music track for streaming.
bool audio_music_load(audio_sound_t* music_track, const char* file_path);
// Plays the loaded music track. Will loop automatically.
void audio_music_play(audio_sound_t* music_track);
// Stops the loaded music track.
void audio_music_stop(audio_sound_t* music_track);
// Unloads the music track.
void audio_music_unload(audio_sound_t* music_track);
Note: You might notice audio_music_unload is identical to audio_sound_unload from the previous lesson. We'll create it for semantic clarity in our API.
2. Implement in audio.c
Now, let's write the code for these functions.
// src/audio.c
// ... (other functions) ...
bool audio_music_load(audio_sound_t* music_track, const char* file_path) {
assert(ma_engine_is_started(&engine));
// The key difference is the MA_SOUND_FLAG_STREAM flag.
// This tells miniaudio to stream the file from disk instead of loading it all into memory.
ma_result result = ma_sound_init_from_file(&engine, file_path, MA_SOUND_FLAG_STREAM, NULL, NULL, music_track);
if (result != MA_SUCCESS) {
printf("Failed to load music stream '%s': %s\n", file_path, ma_result_description(result));
return false;
}
printf("Successfully loaded music stream: %s\n", file_path);
return true;
}
void audio_music_play(audio_sound_t* music_track) {
// Set the sound to loop. MA_TRUE is just 1.
ma_sound_set_looping(music_track, MA_TRUE);
ma_sound_start(music_track);
}
void audio_music_stop(audio_sound_t* music_track) {
ma_sound_stop(music_track);
}
void audio_music_unload(audio_sound_t* music_track) {
// This is the same as unloading a regular sound.
ma_sound_uninit(music_track);
}
Part 3: Testing in main.c
Let's put it all together. You'll need two audio files in your assets folder for this:
- A short sound effect (e.g.,
coin.wav). You can generate one at bfxr.net. - A longer music file (e.g.,
music.mp3ormusic.ogg).miniaudiosupports these formats out of the box! You can find royalty-free music on sites like freepd.com.
Update your main.c to load and play both.
// src/main.c
#include <stdio.h>
#include <GLFW/glfw3.h> // We need this for key input
#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;
}
// --- Load Music ---
audio_sound_t background_music;
if (!audio_music_load(&background_music, "assets/your_music_file.mp3")) {
printf("Failed to load background music.\n");
audio_shutdown();
// ... other cleanup ...
return -1;
}
// Start playing the music right away
audio_music_play(&background_music);
// We'll use this to prevent firing the sound effect every frame
bool space_was_pressed = false;
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// --- Trigger Sound Effect on Key Press ---
if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) {
if (!space_was_pressed) {
printf("Playing SFX!\n");
audio_play_sfx("assets/your_sfx_file.wav");
space_was_pressed = true;
}
} else {
space_was_pressed = false;
}
// ... rendering would go here ...
}
// --- Cleanup ---
audio_music_stop(&background_music); // Stop before unloading
audio_music_unload(&background_music);
audio_shutdown();
// ... other cleanup ...
return 0;
}
Compile and run your program. You should hear the background music start immediately and loop. When you press the spacebar, you should hear your one-shot sound effect play over the music.
Conclusion
Excellent work! You've now implemented the two most critical audio playback systems for any game engine. Your audio module is becoming quite capable.
Key Takeaways:
- There's a fundamental design difference between short one-shot effects and long, streaming background music.
- For simple, "fire and forget" sound effects,
ma_engine_play_soundis the easiest and most efficient method. - For music, you must load the sound with the
MA_SOUND_FLAG_STREAMflag to conserve memory. - The
ma_sound_set_loopingfunction is used to make a loaded sound repeat automatically. - Playback of a pre-loaded sound is controlled with
ma_sound_startandma_sound_stop.
Preview of the Next Lesson:
Our game now has sound, but all sounds play at full volume. What if you want to fade music out, or let the player adjust the SFX volume in a settings menu? In the next lesson, we will tackle "Control global and per-source audio volume at runtime." You'll learn how to change the master volume for the entire engine and also adjust the volume of individual sounds.