Real-time audio in Rust means using the cpal crate for cross-platform audio I/O, a lock-free ring buffer for thread communication, and ZERO heap allocations on the audio callback. Rust's ownership model helps but doesn't eliminate the discipline needed.

Advertisement

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.

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.

Disabling allocations in the audio thread

Use assert_no_alloc crate during dev — it panics if you allocate inside a wrapped block. Catches accidental String::new(), vec![] before they cause field crashes. Disable in release if you're confident.

Sample-rate negotiation

cpal lets you request a sample rate but the OS may give you something else. Always check config.sample_rate() and resample if needed. Use rubato crate for SRC inside the producer (NOT the audio callback).

cpal + SPSC ringbuf + no-alloc discipline + rubato for SRC. Zero locks in the audio thread, period.