Domain Deep-Dive
IntermediateGame Audio in C++
Integrating FMOD, Wwise, miniaudio, and SoLoud into C++ games: streaming, 3D spatialization, DSP effects, and low-latency audio pipelines.
Audio library landscape
| Library | License | Best for |
|---|---|---|
| FMOD | Commercial (free indie) | AAA games, Unreal/Unity integration |
| Wwise | Commercial (free indie) | Data-driven audio design, console certified |
| miniaudio | Public Domain | Lightweight, single-header, embedded |
| SoLoud | zlib | Simple 2D/indie games, easy integration |
| OpenAL Soft | LGPL | Open-source alternative, cross-platform |
miniaudio — the lightweight choice
cpp
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
// Initialize audio engine
ma_engine engine;
ma_result result = ma_engine_init(nullptr, &engine);
if (result != MA_SUCCESS) {
// Handle error
}
// Play a sound (fire-and-forget)
ma_engine_play_sound(&engine, "explosion.wav", nullptr);
// Load and play with control
ma_sound sound;
ma_sound_init_from_file(&engine, "music.ogg", 0, nullptr, nullptr, &sound);
ma_sound_set_looping(&sound, MA_TRUE);
ma_sound_start(&sound);
// 3D positioning
ma_sound_set_position(&sound, 10.0f, 0.0f, 5.0f);
ma_sound_set_velocity(&sound, 1.0f, 0.0f, 0.0f);
// Cleanup
ma_sound_uninit(&sound);
ma_engine_uninit(&engine);Audio thread architecture
Audio runs on a dedicated real-time thread — never block it:
cpp
Game Thread Audio Thread (high priority, 256-512 sample buffer)
│ │
│ command queue │
├──────────────────────▶│ mix_buffer()
│ │ apply_3d_spatialization()
│ audio events │ run_dsp_effects()
│◀──────────────────────│ submit_to_driver()
│ (onSoundComplete) │cpp
class AudioSystem {
// Ring buffer for thread-safe commands
struct Command { enum Type { Play, Stop, SetVolume, Set3DPos } type; };
moodycamel::ConcurrentQueue<Command> m_commands;
public:
// Called from game thread — non-blocking
void PlaySound(SoundHandle handle) {
m_commands.enqueue({ Command::Play, handle });
}
// Called from audio thread — processes queued commands
void AudioThreadUpdate() {
Command cmd;
while (m_commands.try_dequeue(cmd)) {
ProcessCommand(cmd);
}
Mix();
}
};Rules for the audio callback:
- No memory allocation (no
new, nostd::vector::push_backthat reallocates) - No mutexes (spinlocks only, or lock-free queues)
- No file I/O (stream from pre-loaded buffer)
- No logging
- Target
< 1msexecution time
3D spatialization
cpp
// Listener (camera/player position)
ma_engine_listener_set_position(&engine, 0, /* x */ 0, /* y */ 0, /* z */ 0);
ma_engine_listener_set_direction(&engine, 0, /* forward */ 0, 0, -1);
ma_engine_listener_set_world_up(&engine, 0, 0, 1, 0);
// Sound source in 3D space
ma_sound footstep;
ma_sound_init_from_file(&engine, "footstep.wav",
MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_NO_SPATIALIZATION, nullptr, nullptr, &footstep);
// Override: enable spatialization
ma_sound_set_spatialization_enabled(&footstep, MA_TRUE);
// Update position from game object transform
void UpdateAudioSource(const glm::vec3& world_pos, const glm::vec3& velocity) {
ma_sound_set_position(&footstep, world_pos.x, world_pos.y, world_pos.z);
ma_sound_set_velocity(&footstep, velocity.x, velocity.y, velocity.z);
// Velocity enables Doppler effect
}Attenuation models:
cpp
// Set falloff: linear (simple) or inverse (realistic)
ma_sound_set_attenuation_model(&sound, ma_attenuation_model_inverse);
ma_sound_set_rolloff_factor(&sound, 1.0f);
ma_sound_set_min_distance(&sound, 1.0f); // full volume within 1m
ma_sound_set_max_distance(&sound, 100.0f); // silent beyond 100mAudio streaming vs preloading
cpp
// Preload — fits in RAM, instant play, no seek
// Use for: SFX, one-shots, short loops
ma_sound_init_from_file(&engine, "gunshot.wav",
MA_SOUND_FLAG_DECODE, // decode into RAM
nullptr, nullptr, &sound);
// Stream — decode on-the-fly, lower memory
// Use for: music, long ambient, voice-over
ma_sound_init_from_file(&engine, "soundtrack.ogg",
0, // stream (default)
nullptr, nullptr, &music);Memory budget rules of thumb:
- Music: always stream (10+ MB as WAV)
- Voice-over: stream
- SFX
< 2s: preload as 16-bit PCM - SFX 2-10s: preload at lower sample rate or stream
- SFX > 10s: always stream
DSP effects chain
cpp
// miniaudio node graph for DSP
ma_engine_node_graph* graph = ma_engine_get_node_graph(&engine);
// Low-pass filter (muffle underwater)
ma_lpf_node lpf_node;
ma_lpf_node_config lpf_config = ma_lpf_node_config_init(
ma_engine_get_channels(&engine),
ma_engine_get_sample_rate(&engine),
1000.0f, // cutoff Hz
2 // order
);
ma_lpf_node_init(graph, &lpf_config, nullptr, &lpf_node);
// Route: sound → LPF → endpoint
ma_node_attach_output_bus(&lpf_node, 0,
ma_engine_get_endpoint(&engine), 0);
ma_sound_set_output_bus(&underwater_sound, &lpf_node, 0);Frame budget
Audio mixing typically takes < 0.5ms per frame. Keep it that way:
- Pre-decode all sounds > 2 seconds into memory at load time
- Limit simultaneous voices: 32-64 is typical for AAA, 8-16 for indie
- Voice stealing: when at voice limit, kill the quietest or furthest sound
- Occlusion: use raycasts to detect walls; reduce high frequencies (simulate wall absorption)
cpp
// Simple voice stealing: kill the quietest active sound
void VoiceSteal(AudioSystem& sys) {
if (sys.ActiveVoiceCount() < MAX_VOICES) return;
float min_vol = std::numeric_limits<float>::max();
VoiceHandle steal_target;
for (auto& v : sys.ActiveVoices()) {
float eff_vol = v.volume * v.distance_attenuation;
if (eff_vol < min_vol) { min_vol = eff_vol; steal_target = v.handle; }
}
sys.Stop(steal_target);
}Edit on GitHubUpdated 2026-05-24T00:00:00.000Z