Skip to content
C++
Domain Deep-Dive
Intermediate

Game 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

LibraryLicenseBest for
FMODCommercial (free indie)AAA games, Unreal/Unity integration
WwiseCommercial (free indie)Data-driven audio design, console certified
miniaudioPublic DomainLightweight, single-header, embedded
SoLoudzlibSimple 2D/indie games, easy integration
OpenAL SoftLGPLOpen-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, no std::vector::push_back that reallocates)
  • No mutexes (spinlocks only, or lock-free queues)
  • No file I/O (stream from pre-loaded buffer)
  • No logging
  • Target < 1ms execution 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 100m

Audio 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