use std::cell::UnsafeCell;
use crate::error::GraphError;
use crate::flush::{FlushError, SinkNode};
use crate::topology::{topological_sort, Edge};
use audio_core_bsd::{AudioFrame, AudioNode, PortDirection, ProcessContext};
#[cfg(feature = "topology")]
use crate::topology_pub::{
NodeSnapshot, PortMeta, SnapshotEdge, SnapshotSource, TopologyEvent, TopologySnapshot,
};
#[cfg(feature = "distributed")]
use crate::distributed::{BoundaryPort, PartitionHint, PortKind, RemoteNode};
#[cfg(feature = "distributed")]
use crate::topology_pub::PortDir;
pub type NodeId = usize;
pub type PortIdx = usize;
pub type LinkId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GraphConfig {
pub num_frames: usize,
pub sample_rate: u32,
pub channels: u16,
}
impl GraphConfig {
#[must_use]
pub const fn new(num_frames: usize, sample_rate: u32, channels: u16) -> Self {
Self {
num_frames,
sample_rate,
channels,
}
}
}
enum NodeSlot {
Plain(Box<dyn AudioNode>),
Sink(Box<dyn SinkNode>),
}
impl AudioNode for NodeSlot {
fn inputs(&self) -> &[audio_core_bsd::PortDescriptor] {
match self {
Self::Plain(n) => n.inputs(),
Self::Sink(n) => n.inputs(),
}
}
fn outputs(&self) -> &[audio_core_bsd::PortDescriptor] {
match self {
Self::Plain(n) => n.outputs(),
Self::Sink(n) => n.outputs(),
}
}
fn process(&mut self, ctx: &mut ProcessContext, i: &[AudioFrame], o: &mut [AudioFrame]) {
match self {
Self::Plain(n) => n.process(ctx, i, o),
Self::Sink(n) => n.process(ctx, i, o),
}
}
}
struct GraphScratch {
nodes: Vec<NodeSlot>,
input_scratch: Vec<Vec<AudioFrame>>,
output_scratch: Vec<Vec<AudioFrame>>,
}
pub struct Graph {
edges: Vec<Edge>,
execution_order: Vec<NodeId>,
config: GraphConfig,
compiled: bool,
scratch: UnsafeCell<GraphScratch>,
#[cfg(feature = "topology")]
subscribers: Vec<std::sync::mpsc::Sender<TopologyEvent>>,
}
impl Graph {
#[must_use]
pub fn new() -> Self {
Self {
edges: Vec::new(),
execution_order: Vec::new(),
config: GraphConfig::new(0, 0, 0),
compiled: false,
scratch: UnsafeCell::new(GraphScratch {
nodes: Vec::new(),
input_scratch: Vec::new(),
output_scratch: Vec::new(),
}),
#[cfg(feature = "topology")]
subscribers: Vec::new(),
}
}
#[must_use]
pub fn add_node(&mut self, node: Box<dyn AudioNode>) -> NodeId {
let scratch = self.scratch.get_mut();
let id = scratch.nodes.len();
scratch.nodes.push(NodeSlot::Plain(node));
scratch.input_scratch.push(Vec::new());
scratch.output_scratch.push(Vec::new());
#[cfg(feature = "topology")]
self.emit_node_added(id);
id
}
pub fn add_sink(&mut self, sink: Box<dyn SinkNode>) -> NodeId {
let scratch = self.scratch.get_mut();
let id = scratch.nodes.len();
scratch.nodes.push(NodeSlot::Sink(sink));
scratch.input_scratch.push(Vec::new());
scratch.output_scratch.push(Vec::new());
#[cfg(feature = "topology")]
self.emit_node_added(id);
id
}
pub fn link(
&mut self,
from: (NodeId, PortIdx),
to: (NodeId, PortIdx),
) -> Result<LinkId, GraphError> {
let (from_node, from_port) = from;
let (to_node, to_port) = to;
let (from_desc, to_desc) = {
let scratch = self.scratch.get_mut();
let from_n = scratch
.nodes
.get(from_node)
.ok_or(GraphError::NodeNotFound(from_node))?;
let to_n = scratch
.nodes
.get(to_node)
.ok_or(GraphError::NodeNotFound(to_node))?;
let from_desc = from_n
.outputs()
.get(from_port)
.ok_or(GraphError::PortNotFound {
node: from_node,
port: from_port,
})?;
let to_desc = to_n.inputs().get(to_port).ok_or(GraphError::PortNotFound {
node: to_node,
port: to_port,
})?;
(*from_desc, *to_desc)
};
if from_desc.direction != PortDirection::Output || to_desc.direction != PortDirection::Input
{
return Err(GraphError::PortDirectionMismatch { from, to });
}
if from_desc.channels != to_desc.channels
|| from_desc.sample_format != to_desc.sample_format
{
return Err(GraphError::PortIncompatible { from, to });
}
let link_id = self.edges.len();
self.edges.push(Edge { from, to });
#[cfg(feature = "topology")]
self.emit_event(&TopologyEvent::LinkAdded(SnapshotEdge { from, to }));
Ok(link_id)
}
pub fn compile(&mut self, config: GraphConfig) -> Result<(), GraphError> {
if self.compiled {
return Err(GraphError::AlreadyCompiled);
}
let order = topological_sort(self.scratch.get_mut().nodes.len(), &self.edges)
.map_err(|remaining| GraphError::CycleDetected { nodes: remaining })?;
self.execution_order = order;
self.config = config;
let scratch = self.scratch.get_mut();
scratch.input_scratch = Vec::with_capacity(scratch.nodes.len());
scratch.output_scratch = Vec::with_capacity(scratch.nodes.len());
for node in &scratch.nodes {
let in_slots: Vec<AudioFrame> = node
.inputs()
.iter()
.map(|_| {
AudioFrame::silence(config.channels, config.num_frames, config.sample_rate)
})
.collect();
let out_slots: Vec<AudioFrame> = node
.outputs()
.iter()
.map(|_| {
AudioFrame::silence(config.channels, config.num_frames, config.sample_rate)
})
.collect();
scratch.input_scratch.push(in_slots);
scratch.output_scratch.push(out_slots);
}
self.compiled = true;
#[cfg(feature = "topology")]
self.emit_event(&TopologyEvent::GraphCompiled);
Ok(())
}
pub fn process_cycle(&self, ctx: &mut ProcessContext) -> Result<(), GraphError> {
if !self.compiled {
return Err(GraphError::NotCompiled);
}
let GraphScratch {
nodes,
input_scratch,
output_scratch,
} = unsafe { &mut *self.scratch.get() };
for &n in &self.execution_order {
let Some(in_slots) = input_scratch.get_mut(n) else {
continue;
};
for (pi, slot) in in_slots.iter_mut().enumerate() {
let mut sourced = false;
for edge in &self.edges {
if edge.to == (n, pi) {
let (src, src_port) = edge.from;
if let Some(src_slots) = output_scratch.get(src) {
if let Some(src_frame) = src_slots.get(src_port) {
slot.channels = src_frame.channels;
slot.sample_rate = src_frame.sample_rate;
let copy_len = src_frame.samples.len().min(slot.samples.len());
slot.samples[..copy_len]
.copy_from_slice(&src_frame.samples[..copy_len]);
}
}
sourced = true;
break;
}
}
if !sourced {
for s in &mut slot.samples {
*s = 0.0;
}
}
}
let Some(out_slots) = output_scratch.get_mut(n) else {
continue;
};
let Some(node) = nodes.get_mut(n) else {
continue;
};
node.process(ctx, in_slots.as_slice(), out_slots.as_mut_slice());
}
Ok(())
}
pub fn feed(&mut self, node: NodeId, port: PortIdx, src: &AudioFrame) {
if let Some(slots) = self.scratch.get_mut().output_scratch.get_mut(node) {
if let Some(dst) = slots.get_mut(port) {
dst.channels = src.channels;
dst.sample_rate = src.sample_rate;
let copy_len = src.samples.len().min(dst.samples.len());
dst.samples[..copy_len].copy_from_slice(&src.samples[..copy_len]);
}
}
}
pub fn flush_sinks(&mut self) -> (usize, Option<FlushError>) {
let scratch = self.scratch.get_mut();
let mut count = 0;
let mut first_err = None;
for slot in &mut scratch.nodes {
if let NodeSlot::Sink(sink) = slot {
count += 1;
if let Err(e) = sink.flush() {
if first_err.is_none() {
first_err = Some(e);
}
}
}
}
(count, first_err)
}
pub fn flush_sink(&mut self, node: NodeId) -> Result<(), FlushError> {
let scratch = self.scratch.get_mut();
match scratch.nodes.get_mut(node) {
Some(NodeSlot::Sink(sink)) => sink.flush(),
Some(NodeSlot::Plain(_)) => Err(FlushError::NotFlushable(node)),
None => Err(FlushError::NodeNotFound(node)),
}
}
#[must_use]
pub fn read_output(&self, node: NodeId, port: PortIdx) -> Option<&AudioFrame> {
let scratch = unsafe { &*self.scratch.get() };
scratch.output_scratch.get(node).and_then(|s| s.get(port))
}
#[must_use]
pub fn read_input(&self, node: NodeId, port: PortIdx) -> Option<&AudioFrame> {
let scratch = unsafe { &*self.scratch.get() };
scratch.input_scratch.get(node).and_then(|s| s.get(port))
}
#[must_use]
pub fn node_count(&self) -> usize {
unsafe { &*self.scratch.get() }.nodes.len()
}
#[must_use]
pub fn link_count(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn is_compiled(&self) -> bool {
self.compiled
}
#[must_use]
pub fn config(&self) -> GraphConfig {
self.config
}
}
impl Default for Graph {
fn default() -> Self {
Self::new()
}
}
unsafe impl Sync for Graph {}
#[cfg(feature = "topology")]
impl SnapshotSource for Graph {
fn topology_snapshot(&self) -> TopologySnapshot {
let scratch = unsafe { &*self.scratch.get() };
let nodes = scratch
.nodes
.iter()
.enumerate()
.map(|(id, n)| NodeSnapshot {
id,
inputs: n
.inputs()
.iter()
.map(|d| PortMeta::from_descriptor(*d))
.collect(),
outputs: n
.outputs()
.iter()
.map(|d| PortMeta::from_descriptor(*d))
.collect(),
})
.collect();
let edges = self
.edges
.iter()
.map(|e| SnapshotEdge {
from: e.from,
to: e.to,
})
.collect();
TopologySnapshot { nodes, edges }
}
}
#[cfg(feature = "topology")]
impl Graph {
pub fn from_snapshot(
snapshot: &TopologySnapshot,
factory: &mut dyn FnMut(NodeId) -> Option<Box<dyn AudioNode>>,
) -> Result<Graph, GraphError> {
let mut g = Graph::new();
let mut id_map = std::collections::HashMap::<NodeId, NodeId>::new();
for ns in &snapshot.nodes {
let node = factory(ns.id).ok_or(GraphError::NodeNotFound(ns.id))?;
let new_id = g.add_node(node);
id_map.insert(ns.id, new_id);
}
for se in &snapshot.edges {
let new_from = id_map
.get(&se.from.0)
.copied()
.ok_or(GraphError::NodeNotFound(se.from.0))?;
let new_to = id_map
.get(&se.to.0)
.copied()
.ok_or(GraphError::NodeNotFound(se.to.0))?;
g.link((new_from, se.from.1), (new_to, se.to.1))?;
}
Ok(g)
}
#[must_use]
pub fn subscribe_topology(&mut self) -> std::sync::mpsc::Receiver<TopologyEvent> {
let (tx, rx) = std::sync::mpsc::channel();
self.subscribers.push(tx);
rx
}
fn emit_event(&mut self, event: &TopologyEvent) {
self.subscribers.retain(|tx| tx.send(event.clone()).is_ok());
}
fn emit_node_added(&mut self, id: NodeId) {
let snapshot = {
let n = self
.scratch
.get_mut()
.nodes
.get(id)
.expect("emit_node_added called with a just-assigned NodeId");
NodeSnapshot {
id,
inputs: n
.inputs()
.iter()
.map(|d| PortMeta::from_descriptor(*d))
.collect(),
outputs: n
.outputs()
.iter()
.map(|d| PortMeta::from_descriptor(*d))
.collect(),
}
};
self.emit_event(&TopologyEvent::NodeAdded(snapshot));
}
}
#[cfg(feature = "distributed")]
impl Graph {
#[must_use]
pub fn partition_hints(
&self,
boundaries: &[(NodeId, PortIdx, RemoteNode)],
) -> Vec<PartitionHint> {
let local_nodes: Vec<NodeId> = (0..self.node_count()).collect();
let boundary_ports: Vec<BoundaryPort> = boundaries
.iter()
.filter_map(|(node, port, remote)| {
let direction = self.boundary_port_direction(*node, *port)?;
Some(BoundaryPort {
node: *node,
port: *port,
kind: PortKind::Network {
remote: remote.clone(),
},
direction,
})
})
.collect();
vec![PartitionHint {
local_nodes,
boundary_ports,
}]
}
fn boundary_port_direction(&self, node: NodeId, port: PortIdx) -> Option<PortDir> {
let n = unsafe { &*self.scratch.get() }.nodes.get(node)?;
if port < n.outputs().len() {
Some(PortDir::Output)
} else if port < n.inputs().len() {
Some(PortDir::Input)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use audio_core_bsd::{
AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
};
struct GainNode {
gain: f32,
in_port: [PortDescriptor; 1],
out_port: [PortDescriptor; 1],
}
impl GainNode {
fn new(gain: f32) -> Self {
Self {
gain,
in_port: [PortDescriptor::new(
PortDirection::Input,
1,
SampleFormat::F32,
)],
out_port: [PortDescriptor::new(
PortDirection::Output,
1,
SampleFormat::F32,
)],
}
}
}
impl AudioNode for GainNode {
fn inputs(&self) -> &[PortDescriptor] {
&self.in_port
}
fn outputs(&self) -> &[PortDescriptor] {
&self.out_port
}
fn process(
&mut self,
_ctx: &mut ProcessContext,
in_frames: &[AudioFrame],
out_frames: &mut [AudioFrame],
) {
let Some(inp) = in_frames.first() else {
return;
};
let Some(out) = out_frames.get_mut(0) else {
return;
};
let n = inp.samples.len().min(out.samples.len());
for i in 0..n {
out.samples[i] = inp.samples[i] * self.gain;
}
}
}
struct SourceNode {
out_port: [PortDescriptor; 1],
}
impl SourceNode {
fn new(channels: u16) -> Self {
Self {
out_port: [PortDescriptor::new(
PortDirection::Output,
channels,
SampleFormat::F32,
)],
}
}
}
impl AudioNode for SourceNode {
fn inputs(&self) -> &[PortDescriptor] {
&[]
}
fn outputs(&self) -> &[PortDescriptor] {
&self.out_port
}
fn process(
&mut self,
_ctx: &mut ProcessContext,
_in_frames: &[AudioFrame],
_out_frames: &mut [AudioFrame],
) {
}
}
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-6
}
#[test]
fn empty_graph_compiles_and_runs() {
let mut g = Graph::new();
assert_eq!(g.node_count(), 0);
assert_eq!(g.link_count(), 0);
g.compile(GraphConfig::new(64, 48_000, 1)).unwrap();
let mut ctx = ProcessContext::new(64, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
}
#[test]
fn default_equals_new() {
let a = Graph::new();
let b = Graph::default();
assert_eq!(a.node_count(), b.node_count());
assert_eq!(a.link_count(), b.link_count());
}
#[test]
fn add_node_returns_sequential_ids() {
let mut g = Graph::new();
let a = g.add_node(Box::new(GainNode::new(1.0)));
let b = g.add_node(Box::new(GainNode::new(1.0)));
assert_eq!(a, 0);
assert_eq!(b, 1);
assert_eq!(g.node_count(), 2);
}
#[test]
fn link_valid_ports_succeeds() {
let mut g = Graph::new();
let s = g.add_node(Box::new(GainNode::new(1.0)));
let d = g.add_node(Box::new(GainNode::new(1.0)));
let link = g.link((s, 0), (d, 0)).unwrap();
assert_eq!(link, 0);
assert_eq!(g.link_count(), 1);
}
#[test]
fn link_unknown_node_errors() {
let mut g = Graph::new();
let s = g.add_node(Box::new(GainNode::new(1.0)));
assert_eq!(g.link((s, 0), (99, 0)), Err(GraphError::NodeNotFound(99)));
assert_eq!(g.link((99, 0), (s, 0)), Err(GraphError::NodeNotFound(99)));
}
#[test]
fn link_bad_port_errors() {
let mut g = Graph::new();
let s = g.add_node(Box::new(GainNode::new(1.0)));
let d = g.add_node(Box::new(GainNode::new(1.0)));
assert_eq!(
g.link((s, 5), (d, 0)),
Err(GraphError::PortNotFound { node: s, port: 5 })
);
assert_eq!(
g.link((s, 0), (d, 9)),
Err(GraphError::PortNotFound { node: d, port: 9 })
);
}
#[test]
fn process_before_compile_errors() {
let g = Graph::new();
let mut ctx = ProcessContext::new(64, 0, 48_000);
assert_eq!(g.process_cycle(&mut ctx), Err(GraphError::NotCompiled));
}
#[test]
fn compile_twice_errors() {
let mut g = Graph::new();
g.compile(GraphConfig::new(64, 48_000, 1)).unwrap();
assert_eq!(
g.compile(GraphConfig::new(64, 48_000, 1)),
Err(GraphError::AlreadyCompiled)
);
}
#[test]
fn cycle_is_rejected_at_compile() {
let mut g = Graph::new();
let a = g.add_node(Box::new(GainNode::new(1.0)));
let b = g.add_node(Box::new(GainNode::new(1.0)));
g.link((a, 0), (b, 0)).unwrap();
g.link((b, 0), (a, 0)).unwrap();
let err = g.compile(GraphConfig::new(64, 48_000, 1)).unwrap_err();
assert!(matches!(err, GraphError::CycleDetected { .. }));
}
#[test]
fn end_to_end_gain_chain() {
let mut g = Graph::new();
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();
g.compile(GraphConfig::new(8, 48_000, 1)).unwrap();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 8]));
let mut ctx = ProcessContext::new(8, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let mid_in = g.read_input(mid, 0).unwrap();
assert!(mid_in.samples.iter().all(|&s| approx_eq(s, 1.0)));
let mid_out = g.read_output(mid, 0).unwrap();
assert!(mid_out.samples.iter().all(|&s| approx_eq(s, 0.5)));
}
#[test]
fn three_stage_chain_composes_gains() {
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let a = g.add_node(Box::new(GainNode::new(2.0)));
let b = g.add_node(Box::new(GainNode::new(3.0)));
let c = g.add_node(Box::new(GainNode::new(0.5)));
g.link((src, 0), (a, 0)).unwrap();
g.link((a, 0), (b, 0)).unwrap();
g.link((b, 0), (c, 0)).unwrap();
g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 4]));
let mut ctx = ProcessContext::new(4, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let out = g.read_output(c, 0).unwrap();
assert!(out.samples.iter().all(|&s| approx_eq(s, 3.0)));
}
#[test]
fn unconnected_input_is_silenced() {
let mut g = Graph::new();
let n = g.add_node(Box::new(GainNode::new(1.0)));
g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
let mut ctx = ProcessContext::new(4, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let inp = g.read_input(n, 0).unwrap();
assert!(inp.samples.iter().all(|&s| approx_eq(s, 0.0)));
}
#[test]
fn read_out_of_range_returns_none() {
let mut g = Graph::new();
let n = g.add_node(Box::new(GainNode::new(1.0)));
g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
assert!(g.read_output(99, 0).is_none());
assert!(g.read_output(n, 99).is_none());
assert!(g.read_input(99, 0).is_none());
assert!(g.read_input(n, 99).is_none());
}
#[test]
fn feed_out_of_range_is_noop() {
let mut g = Graph::new();
let _n = g.add_node(Box::new(GainNode::new(1.0)));
g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
g.feed(99, 0, &AudioFrame::silence(1, 4, 48_000));
g.feed(0, 99, &AudioFrame::silence(1, 4, 48_000));
}
#[test]
fn graphconfig_new_and_equality() {
let a = GraphConfig::new(256, 48_000, 2);
assert_eq!(a, GraphConfig::new(256, 48_000, 2));
assert_ne!(a, GraphConfig::new(128, 48_000, 2));
assert_eq!(a.num_frames, 256);
assert_eq!(a.sample_rate, 48_000);
assert_eq!(a.channels, 2);
}
#[test]
fn is_compiled_and_config_reflect_state() {
let mut g = Graph::new();
assert!(!g.is_compiled());
g.compile(GraphConfig::new(4, 44_100, 2)).unwrap();
assert!(g.is_compiled());
assert_eq!(g.config(), GraphConfig::new(4, 44_100, 2));
}
#[test]
fn diamond_topology_processes_in_dependency_order() {
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new(1)));
let a = g.add_node(Box::new(GainNode::new(2.0)));
let b = g.add_node(Box::new(GainNode::new(3.0)));
let sink = g.add_node(Box::new(GainNode::new(1.0)));
g.link((src, 0), (a, 0)).unwrap();
g.link((src, 0), (b, 0)).unwrap();
g.link((a, 0), (sink, 0)).unwrap();
let _ = b;
g.compile(GraphConfig::new(4, 48_000, 1)).unwrap();
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 4]));
let mut ctx = ProcessContext::new(4, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
let out = g.read_output(sink, 0).unwrap();
assert!(out.samples.iter().all(|&s| approx_eq(s, 2.0)));
}
}