| name | audio-dsp-review |
| description | Reviews audio DSP and audio processing code for realtime safety violations. Use whenever the user asks to review, audit, or check audio processing code โ including plugin process callbacks, audio engine render functions, DSP implementations, or any code that runs on the audio thread. Trigger on phrases like "review my processBlock", "check this DSP code", "is this safe for the audio thread?", "review my JUCE plugin", or when you see an audio callback and spot potential realtime violations. Flag issues proactively even when the user hasn't explicitly asked for a review.
|
Audio DSP Realtime Safety Review
The cardinal rule: If you don't know how long it will take, don't do it.
The audio callback has a hard deadline โ typically 1โ5ms per buffer. Miss it once and the
user hears a glitch. The three categories of violations that cause this are: allocations,
locking, and system calls โ all of which can block for unbounded time.
Step 1 โ Find the realtime context
Identify the audio callback and every function it calls (transitively). Common names:
processBlock, process, render, getNextAudioBlock, audioDeviceIOCallback,
AURenderCallback, JackProcessCallback, and the output-stream closure in cpal/JUCE/PortAudio.
Step 2 โ Scan for violations
Memory allocations (allocator paths may lock, grow arenas, touch new pages, or run
unbounded bookkeeping):
new / delete / malloc / free / make_shared / make_unique
std::string construction, concatenation, to_string()
- Container growth:
vector::push_back without pre-reserve, map::operator[] on new keys,
unordered_map::insert, deque::push_back
std::function with non-trivial captures (may allocate), throwing exceptions
Locking (priority inversion: the low-priority UI thread holds the lock your audio thread needs):
std::mutex, lock_guard, unique_lock, scoped_lock, shared_mutex
pthread_mutex_lock, pthread_rwlock_*
condition_variable::wait(), future::get(), semaphore::acquire()
std::mutex::try_lock() + RAII wrapper โ try_lock() itself is non-blocking, but the
RAII destructor calls unlock(), which does a system call to wake waiting threads.
Not realtime-safe. Same for std::unique_lock(mtx, std::try_to_lock).
- Spinlocks without exponential back-off on the non-audio thread โ busy-wait burns CPU,
causes starvation under contention, and drains battery on mobile devices.
If you must use a spinlock: audio thread calls only
try_lock() + fallback;
non-audio thread uses progressive back-off (spin โ _mm_pause() โ batched pauses โ occasional yield).
System calls / blocking I/O (kernel transitions stall for unbounded time):
printf, fprintf, cout, cerr, DBG() (JUCE debug), syslog, os_log
- File I/O:
fopen, fclose, fread, fwrite, open, read, write
sleep, usleep, nanosleep, std::this_thread::sleep_for, yield
- Network:
connect, send, recv, select
std::system(), assert() (in debug builds: prints + aborts)
Warnings (flag but don't block):
- First-touch page faults โ allocate and pre-touch buffers in
prepareToPlay
- Denormal floats โ missing
ScopedNoDenormals or _MM_SET_FLUSH_ZERO_MODE causes 100ร slowdown
std::rand() โ uses a global lock on some implementations
Step 3 โ Write the review
## Audio Realtime Safety Review: `[file / function]`
### Verdict
[Safe | Has critical violations | Warnings only] โ [one sentence summary]
### Critical Violations
**[Category]: [description]**
`file:line` โ `offending code`
Why: [one sentence on the realtime risk]
Fix: [concrete suggestion]
### Warnings
[same format]
### What's Done Well
[correct patterns observed โ pre-allocation, atomics, lock-free queues, etc.]
### Recommended Fixes (priority order)
1. ...
Quick fix table
| Violation | Realtime-safe alternative |
|---|
std::mutex for shared state | Lock-free std::atomic<T> for scalars after checking is_lock_free(); SPSC lock-free queue for structs |
std::mutex::try_lock() + RAII | unlock() in destructor does a syscall โ not safe. Use std::atomic_flag spinlock with try_lock() only on audio thread + fallback |
| Spinlock with busy-wait on non-audio thread | Progressive back-off: spin 5ร โ _mm_pause() 10ร โ batched 10ร _mm_pause() โ occasional std::this_thread::yield() |
new/delete in callback | Allocate in prepareToPlay; use pre-allocated pool or ring buffer |
printf/DBG() | Write to a lock-free ring buffer; drain from a background thread |
std::string ops | std::array<char,N> + snprintf; format on UI thread |
vector growing | reserve() on init; fixed-capacity container (etl::vector, heapless::Vec) |
map::operator[] insertion | find() + pre-populate; or std::array with index lookup |
| Exception throwing | Validate in prepareToPlay; use error codes; never throw from callback |
std::function w/ captures | Function pointer + void*; or pre-store as member |
For deeper patterns (JUCE-specific, VST3, CLAP, Rust/cpal, Web Audio) see
references/realtime-violations.md.