1use crate::error::GraphError;
9use crate::topology::{topological_sort, Edge};
10use audio_core_bsd::{AudioFrame, AudioNode, PortDirection, ProcessContext};
11
12pub type NodeId = usize;
14
15pub type PortIdx = usize;
19
20pub type LinkId = usize;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct GraphConfig {
31 pub num_frames: usize,
33 pub sample_rate: u32,
35 pub channels: u16,
37}
38
39impl GraphConfig {
40 #[must_use]
42 pub const fn new(num_frames: usize, sample_rate: u32, channels: u16) -> Self {
43 Self {
44 num_frames,
45 sample_rate,
46 channels,
47 }
48 }
49}
50
51pub struct Graph {
66 nodes: Vec<Box<dyn AudioNode>>,
68 edges: Vec<Edge>,
70 execution_order: Vec<NodeId>,
72 config: GraphConfig,
74 compiled: bool,
76 input_scratch: Vec<Vec<AudioFrame>>,
78 output_scratch: Vec<Vec<AudioFrame>>,
80}
81
82impl Graph {
83 #[must_use]
85 pub fn new() -> Self {
86 Self {
87 nodes: Vec::new(),
88 edges: Vec::new(),
89 execution_order: Vec::new(),
90 config: GraphConfig::new(0, 0, 0),
91 compiled: false,
92 input_scratch: Vec::new(),
93 output_scratch: Vec::new(),
94 }
95 }
96
97 #[must_use]
103 pub fn add_node(&mut self, node: Box<dyn AudioNode>) -> NodeId {
104 let id = self.nodes.len();
105 self.nodes.push(node);
106 self.input_scratch.push(Vec::new());
109 self.output_scratch.push(Vec::new());
110 id
111 }
112
113 pub fn link(
128 &mut self,
129 from: (NodeId, PortIdx),
130 to: (NodeId, PortIdx),
131 ) -> Result<LinkId, GraphError> {
132 let (from_node, from_port) = from;
133 let (to_node, to_port) = to;
134
135 let (from_desc, to_desc) = {
138 let from_n = self
139 .nodes
140 .get(from_node)
141 .ok_or(GraphError::NodeNotFound(from_node))?;
142 let to_n = self
143 .nodes
144 .get(to_node)
145 .ok_or(GraphError::NodeNotFound(to_node))?;
146 let from_desc = from_n
147 .outputs()
148 .get(from_port)
149 .ok_or(GraphError::PortNotFound {
150 node: from_node,
151 port: from_port,
152 })?;
153 let to_desc = to_n.inputs().get(to_port).ok_or(GraphError::PortNotFound {
154 node: to_node,
155 port: to_port,
156 })?;
157 (*from_desc, *to_desc)
158 };
159
160 if from_desc.direction != PortDirection::Output || to_desc.direction != PortDirection::Input
161 {
162 return Err(GraphError::PortDirectionMismatch { from, to });
163 }
164 if from_desc.channels != to_desc.channels
165 || from_desc.sample_format != to_desc.sample_format
166 {
167 return Err(GraphError::PortIncompatible { from, to });
168 }
169
170 let link_id = self.edges.len();
171 self.edges.push(Edge { from, to });
172 Ok(link_id)
173 }
174
175 pub fn compile(&mut self, config: GraphConfig) -> Result<(), GraphError> {
186 if self.compiled {
187 return Err(GraphError::AlreadyCompiled);
188 }
189 let order = topological_sort(self.nodes.len(), &self.edges)
190 .map_err(|remaining| GraphError::CycleDetected { nodes: remaining })?;
191 self.execution_order = order;
192 self.config = config;
193
194 self.input_scratch = Vec::with_capacity(self.nodes.len());
197 self.output_scratch = Vec::with_capacity(self.nodes.len());
198 for node in &self.nodes {
199 let in_slots: Vec<AudioFrame> = node
200 .inputs()
201 .iter()
202 .map(|_| {
203 AudioFrame::silence(config.channels, config.num_frames, config.sample_rate)
204 })
205 .collect();
206 let out_slots: Vec<AudioFrame> = node
207 .outputs()
208 .iter()
209 .map(|_| {
210 AudioFrame::silence(config.channels, config.num_frames, config.sample_rate)
211 })
212 .collect();
213 self.input_scratch.push(in_slots);
214 self.output_scratch.push(out_slots);
215 }
216 self.compiled = true;
217 Ok(())
218 }
219
220 pub fn process_cycle(&mut self, ctx: &mut ProcessContext) -> Result<(), GraphError> {
241 if !self.compiled {
242 return Err(GraphError::NotCompiled);
243 }
244
245 let Graph {
249 nodes,
250 edges,
251 execution_order,
252 input_scratch,
253 output_scratch,
254 ..
255 } = self;
256
257 for &n in execution_order.iter() {
258 let Some(in_slots) = input_scratch.get_mut(n) else {
260 continue;
261 };
262 for (pi, slot) in in_slots.iter_mut().enumerate() {
263 let mut sourced = false;
264 for edge in edges.iter() {
265 if edge.to == (n, pi) {
266 let (src, src_port) = edge.from;
267 if let Some(src_slots) = output_scratch.get(src) {
268 if let Some(src_frame) = src_slots.get(src_port) {
269 slot.channels = src_frame.channels;
270 slot.sample_rate = src_frame.sample_rate;
271 let copy_len = src_frame.samples.len().min(slot.samples.len());
272 slot.samples[..copy_len]
274 .copy_from_slice(&src_frame.samples[..copy_len]);
275 }
276 }
277 sourced = true;
278 break;
279 }
280 }
281 if !sourced {
282 for s in &mut slot.samples {
284 *s = 0.0;
285 }
286 }
287 }
288
289 let Some(out_slots) = output_scratch.get_mut(n) else {
291 continue;
292 };
293 let Some(node) = nodes.get_mut(n) else {
294 continue;
295 };
296 node.process(ctx, in_slots.as_slice(), out_slots.as_mut_slice());
297 }
298
299 Ok(())
300 }
301
302 pub fn feed(&mut self, node: NodeId, port: PortIdx, src: &AudioFrame) {
309 if let Some(slots) = self.output_scratch.get_mut(node) {
310 if let Some(dst) = slots.get_mut(port) {
311 dst.channels = src.channels;
312 dst.sample_rate = src.sample_rate;
313 let copy_len = src.samples.len().min(dst.samples.len());
314 dst.samples[..copy_len].copy_from_slice(&src.samples[..copy_len]);
315 }
316 }
317 }
318
319 #[must_use]
323 pub fn read_output(&self, node: NodeId, port: PortIdx) -> Option<&AudioFrame> {
324 self.output_scratch.get(node).and_then(|s| s.get(port))
325 }
326
327 #[must_use]
332 pub fn read_input(&self, node: NodeId, port: PortIdx) -> Option<&AudioFrame> {
333 self.input_scratch.get(node).and_then(|s| s.get(port))
334 }
335
336 #[must_use]
338 pub fn node_count(&self) -> usize {
339 self.nodes.len()
340 }
341
342 #[must_use]
344 pub fn link_count(&self) -> usize {
345 self.edges.len()
346 }
347
348 #[must_use]
350 pub fn is_compiled(&self) -> bool {
351 self.compiled
352 }
353
354 #[must_use]
358 pub fn config(&self) -> GraphConfig {
359 self.config
360 }
361}
362
363impl Default for Graph {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use audio_core_bsd::{
373 AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
374 };
375
376 struct GainNode {
378 gain: f32,
379 in_port: [PortDescriptor; 1],
380 out_port: [PortDescriptor; 1],
381 }
382
383 impl GainNode {
384 fn new(gain: f32) -> Self {
385 Self {
386 gain,
387 in_port: [PortDescriptor::new(
388 PortDirection::Input,
389 1,
390 SampleFormat::F32,
391 )],
392 out_port: [PortDescriptor::new(
393 PortDirection::Output,
394 1,
395 SampleFormat::F32,
396 )],
397 }
398 }
399 }
400
401 impl AudioNode for GainNode {
402 fn inputs(&self) -> &[PortDescriptor] {
403 &self.in_port
404 }
405 fn outputs(&self) -> &[PortDescriptor] {
406 &self.out_port
407 }
408 fn process(
409 &mut self,
410 _ctx: &mut ProcessContext,
411 in_frames: &[AudioFrame],
412 out_frames: &mut [AudioFrame],
413 ) {
414 let Some(inp) = in_frames.first() else {
415 return;
416 };
417 let Some(out) = out_frames.get_mut(0) else {
418 return;
419 };
420 let n = inp.samples.len().min(out.samples.len());
421 for i in 0..n {
422 out.samples[i] = inp.samples[i] * self.gain;
423 }
424 }
425 }
426
427 struct SourceNode {
433 out_port: [PortDescriptor; 1],
434 }
435
436 impl SourceNode {
437 fn new(channels: u16) -> Self {
438 Self {
439 out_port: [PortDescriptor::new(
440 PortDirection::Output,
441 channels,
442 SampleFormat::F32,
443 )],
444 }
445 }
446 }
447
448 impl AudioNode for SourceNode {
449 fn inputs(&self) -> &[PortDescriptor] {
450 &[]
451 }
452 fn outputs(&self) -> &[PortDescriptor] {
453 &self.out_port
454 }
455 fn process(
456 &mut self,
457 _ctx: &mut ProcessContext,
458 _in_frames: &[AudioFrame],
459 _out_frames: &mut [AudioFrame],
460 ) {
461 }
463 }
464
465 fn approx_eq(a: f32, b: f32) -> bool {
467 (a - b).abs() < 1e-6
468 }
469
470 #[test]
471 fn empty_graph_compiles_and_runs() {
472 let mut g = Graph::new();
473 assert_eq!(g.node_count(), 0);
474 assert_eq!(g.link_count(), 0);
475 g.compile(GraphConfig::new(64, 48_000, 1)).unwrap();
476 let mut ctx = ProcessContext::new(64, 0, 48_000);
477 g.process_cycle(&mut ctx).unwrap();
478 }
479
480 #[test]
481 fn default_equals_new() {
482 let a = Graph::new();
483 let b = Graph::default();
484 assert_eq!(a.node_count(), b.node_count());
485 assert_eq!(a.link_count(), b.link_count());
486 }
487
488 #[test]
489 fn add_node_returns_sequential_ids() {
490 let mut g = Graph::new();
491 let a = g.add_node(Box::new(GainNode::new(1.0)));
492 let b = g.add_node(Box::new(GainNode::new(1.0)));
493 assert_eq!(a, 0);
494 assert_eq!(b, 1);
495 assert_eq!(g.node_count(), 2);
496 }
497
498 #[test]
499 fn link_valid_ports_succeeds() {
500 let mut g = Graph::new();
501 let s = g.add_node(Box::new(GainNode::new(1.0)));
502 let d = g.add_node(Box::new(GainNode::new(1.0)));
503 let link = g.link((s, 0), (d, 0)).unwrap();
504 assert_eq!(link, 0);
505 assert_eq!(g.link_count(), 1);
506 }
507
508 #[test]
509 fn link_unknown_node_errors() {
510 let mut g = Graph::new();
511 let s = g.add_node(Box::new(GainNode::new(1.0)));
512 assert_eq!(g.link((s, 0), (99, 0)), Err(GraphError::NodeNotFound(99)));
513 assert_eq!(g.link((99, 0), (s, 0)), Err(GraphError::NodeNotFound(99)));
514 }
515
516 #[test]
517 fn link_bad_port_errors() {
518 let mut g = Graph::new();
519 let s = g.add_node(Box::new(GainNode::new(1.0)));
520 let d = g.add_node(Box::new(GainNode::new(1.0)));
521 assert_eq!(
522 g.link((s, 5), (d, 0)),
523 Err(GraphError::PortNotFound { node: s, port: 5 })
524 );
525 assert_eq!(
526 g.link((s, 0), (d, 9)),
527 Err(GraphError::PortNotFound { node: d, port: 9 })
528 );
529 }
530
531 #[test]
532 fn process_before_compile_errors() {
533 let mut g = Graph::new();
534 let mut ctx = ProcessContext::new(64, 0, 48_000);
535 assert_eq!(g.process_cycle(&mut ctx), Err(GraphError::NotCompiled));
536 }
537
538 #[test]
539 fn compile_twice_errors() {
540 let mut g = Graph::new();
541 g.compile(GraphConfig::new(64, 48_000, 1)).unwrap();
542 assert_eq!(
543 g.compile(GraphConfig::new(64, 48_000, 1)),
544 Err(GraphError::AlreadyCompiled)
545 );
546 }
547
548 #[test]
549 fn cycle_is_rejected_at_compile() {
550 let mut g = Graph::new();
551 let a = g.add_node(Box::new(GainNode::new(1.0)));
552 let b = g.add_node(Box::new(GainNode::new(1.0)));
553 g.link((a, 0), (b, 0)).unwrap();
554 g.link((b, 0), (a, 0)).unwrap();
555 let err = g.compile(GraphConfig::new(64, 48_000, 1)).unwrap_err();
556 assert!(matches!(err, GraphError::CycleDetected { .. }));
557 }
558
559 #[test]
560 fn end_to_end_gain_chain() {
561 let mut g = Graph::new();
562 let src = g.add_node(Box::new(SourceNode::new(1))); let mid = g.add_node(Box::new(GainNode::new(0.5))); g.link((src, 0), (mid, 0)).unwrap();
565 g.compile(GraphConfig::new(8, 48_000, 1)).unwrap();
566
567 g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 8]));
570 let mut ctx = ProcessContext::new(8, 0, 48_000);
571 g.process_cycle(&mut ctx).unwrap();
572
573 let mid_in = g.read_input(mid, 0).unwrap();
575 assert!(mid_in.samples.iter().all(|&s| approx_eq(s, 1.0)));
576 let mid_out = g.read_output(mid, 0).unwrap();
577 assert!(mid_out.samples.iter().all(|&s| approx_eq(s, 0.5)));
578 }
579
580 #[test]
581 fn three_stage_chain_composes_gains() {
582 let mut g = Graph::new();
583 let src = g.add_node(Box::new(SourceNode::new(1)));
584 let a = g.add_node(Box::new(GainNode::new(2.0)));
585 let b = g.add_node(Box::new(GainNode::new(3.0)));
586 let c = g.add_node(Box::new(GainNode::new(0.5)));
587 g.link((src, 0), (a, 0)).unwrap();
588 g.link((a, 0), (b, 0)).unwrap();
589 g.link((b, 0), (c, 0)).unwrap();
590 g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
591
592 g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 4]));
593 let mut ctx = ProcessContext::new(4, 0, 48_000);
594 g.process_cycle(&mut ctx).unwrap();
595
596 let out = g.read_output(c, 0).unwrap();
598 assert!(out.samples.iter().all(|&s| approx_eq(s, 3.0)));
599 }
600
601 #[test]
602 fn unconnected_input_is_silenced() {
603 let mut g = Graph::new();
604 let n = g.add_node(Box::new(GainNode::new(1.0)));
605 g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
606 let mut ctx = ProcessContext::new(4, 0, 48_000);
607 g.process_cycle(&mut ctx).unwrap();
608 let inp = g.read_input(n, 0).unwrap();
609 assert!(inp.samples.iter().all(|&s| approx_eq(s, 0.0)));
610 }
611
612 #[test]
613 fn read_out_of_range_returns_none() {
614 let mut g = Graph::new();
615 let n = g.add_node(Box::new(GainNode::new(1.0)));
616 g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
617 assert!(g.read_output(99, 0).is_none());
618 assert!(g.read_output(n, 99).is_none());
619 assert!(g.read_input(99, 0).is_none());
620 assert!(g.read_input(n, 99).is_none());
621 }
622
623 #[test]
624 fn feed_out_of_range_is_noop() {
625 let mut g = Graph::new();
626 let _n = g.add_node(Box::new(GainNode::new(1.0)));
627 g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
628 g.feed(99, 0, &AudioFrame::silence(1, 4, 48_000));
630 g.feed(0, 99, &AudioFrame::silence(1, 4, 48_000));
631 }
632
633 #[test]
634 fn graphconfig_new_and_equality() {
635 let a = GraphConfig::new(256, 48_000, 2);
636 assert_eq!(a, GraphConfig::new(256, 48_000, 2));
637 assert_ne!(a, GraphConfig::new(128, 48_000, 2));
638 assert_eq!(a.num_frames, 256);
639 assert_eq!(a.sample_rate, 48_000);
640 assert_eq!(a.channels, 2);
641 }
642
643 #[test]
644 fn is_compiled_and_config_reflect_state() {
645 let mut g = Graph::new();
646 assert!(!g.is_compiled());
647 g.compile(GraphConfig::new(4, 44_100, 2)).unwrap();
648 assert!(g.is_compiled());
649 assert_eq!(g.config(), GraphConfig::new(4, 44_100, 2));
650 }
651
652 #[test]
653 fn diamond_topology_processes_in_dependency_order() {
654 let mut g = Graph::new();
657 let src = g.add_node(Box::new(SourceNode::new(1)));
658 let a = g.add_node(Box::new(GainNode::new(2.0)));
659 let b = g.add_node(Box::new(GainNode::new(3.0)));
660 let sink = g.add_node(Box::new(GainNode::new(1.0)));
661 g.link((src, 0), (a, 0)).unwrap();
662 g.link((src, 0), (b, 0)).unwrap();
663 g.link((a, 0), (sink, 0)).unwrap();
664 let _ = b;
665 g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
666
667 g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 4]));
668 let mut ctx = ProcessContext::new(4, 0, 48_000);
669 g.process_cycle(&mut ctx).unwrap();
670 let out = g.read_output(sink, 0).unwrap();
671 assert!(out.samples.iter().all(|&s| approx_eq(s, 2.0)));
673 }
674}