use audio_core_bsd::{AudioFrame, AudioNode, PortDescriptor, ProcessContext, SampleFormat};
use audio_graph_bsd::{Graph, GraphConfig};
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 PassThroughNode {
in_p: [PortDescriptor; 1],
out_p: [PortDescriptor; 1],
}
impl PassThroughNode {
fn new(channels: u16) -> Self {
Self {
in_p: [PortDescriptor::input(channels, SampleFormat::F32)],
out_p: [PortDescriptor::output(channels, SampleFormat::F32)],
}
}
}
impl AudioNode for PassThroughNode {
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());
out.samples[..n].copy_from_slice(&inp.samples[..n]);
}
}
struct SumNode {
in_p: [PortDescriptor; 2],
out_p: [PortDescriptor; 1],
}
impl SumNode {
fn new(channels: u16) -> Self {
Self {
in_p: [
PortDescriptor::input(channels, SampleFormat::F32),
PortDescriptor::input(channels, SampleFormat::F32),
],
out_p: [PortDescriptor::output(channels, SampleFormat::F32)],
}
}
}
impl AudioNode for SumNode {
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(out) = o.get_mut(0) else {
return;
};
for s in &mut out.samples {
*s = 0.0;
}
for inp in i {
let n = inp.samples.len().min(out.samples.len());
for k in 0..n {
out.samples[k] += inp.samples[k];
}
}
}
}
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-6
}
fn slices_approx_eq(a: &[f32], b: &[f32]) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(&x, &y)| approx_eq(x, y))
}
#[test]
fn source_to_sink_passes_samples_unchanged() {
const N: usize = 16;
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let mid = g.add_node(Box::new(PassThroughNode::new(1)));
let sink = g.add_node(Box::new(PassThroughNode::new(1)));
g.link((src, 0), (mid, 0)).unwrap();
g.link((mid, 0), (sink, 0)).unwrap();
g.compile(GraphConfig::new(N, 48_000, 1)).unwrap();
let ramp: Vec<f32> = (0..N).map(|k| k as f32 * 0.1).collect();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, ramp.clone()));
let mut ctx = ProcessContext::new(N, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let out = g.read_output(sink, 0).expect("sink output exists");
assert!(
slices_approx_eq(&out.samples, &ramp),
"ramp not preserved through source→pass→pass; got {:?}",
out.samples
);
}
#[test]
fn ten_node_chain_thousand_cycles_no_panic() {
const N: usize = 32;
const CHAIN_LEN: usize = 10; const CYCLES: usize = 1000;
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let mut prev = src;
let mut last = src;
for _ in 0..(CHAIN_LEN - 1) {
let node = g.add_node(Box::new(PassThroughNode::new(1)));
g.link((prev, 0), (node, 0)).unwrap();
prev = node;
last = node;
}
g.compile(GraphConfig::new(N, 48_000, 1)).unwrap();
let ramp: Vec<f32> = (0..N).map(|k| (k as f32) / (N as f32)).collect();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, ramp.clone()));
let mut ctx = ProcessContext::new(N, 0, 48_000);
for _ in 0..CYCLES {
g.process_cycle(&mut ctx).unwrap();
}
let out = g.read_output(last, 0).expect("chain tail output exists");
assert!(
slices_approx_eq(&out.samples, &ramp),
"ramp corrupted after {CYCLES} cycles across {CHAIN_LEN} nodes"
);
}
#[test]
fn stereo_two_channel_signal_preserved() {
const N: usize = 8;
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(2)));
let mid = g.add_node(Box::new(PassThroughNode::new(2)));
let sink = g.add_node(Box::new(PassThroughNode::new(2)));
g.link((src, 0), (mid, 0)).unwrap();
g.link((mid, 0), (sink, 0)).unwrap();
g.compile(GraphConfig::new(N, 48_000, 2)).unwrap();
let mut stereo = Vec::with_capacity(N * 2);
for k in 0..N {
stereo.push(k as f32 * 0.05); }
for k in 0..N {
stereo.push(1.0 - k as f32 * 0.05); }
g.feed(src, 0, &AudioFrame::from_planar(2, 48_000, stereo.clone()));
let mut ctx = ProcessContext::new(N, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let out = g.read_output(sink, 0).expect("stereo sink output exists");
assert_eq!(out.channels, 2);
assert!(
slices_approx_eq(out.channel_slice(0), &stereo[..N]),
"stereo ch0 not preserved"
);
assert!(
slices_approx_eq(out.channel_slice(1), &stereo[N..]),
"stereo ch1 not preserved"
);
}
#[test]
fn diamond_fanout_fanin() {
const N: usize = 8;
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let a = g.add_node(Box::new(PassThroughNode::new(1)));
let b = g.add_node(Box::new(PassThroughNode::new(1)));
let sum = g.add_node(Box::new(SumNode::new(1)));
g.link((src, 0), (a, 0)).unwrap();
g.link((src, 0), (b, 0)).unwrap();
g.link((a, 0), (sum, 0)).unwrap();
g.link((b, 0), (sum, 1)).unwrap();
g.compile(GraphConfig::new(N, 48_000, 1)).unwrap();
let ramp: Vec<f32> = (0..N).map(|k| k as f32 * 0.1).collect();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, ramp.clone()));
let mut ctx = ProcessContext::new(N, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let out = g.read_output(sum, 0).expect("sum output exists");
let expected: Vec<f32> = ramp.iter().map(|&v| v * 2.0).collect();
assert!(
slices_approx_eq(&out.samples, &expected),
"diamond sum mismatch; got {:?} expected {:?}",
out.samples,
expected
);
}