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.