#![cfg(feature = "distributed")]
use crate::error::GraphError;
use crate::graph::{Graph, GraphConfig, NodeId, PortIdx};
use crate::topology_pub::PortDir;
use audio_core_bsd::{AudioNode, ProcessContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum TransportHint {
Rtp,
Aes67,
Custom,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RemoteNode {
pub node_id: NodeId,
pub host: String,
pub port: u16,
pub transport_hint: TransportHint,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PortKind {
Local,
Network {
remote: RemoteNode,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct BoundaryPort {
pub node: NodeId,
pub port: PortIdx,
pub kind: PortKind,
pub direction: PortDir,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PartitionHint {
pub local_nodes: Vec<NodeId>,
pub boundary_ports: Vec<BoundaryPort>,
}
pub struct GraphPartition {
graph: Graph,
boundary: Vec<BoundaryPort>,
}
impl GraphPartition {
#[must_use]
pub fn new(graph: Graph, boundary: Vec<BoundaryPort>) -> Self {
Self { graph, boundary }
}
pub fn compile(&mut self, config: GraphConfig) -> Result<(), GraphError> {
self.graph.compile(config)
}
pub fn process_cycle(&mut self, ctx: &mut ProcessContext) -> Result<(), GraphError> {
self.graph.process_cycle(ctx)
}
#[must_use]
pub fn boundary(&self) -> &[BoundaryPort] {
&self.boundary
}
#[must_use]
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn graph_mut(&mut self) -> &mut Graph {
&mut self.graph
}
}
pub trait NetworkLinkNode: AudioNode {
#[must_use]
fn remote(&self) -> &RemoteNode;
}
#[cfg(test)]
mod tests {
use super::*;
use audio_core_bsd::{
AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
};
struct PassthroughNode {
gain: f32,
in_port: [PortDescriptor; 1],
out_port: [PortDescriptor; 1],
}
impl PassthroughNode {
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 PassthroughNode {
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;
}
}
}
#[test]
fn remote_node_serde_roundtrip() {
let node = RemoteNode {
node_id: 7,
host: "192.168.1.42".to_string(),
port: 5004,
transport_hint: TransportHint::Rtp,
};
let bytes = bincode::serialize(&node).expect("serialize");
let back: RemoteNode = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(node, back);
}
#[test]
fn transport_hint_serde_roundtrip() {
for hint in [
TransportHint::Rtp,
TransportHint::Aes67,
TransportHint::Custom,
] {
let bytes = bincode::serialize(&hint).expect("serialize");
let back: TransportHint = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(hint, back);
}
}
#[test]
fn port_kind_local_vs_network() {
let local = PortKind::Local;
let remote = RemoteNode {
node_id: 1,
host: "host".to_string(),
port: 10,
transport_hint: TransportHint::Aes67,
};
let net = PortKind::Network {
remote: remote.clone(),
};
assert_eq!(local, PortKind::Local);
let PortKind::Network { remote: r } = &net else {
panic!("expected Network");
};
assert_eq!(r, &remote);
}
#[test]
fn partition_hint_construction() {
let bp = BoundaryPort {
node: 0,
port: 0,
kind: PortKind::Network {
remote: RemoteNode {
node_id: 9,
host: "h".to_string(),
port: 1,
transport_hint: TransportHint::Custom,
},
},
direction: PortDir::Output,
};
let hint = PartitionHint {
local_nodes: vec![0, 1],
boundary_ports: vec![bp.clone()],
};
assert_eq!(hint.local_nodes, vec![0, 1]);
assert_eq!(hint.boundary_ports, vec![bp]);
}
#[test]
fn graph_partition_delegates_compile_and_process() {
let mut g = Graph::new();
let a = g.add_node(Box::new(PassthroughNode::new(1.0)));
let b = g.add_node(Box::new(PassthroughNode::new(1.0)));
g.link((a, 0), (b, 0)).unwrap();
let boundary = vec![BoundaryPort {
node: b,
port: 0,
kind: PortKind::Network {
remote: RemoteNode {
node_id: 100,
host: "remote".to_string(),
port: 5004,
transport_hint: TransportHint::Rtp,
},
},
direction: PortDir::Output,
}];
let mut part = GraphPartition::new(g, boundary);
part.compile(GraphConfig::new(8, 48_000, 1)).unwrap();
let mut ctx = ProcessContext::new(8, 0, 48_000);
part.process_cycle(&mut ctx).unwrap();
assert_eq!(part.boundary().len(), 1);
assert_eq!(part.boundary()[0].node, b);
assert_eq!(part.graph().node_count(), 2);
assert_eq!(part.graph().link_count(), 1);
}
#[test]
fn partition_hints_returns_one_hint_with_boundary() {
let mut g = Graph::new();
let a = g.add_node(Box::new(PassthroughNode::new(1.0)));
let remote = RemoteNode {
node_id: 5,
host: "far".to_string(),
port: 6004,
transport_hint: TransportHint::Aes67,
};
let hints = g.partition_hints(&[(a, 0, remote.clone())]);
assert_eq!(hints.len(), 1);
let hint = &hints[0];
assert_eq!(hint.local_nodes, vec![a]);
assert_eq!(hint.boundary_ports.len(), 1);
let bp = &hint.boundary_ports[0];
assert_eq!(bp.node, a);
assert_eq!(bp.port, 0);
assert_eq!(bp.direction, PortDir::Output);
let PortKind::Network { remote: r } = &bp.kind else {
panic!("expected Network");
};
assert_eq!(r, &remote);
}
#[test]
fn partition_hints_skips_out_of_range_port() {
let mut g = Graph::new();
let a = g.add_node(Box::new(PassthroughNode::new(1.0)));
let remote = RemoteNode {
node_id: 5,
host: "far".to_string(),
port: 6004,
transport_hint: TransportHint::Rtp,
};
let hints = g.partition_hints(&[(a, 99, remote)]);
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].local_nodes, vec![a]);
assert!(hints[0].boundary_ports.is_empty());
}
}