Real-Time Safety
Constraints and patterns for audio thread processing.
What is Real-Time Safe?
Audio processing must complete within a fixed time budget (buffer duration). Operations that can cause unbounded delays are forbidden.
Forbidden in Audio Thread
Memory Allocation
#![allow(unused)] fn main() { // BAD - Heap allocation let vec = Vec::new(); let boxed = Box::new(value); // GOOD - Pre-allocated let mut vec: StackVec<f32, 8> = StackVec::new(); }
Locking
#![allow(unused)] fn main() { // BAD - Can block let guard = mutex.lock().unwrap(); // GOOD - Lock-free let value = atomic.load(Ordering::Relaxed); }
System Calls
#![allow(unused)] fn main() { // BAD - Unbounded time std::fs::read_to_string("file.txt"); std::thread::sleep(duration); // GOOD - Pre-loaded data let data = &self.preloaded_buffer[..]; }
bbx_audio's Approach
Stack Allocation
Using StackVec for bounded collections:
#![allow(unused)] fn main() { const MAX_BLOCK_INPUTS: usize = 8; let mut input_slices: StackVec<&[S], MAX_BLOCK_INPUTS> = StackVec::new(); }
Pre-computed Lookups
Connection indices computed during prepare:
#![allow(unused)] fn main() { // Computed once in prepare_for_playback() self.block_input_buffers = vec![Vec::new(); self.blocks.len()]; // O(1) lookup during processing let input_indices = &self.block_input_buffers[block_id.0]; }
Pre-allocated Buffers
All audio buffers allocated upfront:
#![allow(unused)] fn main() { // During add_block() self.audio_buffers.push(AudioBuffer::new(self.buffer_size)); // During process - just clear buffer.zeroize(); }
Performance Considerations
Cache Efficiency
- Contiguous buffer storage
- Sequential processing order
- Minimal pointer chasing
Branch Prediction
- Consistent code paths
- Avoid data-dependent branches
- Use branchless algorithms where possible
SIMD Potential
- Buffer alignment
- Processing in chunks
- Sample-type genericity