Implementing Low‑Latency Audio Playback with SDL 2's Audio Callback
Learn how to open an SDL 2 audio device, respect the negotiated format, and fill the entire buffer in a non‑blocking callback to achieve low‑latency playback.
05 Sept 2026, 02:35 UTC

Useful answer
To achieve low‑latency audio output in SDL 2, open the device with SDL_AUDIO_ALLOW_ANY_CHANGE, respect the exact format the device reports, and fill the entire buffer in the audio callback without any blocking operations. This guarantees that the hardware receives data on time and avoids clicks or silence.
How the callback works – a worked example
The following C snippet shows the minimal setup and callback for 16‑bit stereo PCM at 48 kHz. Adjust desiredFreq, desiredChannels, and desiredSamples as needed for your application.
#include <SDL2/SDL.h>
#include <stdint.h>
static SDL_AudioDeviceID devID = 0;
static uint8_t *mixBuffer = NULL;
static int mixBufferSize = 0;
/* Fill the buffer with silence – replace with your own mixing code */
static void audioCallback(void *userdata, Uint8 *stream, int len)
{
(void)userdata; /* unused */
/* The callback must write exactly ‘len’ bytes */
if (mixBufferSize < len) {
/* Re‑allocate only during init, never here */
return;
}
/* Copy pre‑mixed data into the stream */
SDL_memcpy(stream, mixBuffer, len);
}
int main(int argc, char *argv[])
{
(void)argc; (void)argv;
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
return 1;
}
SDL_AudioSpec want, have;
SDL_zero(want);
want.freq = 48000; /* desired sample rate */
want.format = AUDIO_S16SYS; /* 16‑bit signed, native endianness */
want.channels = 2; /* stereo */
want.samples = 1024; /* buffer size in samples (power of two) */
want.callback = audioCallback;
want.userdata = NULL;
/* Allow SDL to change the format if the exact request cannot be met */
devID = SDL_OpenAudioDevice(NULL, 0, &want, &have,
SDL_AUDIO_ALLOW_ANY_CHANGE);
if (devID == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_OpenAudioDevice failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
/* Verify that the format we received matches what we will write */
if (have.format != want.format || have.channels != want.channels ||
have.freq != want.freq) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device reports different format: %d Hz, %d ch, %d format",
have.freq, have.channels, have.format);
/* In a real program you would adapt your mixing code to ‘have’ */
}
/* Allocate a mixing buffer that matches the device’s reported size */
mixBufferSize = have.size; /* bytes per callback */
mixBuffer = SDL_malloc(mixBufferSize);
if (!mixBuffer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to allocate mix buffer");
SDL_CloseAudioDevice(devID);
SDL_Quit();
return 1;
}
/* Start playback */
SDL_PauseAudioDevice(devID, 0);
/* Main loop – fill mixBuffer with your audio data here */
int running = 1;
while (running) {
/* Example: fill with a simple sine wave (non‑blocking) */
static double phase = 0.0;
const double freq = 440.0; /* A4 */
const double phaseInc = 2.0 * M_PI * freq / have.freq;
Sint16 *ptr = (Sint16 *)mixBuffer;
for (int i = 0; i < have.samples * have.channels; ++i) {
*ptr++ = (Sint16)(sin(phase) * 32760.0);
phase += phaseInc;
if (phase > 2.0 * M_PI) phase -= 2.0 * M_PI;
}
/* Simulate work; replace with actual event handling */
SDL_Delay(10);
/* Break after a few seconds for demo purposes */
static Uint32 start = SDL_GetTicks();
if (SDL_GetTicks() - start > 5000) running = 0;
}
/* Cleanup */
SDL_CloseAudioDevice(devID);
SDL_free(mixBuffer);
SDL_Quit();
return 0;
}
Why this works
- Format negotiation:
SDL_AUDIO_ALLOW_ANY_CHANGElets SDL pick the closest supported format. Thehavestruct afterSDL_OpenAudioDevicetells you the exact sample rate, channel count, and format you must use in the callback. - Buffer completeness: The callback receives a pointer
streamand a lengthlen(in bytes). Writing fewer thanlenbytes leaves the rest undefined, which the hardware interprets as silence or noise, causing audible gaps. - Non‑blocking execution: The audio callback runs on a high‑priority real‑time thread. Any call that may sleep (e.g.,
SDL_Delay, file I/O, mutex lock,malloc) can overrun the deadline and produce glitches. All heavy work (mixing, decoding) must be done outside the callback, and the callback merely copies pre‑prepared data.
Limits and common mistakes
Limits
- The latency is bounded by the buffer size (
samples) you request. Smaller buffers reduce latency but increase CPU load because the callback is invoked more frequently. - SDL’s audio subsystem does not provide sample‑accurate synchronization with video; for A/V sync you need an external clock or a higher‑level library.
- On some platforms the minimum buffer size is enforced by the driver; requesting a size too small may cause SDL to round up, which you must accept from the
havestruct.
Common mistakes
- Ignoring the reported format: Writing data that assumes the original
wantvalues while the device actually uses a different sample rate or channel count leads to pitch shifts or distorted audio. - Partial buffer fills: Forgetting to write the full
lenbytes results in periodic silence or static. - Blocking inside the callback: Using
SDL_Delay,printf, or allocating memory can cause the audio thread to miss its deadline, producing clicks. - Not handling device loss: If the underlying audio device is disconnected (e.g., Bluetooth headset power‑off), SDL posts an
SDL_AUDIODEVICELOSSevent. Failing to close and reopen the device leaves the callback dangling.
Verification steps
- Compile the example for your target (e.g.,
gcc -o sdl_audio sdl_audio.c -lSDL2) and run it. Listen for a steady tone without clicks or dropouts. - Measure the callback duration: add a timestamp at the start and end of
audioCallbackusingSDL_GetPerformanceCounterand ensure the elapsed time is well under the interval (samples / freqseconds). For a 1024‑sample buffer at 48 kHz the interval is ≈21 ms; aim for <1 ms of work. - Run the binary on Windows, macOS, and Linux (if available) and confirm that the
havevalues printed by the program match across platforms and that the audio sounds consistent. - Introduce a deliberate mistake (e.g., write only half the buffer) and verify that audible gaps appear, reinforcing the importance of a full fill.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.