A C++20, multiplatform audio engine for game development.
Part of the campello_xxx library family by Ruben Leal.
Inspired by the design philosophy of SoLoud: simple "fire and forget" playback for common cases, with full control available when needed.
This project is a module within the Campello ecosystem.
👉 Main repository: https://github.com/rusoleal/campello
Campello is a modular, composable game engine built as a collection of independent libraries. Each module is designed to work standalone, but integrates seamlessly into the engine runtime.
| Platform | Backend | Status |
|---|---|---|
| macOS | CoreAudio | Complete |
| iOS | CoreAudio | Complete |
| Android | AAudio | Complete |
| Windows | WASAPI | Complete |
| Linux | PulseAudio / ALSA | Complete |
- CMake 3.22.1+
- C++20 compiler (Clang 14+, GCC 12+, MSVC 19.34+)
- Platform SDK for the target backend (Xcode, NDK, Windows SDK)
| Library | Purpose | Source |
|---|---|---|
| vector_math | 3D vectors, matrices, quaternions | Fetched via CMake FetchContent |
cmake -B build
cmake --build build| Option | Default | Description |
|---|---|---|
BUILD_TESTS |
OFF | Build universal unit tests (no audio device) |
BUILD_INTEGRATION_TESTS |
OFF | Build platform integration tests (real device) |
BUILD_EXAMPLES |
OFF | Build example applications |
macOS
cmake -B build && cmake --build buildiOS (cross-compile from macOS)
cmake -B build-ios \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_OSX_ARCHITECTURES=arm64
cmake --build build-iosAndroid (requires NDK)
cmake -B build-android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-26
cmake --build build-androidWindows (Visual Studio generator)
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config ReleaseLinux
# PulseAudio (default if libpulse-dev is installed)
cmake -B build && cmake --build build
# ALSA fallback (auto-selected when PulseAudio is absent)
cmake -B build && cmake --build build# Run universal tests (no audio device required)
cmake -B build -DBUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure# Build platform-specific example applications
cmake -B build -DBUILD_EXAMPLES=ON
cmake --build buildAll public types live in the systems::leal::campello_audio namespace.
#include <campello_audio/audio_engine.hpp>
using namespace systems::leal::campello_audio;
AudioEngine engine;
AudioEngineDescriptor desc;
desc.sampleRate = 44100;
desc.maxVoices = 32;
desc.channels = 2;
engine.init(desc);
// ... use engine ...
engine.deinit();The simplest usage — play a sound and ignore the handle:
#include <campello_audio/wav_source.hpp>
WavSource shot;
shot.load("assets/gunshot.wav");
engine.play(shot); // returns SoundHandle — safe to ignoreStore the handle to adjust the voice after it starts:
WavSource music;
music.load("assets/theme.wav");
PlayDescriptor pd;
pd.looping = true;
pd.volume = 0.8f;
SoundHandle h = engine.play(music, pd);
// Later...
engine.fadeVolume(h, 0.0f, 2.0); // fade out over 2 seconds
engine.stop(h);#include <campello_audio/ogg_source.hpp>
#include <campello_audio/mp3_source.hpp>
OggSource ogg;
ogg.load("assets/ambient.ogg");
engine.play(ogg);
Mp3Source mp3;
mp3.load("assets/track.mp3");
engine.play(mp3);#include <campello_audio/tone_source.hpp>
ToneSource beep(WaveForm::Sine, 440.0f);
SoundHandle h = engine.play(beep);
engine.fadeVolume(h, 0.0f, 0.3); // short beepRoute groups of sounds through a shared effects chain:
#include <campello_audio/audio_bus.hpp>
#include <campello_audio/reverb_filter.hpp>
AudioBus sfxBus;
auto reverb = std::make_shared<ReverbFilter>(0.7f, 0.4f);
sfxBus.addFilter(reverb, 0);
// Play the bus into the main mix
engine.play(sfxBus);
// Route individual sounds through the bus
WavSource explosion;
explosion.load("explosion.wav");
sfxBus.play(explosion);Up to 8 filters per source or bus. Parameters can be automated:
#include <campello_audio/low_pass_filter.hpp>
WavSource rain;
rain.load("rain.wav");
auto lpf = std::make_shared<LowPassFilter>(3000.0f, 0.707f);
rain.addFilter(lpf, 0);
SoundHandle h = engine.play(rain);
// Smoothly sweep the cutoff (underwater effect)
lpf->fadeParam(LowPassFilter::PARAM_CUTOFF, 500.0f, 1.5);
// Remove filter
rain.removeFilter(0);Available filters:
| Class | Effect |
|---|---|
LowPassFilter |
Attenuate high frequencies (bi-quad IIR) |
HighPassFilter |
Attenuate low frequencies (bi-quad IIR) |
EchoFilter |
Delay-line echo / feedback |
ReverbFilter |
Freeverb algorithmic room reverb |
CompressorFilter |
Dynamic range compressor (soft-knee RMS) |
LimiterFilter |
True-peak brickwall limiter with lookahead |
ChorusFilter |
Multi-voice chorus (stereo thickening) |
FlangerFilter |
Modulated comb-filter flanger |
PitchShiftFilter |
Pitch shift without tempo change (WSOLA) |
For large files (music, long ambience) use AudioStream to decode on the fly
without loading the entire file into RAM:
#include <campello_audio/audio_stream.hpp>
AudioStream music;
music.open("assets/music/theme.ogg");
music.setLooping(true);
engine.play(music);Prevent the "machine gun" effect by pooling several variants:
#include <campello_audio/random_source.hpp>
RandomSource gunshot;
for (auto& p : {"shot1.wav", "shot2.wav", "shot3.wav"}) {
auto v = std::make_shared<WavSource>();
v->load(p);
gunshot.addVariant(v);
}
gunshot.setPitchVariation(2.0f); // ±2 semitones
gunshot.setVolumeVariation(2.0f); // ±2 dB
gunshot.setAvoidRepeat(true);
engine.play(gunshot); // picks a different variant each callPlace a compressor on a bus to prevent layered sounds from clipping, and a limiter on the master output to satisfy platform certification:
#include <campello_audio/compressor_filter.hpp>
#include <campello_audio/limiter_filter.hpp>
// SFX bus compressor
AudioBus sfxBus;
auto comp = std::make_shared<CompressorFilter>(-12.0f, 4.0f, 5.0f, 50.0f);
sfxBus.addFilter(comp, 0);
// Master output limiter (on the main mix bus)
AudioBus masterBus;
auto limiter = std::make_shared<LimiterFilter>(-0.3f, 5.0f, 50.0f);
masterBus.addFilter(limiter, 0);Automatically lower music when dialogue plays:
// Dialogue bus triggers ducking of music bus
engine.setSidechain(&dialogueBus, &musicBus,
-12.0f, // duck by 12 dB
0.05f, // 50 ms attack
0.5f); // 500 ms releaseAvoid main-thread hitches by loading assets in the background:
auto shot = std::make_shared<WavSource>();
shot->loadAsync("gunshot.wav", [&](bool ok) {
// Called on the main thread during engine.tick()
if (ok) engine.play(*shot);
});
// In your game loop:
engine.tick(); // drains the callback queueBind named game parameters to audio properties using configurable curves:
#include <campello_audio/audio_parameter.hpp>
// Register a "speed" parameter ranging 0–200 km/h
auto speed = std::make_shared<AudioParameter>("speed", 0.0f, 200.0f);
engine.registerParameter(speed);
// Bind it: engine pitch 0.5× at rest, 2.0× at max speed (exponential curve)
engineSound.bindParameter("speed", AudioSourceProperty::Pitch,
CurveType::Exponential, 0.5f, 2.0f);
// Drive it from game code each frame — no audio code required
engine.setParameter("speed", vehicle.getSpeed());Switch global audio moods smoothly:
#include <campello_audio/audio_snapshot.hpp>
auto underwater = std::make_shared<AudioSnapshot>("underwater");
underwater->setGlobalVolume(0.6f);
underwater->setBusFilter(&sfxBus, std::make_shared<LowPassFilter>(400.0f), 0);
engine.registerSnapshot(underwater);
// Player dives
engine.applySnapshot("underwater", 1.5); // blend in over 1.5 s
// Player surfaces
engine.revertSnapshot(1.0); // blend out over 1.0 sDefine sections and transitions; the engine handles the timing:
#include <campello_audio/music_track.hpp>
auto track = std::make_shared<MusicTrack>();
track->setBpm(120.0f);
track->setTimeSignature(4, 4);
auto explore = std::make_shared<AudioStream>(); explore->open("explore.ogg");
auto combat = std::make_shared<AudioStream>(); combat->open("combat.ogg");
track->addSection(explore, "explore");
track->addSection(combat, "combat");
track->addTransition("explore", "combat", TransitionRule::OnBar, 0.5);
track->addTransition("combat", "explore",TransitionRule::OnBar, 1.0);
engine.play(*track);
// When the player enters combat — transition fires at next bar boundary
engine.requestMusicTransition("combat");// Call once per frame with the current listener state
ListenerDescriptor listener;
listener.position = {camX, camY, camZ};
listener.forward = {fwdX, fwdY, fwdZ};
listener.up = {0.0f, 1.0f, 0.0f};
engine.set3dListenerParameters(listener);
// Play a 3D-positioned sound
PlayDescriptor pd3d;
pd3d.enable3d = true;
pd3d.position = {10.0f, 0.0f, -5.0f};
pd3d.minDistance = 1.0f;
pd3d.maxDistance = 50.0f;
WavSource gunshot;
gunshot.load("gunshot.wav");
SoundHandle h = engine.play(gunshot, pd3d);
// Update source position each frame
engine.set3dSourceParameters(h, newX, newY, newZ);
// Apply 3D calculations — call once per frame before audio processing
engine.update3d();// Smooth volume fade
engine.fadeVolume(h, 0.0f, 2.0);
// Pan sweep
engine.fadePan(h, -1.0f, 1.0);
// LFO volume tremolo
engine.oscillateVolume(h, 0.5f, 1.0f, 0.25); // 4 Hz tremoloAudioEngineDescriptor desc;
desc.visualization = true;
engine.init(desc);
// Each frame
uint32_t count;
const float* samples = engine.getVisualizationData(count);
// samples contains the last mixed buffer (count floats)| Category | Types |
|---|---|
| Engine | AudioEngine, AudioEngineDescriptor |
| Sources | WavSource, OggSource, Mp3Source, ToneSource, AudioStream, RandomSource, AudioBus, MusicTrack |
| Playback | SoundHandle, PlayDescriptor |
| 3D Audio | ListenerDescriptor, AttenuationModel |
| Math | vector_math::Vector3<float> (aliased as Vec3 in descriptors) |
| Filters | LowPassFilter, HighPassFilter, EchoFilter, ReverbFilter, CompressorFilter, LimiterFilter, ChorusFilter, FlangerFilter, PitchShiftFilter |
| RTPC | AudioParameter, AudioSourceProperty, CurveType |
| Snapshots | AudioSnapshot |
| Constants | WaveForm, LoopMode, ResampleQuality, TransitionRule |
All public types are in systems::leal::campello_audio.
See LICENSE.