SDL2’s low‑level audio subsystem gives you raw PCM buffers, letting you implement custom effects ranging from simple volume envelopes to real‑time convolution reverb. This guide walks through device setup, the callback model and several effect recipes.
if (SDL_Init(SDL_INIT_AUDIO) != 0) {
SDL_Log("Init error %s", SDL_GetError());
return ‑1;
}
SDL_AudioSpec want = {0}, have;
want.freq = 48000;
want.format = AUDIO_F32SYS; /* 32‑bit float */
want.channels = 2;
want.samples = 1024;
want.callback = audioCB;
want.userdata = &fxState;
SDL_AudioDeviceID dev = SDL_OpenAudioDevice(
NULL, /* default output */
0, /* is capture? */
&want, &have,
SDL_AUDIO_ALLOW_FORMAT_CHANGE);
SDL_PauseAudioDevice(dev, 0); /* 0 = start, 1 = pause */
Create a state‑struct so multiple effects can share parameters:
typedef struct {
float vol ; /* 0 – 1 */
float pan ; /* ‑1 = L … +1 = R */
float delayBuf[48000]; /* 1 s @ 48 kHz mono */
size_t delayPos;
float delayMix ; /* wet/dry */
float pitchSemis;
} FxState;
Pass &fxState
via want.userdata
.
SDL_MixAudioFormat(stream, stream,
AUDIO_F32SYS,
len,
(int)(fxState.vol * SDL_MIX_MAXVOLUME));
float *f = (float*)stream;
size_t frames = len / sizeof(float);
for (size_t i=0; i < frames; i++)
f[i] *= currentGain(i); /* fade‑in / fade‑out */
Note: keep envelopes in float domain to avoid clipping artefacts.
float L = (fxState.pan < 0) ? 1.0f : 1.0f ‑ fxState.pan;
float R = (fxState.pan > 0) ? 1.0f : 1.0f + fxState.pan;
for (size_t i=0; i < frames; i+=2) {
f[i ] *= L;
f[i+1] *= R;
}
Equal‑power panning uses sqrt()
for perceptual balance.
for (size_t i=0; i < frames; ++i) {
float dry = f[i];
float wet = fxState.delayBuf[fxState.delayPos];
fxState.delayBuf[fxState.delayPos] = dry;
f[i] = dry * (1.0f ‑ fxState.delayMix) + wet * fxState.delayMix;
if (++fxState.delayPos >= 48000) fxState.delayPos = 0;
}
Increase depth: add a feedback term before writing to delayBuf
.
SDL2 has no built‑in resampler, but you can pitch‑shift by buffer‑skipping (naïve, cheap) or with granular overlap‑add. A lightweight approach uses SDL’s SDL_AudioStream:
SDL_AudioStream *rs =
SDL_NewAudioStream(AUDIO_F32SYS, 2, 48000,
AUDIO_F32SYS, 2,
48000 * powf(2.f, fxState.pitchSemis / 12.f));
/* write original PCM into rs, then SDL_AudioStreamGet() resampled PCM */
For smoother artefact‑free shifts, consider third‑party libs (Rubber Band, SoundTouch) and push resampled data into the callback.
typedef struct { float z1, a0, b1; } OnePole;
static inline float lp(OnePole *s, float x) {
float y = s->a0 * x + s->b1 * s->z1;
return s->z1 = y;
}
/* … inside callback … */
for (size_t i=0;i<frames;++i)
f[i] = lp(&lowPass, f[i]);
Set coefficients with omega = 2πf / Fs
and
a0 = 1‑exp(‑omega)
, b1 = 1‑a0
.
SDL_mixer
for Chainable DSPSDL_mixer 2.8+ supports Mix_RegisterEffect for per‑channel DSP.
int effectCB(int chan, void *stream, int len, void *udata) {
(void)chan;
myCoolEffect((float*)stream, len/sizeof(float), udata);
return 0;
}
Mix_RegisterEffect(MIX_CHANNEL_POST, effectCB, NULL, &fxState);
Advantages: automatic mixing, channel grouping, run‑time chaining without writing your own callback. Downside: tied to SDL_mixer’s internal format and thread.
malloc
, heavy math, file I/O.atomic
or SDL_LockAudioDevice
‑protected vars.SDL_AudioStream
capture: write processed PCM
to a WAV for offline inspection.
• SDL Wiki – Audio Subsystem
• “Designing Audio Effect Plug‑ins in C++” – Will Pirkle
• SoundTouch & Rubber Band libraries for pitch/tempo
• Game Audio Coding Patterns – GDC Vault talks