#![cfg(feature = "topology")]
use crate::graph::{LinkId, NodeId, PortIdx};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum PortDir {
Input,
Output,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum SampleFmt {
F32,
F64,
I16,
I32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PortMeta {
pub direction: PortDir,
pub channels: u16,
pub sample_format: SampleFmt,
}
impl PortMeta {
#[must_use]
pub fn from_descriptor(d: audio_core_bsd::PortDescriptor) -> Self {
Self {
direction: match d.direction {
audio_core_bsd::PortDirection::Input => PortDir::Input,
audio_core_bsd::PortDirection::Output => PortDir::Output,
},
channels: d.channels,
sample_format: match d.sample_format {
audio_core_bsd::SampleFormat::F32 => SampleFmt::F32,
audio_core_bsd::SampleFormat::F64 => SampleFmt::F64,
audio_core_bsd::SampleFormat::I16 => SampleFmt::I16,
audio_core_bsd::SampleFormat::I32 => SampleFmt::I32,
},
}
}
#[must_use]
pub fn to_descriptor(self) -> audio_core_bsd::PortDescriptor {
audio_core_bsd::PortDescriptor::new(
match self.direction {
PortDir::Input => audio_core_bsd::PortDirection::Input,
PortDir::Output => audio_core_bsd::PortDirection::Output,
},
self.channels,
match self.sample_format {
SampleFmt::F32 => audio_core_bsd::SampleFormat::F32,
SampleFmt::F64 => audio_core_bsd::SampleFormat::F64,
SampleFmt::I16 => audio_core_bsd::SampleFormat::I16,
SampleFmt::I32 => audio_core_bsd::SampleFormat::I32,
},
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SnapshotEdge {
pub from: (NodeId, PortIdx),
pub to: (NodeId, PortIdx),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct NodeSnapshot {
pub id: NodeId,
pub inputs: Vec<PortMeta>,
pub outputs: Vec<PortMeta>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub struct TopologySnapshot {
pub nodes: Vec<NodeSnapshot>,
pub edges: Vec<SnapshotEdge>,
}
impl TopologySnapshot {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn node(&self, id: NodeId) -> Option<&NodeSnapshot> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn apply(&mut self, mutation: &Mutation) {
match mutation {
Mutation::AddNode(ns) => {
if let Some(existing) = self.nodes.iter_mut().find(|n| n.id == ns.id) {
*existing = ns.clone();
} else {
self.nodes.push(ns.clone());
}
}
Mutation::RemoveNode(id) => {
self.nodes.retain(|n| n.id != *id);
self.edges.retain(|e| e.from.0 != *id && e.to.0 != *id);
}
Mutation::AddLink(se) => {
self.edges.push(*se);
}
Mutation::RemoveLink(link_id) => {
if *link_id < self.edges.len() {
self.edges.remove(*link_id);
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum TopologyEvent {
NodeAdded(NodeSnapshot),
NodeRemoved(NodeId),
LinkAdded(SnapshotEdge),
LinkRemoved(LinkId),
GraphCompiled,
GraphReset,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Mutation {
AddNode(NodeSnapshot),
RemoveNode(NodeId),
AddLink(SnapshotEdge),
RemoveLink(LinkId),
}
pub trait SnapshotSource {
#[must_use]
fn topology_snapshot(&self) -> TopologySnapshot;
}
pub trait TopologyObserver {
fn on_topology_event(&mut self, event: &TopologyEvent);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Graph, GraphConfig};
use audio_core_bsd::{
AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
};
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;
}
}
}
struct SourceNode {
out_p: [PortDescriptor; 1],
}
impl SourceNode {
fn new() -> Self {
Self {
out_p: [PortDescriptor::output(1, 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]) {
}
}
fn mono_out() -> PortMeta {
PortMeta {
direction: PortDir::Output,
channels: 1,
sample_format: SampleFmt::F32,
}
}
fn mono_in() -> PortMeta {
PortMeta {
direction: PortDir::Input,
channels: 1,
sample_format: SampleFmt::F32,
}
}
#[test]
fn portmeta_from_descriptor_roundtrips_all_variants() {
for dir in [PortDirection::Input, PortDirection::Output] {
for (channels, fmt) in [
(1_u16, SampleFormat::F32),
(2_u16, SampleFormat::F64),
(6_u16, SampleFormat::I16),
(8_u16, SampleFormat::I32),
] {
let d = PortDescriptor::new(dir, channels, fmt);
let meta = PortMeta::from_descriptor(d);
assert_eq!(meta.to_descriptor(), d, "roundtrip failed for {d:?}");
}
}
}
#[test]
fn snapshot_edge_is_copy_and_eq() {
let a = SnapshotEdge {
from: (0, 0),
to: (1, 0),
};
let b = a; assert_eq!(a, b);
let c = SnapshotEdge {
from: (0, 0),
to: (2, 0),
};
assert_ne!(a, c);
}
#[test]
fn apply_add_node_inserts_then_replaces() {
let mut snap = TopologySnapshot::new();
let ns = NodeSnapshot {
id: 3,
inputs: vec![mono_in()],
outputs: vec![mono_out()],
};
snap.apply(&Mutation::AddNode(ns.clone()));
assert_eq!(snap.node(3), Some(&ns));
let replaced = NodeSnapshot {
id: 3,
inputs: vec![],
outputs: vec![mono_out()],
};
snap.apply(&Mutation::AddNode(replaced.clone()));
assert_eq!(snap.nodes.len(), 1, "replace must not duplicate");
assert_eq!(snap.node(3), Some(&replaced));
}
#[test]
fn apply_add_and_remove_link_by_positional_id() {
let mut snap = TopologySnapshot::new();
let e0 = SnapshotEdge {
from: (0, 0),
to: (1, 0),
};
let e1 = SnapshotEdge {
from: (1, 0),
to: (2, 0),
};
snap.apply(&Mutation::AddLink(e0));
snap.apply(&Mutation::AddLink(e1));
assert_eq!(snap.edges, vec![e0, e1]);
snap.apply(&Mutation::RemoveLink(0));
assert_eq!(snap.edges, vec![e1]);
snap.apply(&Mutation::RemoveLink(99));
assert_eq!(snap.edges, vec![e1]);
}
#[test]
fn apply_remove_node_drops_incident_edges() {
let mut snap = TopologySnapshot::new();
snap.nodes.push(NodeSnapshot {
id: 0,
inputs: vec![],
outputs: vec![mono_out()],
});
snap.nodes.push(NodeSnapshot {
id: 1,
inputs: vec![mono_in()],
outputs: vec![mono_out()],
});
snap.edges.push(SnapshotEdge {
from: (0, 0),
to: (1, 0),
});
snap.apply(&Mutation::RemoveNode(0));
assert!(snap.node(0).is_none());
assert!(snap.edges.is_empty());
}
#[test]
fn graph_topology_snapshot_matches_structure() {
let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new()));
let gain = g.add_node(Box::new(GainNode::new(0.5)));
let _link = g.link((src, 0), (gain, 0)).unwrap();
let snap = SnapshotSource::topology_snapshot(&g);
assert_eq!(snap.nodes.len(), 2);
assert_eq!(snap.edges.len(), 1);
let src_ns = snap.node(src).unwrap();
assert!(src_ns.inputs.is_empty());
assert_eq!(src_ns.outputs.len(), 1);
assert_eq!(src_ns.outputs[0], mono_out());
let gain_ns = snap.node(gain).unwrap();
assert_eq!(gain_ns.inputs.len(), 1);
assert_eq!(gain_ns.inputs[0], mono_in());
assert_eq!(gain_ns.outputs.len(), 1);
assert_eq!(gain_ns.outputs[0], mono_out());
assert_eq!(
snap.edges[0],
SnapshotEdge {
from: (src, 0),
to: (gain, 0)
}
);
}
#[test]
fn from_snapshot_rebuilds_compilable_graph() {
let mut orig = Graph::new();
let src = orig.add_node(Box::new(SourceNode::new()));
let gain = orig.add_node(Box::new(GainNode::new(0.25)));
orig.link((src, 0), (gain, 0)).unwrap();
let snap = SnapshotSource::topology_snapshot(&orig);
let mut rebuilt = Graph::from_snapshot(&snap, &mut |id| {
if id == src {
Some(Box::new(SourceNode::new()) as Box<dyn AudioNode>)
} else if id == gain {
Some(Box::new(GainNode::new(0.25)) as Box<dyn AudioNode>)
} else {
None
}
})
.expect("factory supplies all nodes");
assert_eq!(rebuilt.node_count(), 2);
assert_eq!(rebuilt.link_count(), 1);
rebuilt
.compile(GraphConfig::new(8, 48_000, 1))
.expect("rebuilt graph compiles");
}
#[test]
fn from_snapshot_errors_when_factory_returns_none() {
let snap = TopologySnapshot {
nodes: vec![NodeSnapshot {
id: 7,
inputs: vec![],
outputs: vec![mono_out()],
}],
edges: vec![],
};
let result = Graph::from_snapshot(&snap, &mut |_| None);
assert!(result.is_err());
}
}