Why no allocations

The audio callback runs at fixed cadence (every ~10ms). If it takes too long → underrun → audible click. Heap allocation (calling alloc) is fundamentally non-deterministic — can take 100ns or 100µs depending on heap state. Don't do it inside the callback.

Advertisement

cpal hello world

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};

let host = cpal::default_host();
let device = host.default_output_device().unwrap();
let config = device.default_output_config().unwrap();

let stream = device.build_output_stream(
    &config.into(),
    move |data: &mut [f32], _| {
        // CALLED EVERY 10ms ON A REAL-TIME THREAD
        // NO alloc, no I/O, no locks!
        for sample in data.iter_mut() { *sample = 0.0; /* fill audio */ }
    },
    move |err| eprintln!("audio error: {err}"),
    None,
)?;
stream.play()?;
Advertisement

SPSC ring buffer between threads

Producer thread fills sample data; audio callback drains it. Use ringbuf crate (single-producer single-consumer, lock-free). Allocate the buffer ONCE at startup; never resize.