ObrImpl::Process() performs heap allocation and deallocation on every call. Since Process() is the render entry point, it runs on a real-time audio thread in any live playback integration, where malloc/free can block on the allocator lock and are not bounded in time. This is the usual cause of intermittent dropouts that are hard to attribute.
This is independent of any threading or locking concern — it reproduces single-threaded, with head tracking disabled and no concurrent control calls.
Measurement
Built obr with upstream LLVM (RealtimeSanitizer is not in Apple clang) and called Process() from inside a [[clang::nonblocking]] region:
void RenderBlock(ObrImpl* renderer, const AudioBuffer& input,
AudioBuffer* output) [[clang::nonblocking]] {
renderer->Process(input, output);
}
with a single k3OA element, 128-frame buffers, 48 kHz, after one warm-up call outside the checked region. Result, per render call:
4 x malloc, 4 x free
obr_impl.cc:225 AudioBuffer group_output(kNumBinauralChannels, buffer_size_per_channel_);
-> audio_buffer.cc:57 vector<float, AlignedAllocator>::resize
-> audio_buffer.cc:60 vector<ChannelView>::reserve
obr_impl.cc:240
obr_impl.cc:242 peak_limiter_->Process(...)
-> peak_limiter.cc:40, :48, :65
Suggested fix
Both are buffers whose size is known once the DSP is configured, so they can be allocated in InitializeDsp() and reused:
- hold
group_output as a member sized to kNumBinauralChannels x buffer_size_per_channel_, cleared per block instead of constructed per block (AudioBuffer::Clear() is already used elsewhere in Process());
- give
PeakLimiter the same treatment for its internal scratch.
Happy to put a PR together if the approach looks right — I did not want to bundle it with #27, which touches the same function for an unrelated reason.
Environment
main @ aae184e, macOS/arm64, clang 22.1.8, -fsanitize=realtime.
ObrImpl::Process()performs heap allocation and deallocation on every call. SinceProcess()is the render entry point, it runs on a real-time audio thread in any live playback integration, wheremalloc/freecan block on the allocator lock and are not bounded in time. This is the usual cause of intermittent dropouts that are hard to attribute.This is independent of any threading or locking concern — it reproduces single-threaded, with head tracking disabled and no concurrent control calls.
Measurement
Built obr with upstream LLVM (RealtimeSanitizer is not in Apple clang) and called
Process()from inside a[[clang::nonblocking]]region:with a single
k3OAelement, 128-frame buffers, 48 kHz, after one warm-up call outside the checked region. Result, per render call:Suggested fix
Both are buffers whose size is known once the DSP is configured, so they can be allocated in
InitializeDsp()and reused:group_outputas a member sized tokNumBinauralChannelsxbuffer_size_per_channel_, cleared per block instead of constructed per block (AudioBuffer::Clear()is already used elsewhere inProcess());PeakLimiterthe same treatment for its internal scratch.Happy to put a PR together if the approach looks right — I did not want to bundle it with #27, which touches the same function for an unrelated reason.
Environment
main@ aae184e, macOS/arm64, clang 22.1.8,-fsanitize=realtime.