If your audio stream stutters, the cause is almost always a buffer underrun (consumer ran out of data) or overrun (producer overwhelmed consumer). Solving both at once requires a ring buffer with explicit drift correction — not a generic queue.
Why not a regular queue?
Standard FIFO queues grow unbounded on overrun and crash on underrun. Audio needs fixed-size circular buffers with read and write pointers that wrap. The buffer size sets your latency floor (e.g., 5ms at 48kHz = 240 samples).
The ring buffer pattern
pub struct RingBuf {
data: Vec, write: AtomicUsize, read: AtomicUsize,
cap: usize,
}
impl RingBuf {
pub fn write(&self, samples: &[f32]) -> usize {
// lock-free SPSC, returns bytes actually written
}
pub fn read(&self, out: &mut [f32]) -> usize {
// ditto
}
pub fn available_read(&self) -> usize { /* ... */ 0 }
} Clock drift correction
Your microphone clock and your speaker clock are not synchronized down to the last nanosecond. Over 10 minutes, a 50 ppm drift produces a 30 ms offset — enough to cause audible glitches. Use a small resampler running at ratio ~1.000005x to correct slow drift without sample drops.
Underrun recovery
When the consumer asks for samples and the buffer is empty, you must emit SOMETHING — silence (audible click), repeat the last frame (audible buzz), or fade-out + fade-in (least artifact). Pick fade for production; log the underrun for ops to alert on.
Choosing buffer size
| Use case | Buffer | Underrun risk |
|---|---|---|
| Pro audio (DAW) | ~5 ms | High |
| Game voice | ~20 ms | Medium |
| VoIP | ~60 ms | Low |
| Streaming playback | 1-3 s | Very low |