use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use audio_core_bsd::{AudioFrame, AudioNode, PortDescriptor, ProcessContext, SampleFormat};
use audio_graph_bsd::{Graph, GraphConfig};
struct CountingAllocator;
thread_local! {
static RT_MEASURING: Cell<bool> = const { Cell::new(false) };
static RT_ALLOC_COUNT: Cell<usize> = const { Cell::new(0) };
}
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let _ = RT_MEASURING.try_with(|m| {
if m.get() {
let _ = RT_ALLOC_COUNT.try_with(|c| c.set(c.get().saturating_add(1)));
}
});
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
}
}
#[global_allocator]
static A: CountingAllocator = CountingAllocator;
fn rt_start_measuring() {
RT_ALLOC_COUNT.with(|c| c.set(0));
RT_MEASURING.with(|m| m.set(true));
}
fn rt_stop_and_count() -> usize {
RT_MEASURING.with(|m| m.set(false));
RT_ALLOC_COUNT.with(|c| c.get())
}
struct SourceNode {
out_p: [PortDescriptor; 1],
}
impl SourceNode {
fn new(channels: u16) -> Self {
Self {
out_p: [PortDescriptor::output(channels, SampleFormat::F32)],
}
}
}
impl AudioNode for SourceNode {
fn inputs(&self) -> &[PortDescriptor] {
&[]
}
fn outputs(&self) -> &[PortDescriptor] {
&self.out_p
}
fn process(&mut self, _ctx: &mut ProcessContext, _i: &[AudioFrame], _o: &mut [AudioFrame]) {}
}
struct GainNode {
gain: f32,
in_p: [PortDescriptor; 1],
out_p: [PortDescriptor; 1],
}
impl GainNode {
fn new(gain: f32) -> Self {
Self {
gain,
in_p: [PortDescriptor::input(1, SampleFormat::F32)],
out_p: [PortDescriptor::output(1, SampleFormat::F32)],
}
}
}
impl AudioNode for GainNode {
fn inputs(&self) -> &[PortDescriptor] {
&self.in_p
}
fn outputs(&self) -> &[PortDescriptor] {
&self.out_p
}
fn process(&mut self, _ctx: &mut ProcessContext, i: &[AudioFrame], o: &mut [AudioFrame]) {
let (Some(inp), Some(out)) = (i.first(), o.get_mut(0)) else {
return;
};
let n = inp.samples.len().min(out.samples.len());
for k in 0..n {
out.samples[k] = inp.samples[k] * self.gain;
}
}
}
#[test]
fn process_cycle_is_alloc_free_across_1000_cycles() {
{
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let a = g.add_node(Box::new(GainNode::new(0.5)));
let b = g.add_node(Box::new(GainNode::new(2.0)));
g.link((src, 0), (a, 0)).unwrap();
g.link((a, 0), (b, 0)).unwrap();
g.compile(GraphConfig::new(256, 48_000, 1)).unwrap();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![0.5; 256]));
let mut ctx = ProcessContext::new(256, 0, 48_000);
rt_start_measuring();
for _ in 0..1000 {
g.process_cycle(&mut ctx).unwrap();
}
let n = rt_stop_and_count();
assert_eq!(
n, 0,
"RT path (gain chain): process_cycle allocated {n} times across 1000 cycles — RT-safe violation"
);
}
{
const N: usize = 128;
let (mut prod_in, cons_in) = rtrb::RingBuffer::<AudioFrame>::new(4);
let (prod_out, cons_out) = rtrb::RingBuffer::<AudioFrame>::new(4);
let mut g = Graph::new();
let rsrc = g.add_node(Box::new(audio_graph_bsd::RingSource::new(
cons_in, 1, 48_000, N,
)));
let gain = g.add_node(Box::new(GainNode::new(1.0)));
let rsink = g.add_node(Box::new(audio_graph_bsd::RingSink::new(
prod_out, 1, 48_000, N,
)));
g.link((rsrc, 0), (gain, 0)).unwrap();
g.link((gain, 0), (rsink, 0)).unwrap();
g.compile(GraphConfig::new(N, 48_000, 1)).unwrap();
for _ in 0..4 {
let _ = prod_in.push(AudioFrame::from_planar(1, 48_000, vec![0.25; N]));
}
let mut ctx = ProcessContext::new(N, 0, 48_000);
rt_start_measuring();
for _ in 0..1000 {
g.process_cycle(&mut ctx).unwrap();
}
let n = rt_stop_and_count();
assert_eq!(
n, 0,
"RT path (ring nodes): process_cycle allocated {n} times across 1000 cycles — RT-safe violation"
);
drop(cons_out);
}
}