Skip to main content

sim_lib_audio_graph_core/
graph.rs

1use std::collections::{BTreeMap, VecDeque};
2
3use sim_kernel::{Error, Result};
4
5use crate::{
6    BlockArena, NullEventSink, Patch, PatchNode, PortDecl, PortDir, PortUri, PrepareConfig,
7    ProcessBlock, Processor, ProcessorDescriptor, RateContract, Transport,
8};
9
10/// A directed connection from one node's output port to another's input port.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Cable {
13    /// Source port URI (an output port).
14    pub from: PortUri,
15    /// Destination port URI (an input port).
16    pub to: PortUri,
17}
18
19impl Cable {
20    /// Creates a cable from a source port to a destination port.
21    pub fn new(from: PortUri, to: PortUri) -> Self {
22        Self { from, to }
23    }
24}
25
26/// A directed audio processor graph: nodes plus the cables between their ports.
27///
28/// Build a graph by adding nodes and connecting ports, [`prepare`](Graph::prepare)
29/// it with a sample rate and block size, then render deterministic blocks with
30/// [`process_offline`](Graph::process_offline). Nodes are processed in
31/// topological order; cycles are rejected at connect time.
32pub struct Graph {
33    nodes: BTreeMap<String, GraphNode>,
34    cables: Vec<Cable>,
35    order: Vec<String>,
36    prepared: Option<PreparedGraph>,
37    arena: BlockArena,
38}
39
40struct GraphNode {
41    processor: Box<dyn Processor>,
42    in_channels: u16,
43    out_channels: u16,
44    descriptor: ProcessorDescriptor,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48struct PreparedGraph {
49    max_block_frames: u32,
50}
51
52impl Default for Graph {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl Graph {
59    /// Creates an empty graph.
60    pub fn new() -> Self {
61        Self {
62            nodes: BTreeMap::new(),
63            cables: Vec::new(),
64            order: Vec::new(),
65            prepared: None,
66            arena: BlockArena::empty(),
67        }
68    }
69
70    /// Adds a processor node with the given id and channel counts.
71    ///
72    /// Fails if the id is empty or already present. Adding a node clears any
73    /// prepared state, requiring a fresh [`prepare`](Graph::prepare).
74    pub fn add_node(
75        &mut self,
76        id: impl Into<String>,
77        processor: Box<dyn Processor>,
78        in_channels: u16,
79        out_channels: u16,
80    ) -> Result<()> {
81        let id = id.into();
82        if id.is_empty() {
83            return Err(Error::Eval(
84                "audio graph node id cannot be empty".to_owned(),
85            ));
86        }
87        if self.nodes.contains_key(&id) {
88            return Err(Error::Eval(format!("duplicate audio graph node: {id}")));
89        }
90        let descriptor = processor.descriptor(in_channels, out_channels);
91        self.nodes.insert(
92            id,
93            GraphNode {
94                processor,
95                in_channels,
96                out_channels,
97                descriptor,
98            },
99        );
100        self.order.clear();
101        self.prepared = None;
102        Ok(())
103    }
104
105    /// Connects an output port to an input port.
106    ///
107    /// Validates both endpoints and their rate contracts, and rejects the cable
108    /// if it would introduce a cycle.
109    pub fn connect(&mut self, from: PortUri, to: PortUri) -> Result<()> {
110        let source_rate = self.validate_endpoint(&from, PortDirection::Out)?;
111        let target_rate = self.validate_endpoint(&to, PortDirection::In)?;
112        source_rate.ensure_compatible(target_rate)?;
113        self.cables.push(Cable::new(from, to));
114        match self.topological_order() {
115            Ok(order) => {
116                self.order = order;
117                self.prepared = None;
118                Ok(())
119            }
120            Err(error) => {
121                self.cables.pop();
122                Err(error)
123            }
124        }
125    }
126
127    /// Prepares every node for the given sample rate and maximum block size,
128    /// sizing the scratch arena. Fails on a zero rate/size or a graph cycle.
129    pub fn prepare(&mut self, sample_rate_hz: u32, max_block_frames: u32) -> Result<()> {
130        if sample_rate_hz == 0 {
131            return Err(Error::Eval("sample rate must be nonzero".to_owned()));
132        }
133        if max_block_frames == 0 {
134            return Err(Error::Eval("max block frames must be nonzero".to_owned()));
135        }
136        let order = self.topological_order()?;
137        let mut max_channels = 1usize;
138        for id in &order {
139            let node = self
140                .nodes
141                .get_mut(id)
142                .ok_or_else(|| Error::Eval(format!("missing audio graph node: {id}")))?;
143            max_channels = max_channels.max(usize::from(node.in_channels));
144            max_channels = max_channels.max(usize::from(node.out_channels));
145            node.processor.prepare(PrepareConfig::new(
146                sample_rate_hz,
147                max_block_frames,
148                node.in_channels,
149                node.out_channels,
150            ));
151        }
152        self.order = order;
153        self.prepared = Some(PreparedGraph { max_block_frames });
154        self.arena = BlockArena::with_f32_capacity(max_block_frames as usize * max_channels);
155        Ok(())
156    }
157
158    /// Renders one block offline, feeding `input` lanes to the source nodes and
159    /// returning the output node's channel buffers.
160    ///
161    /// Requires a prepared graph and `frames` no larger than the prepared block
162    /// size; each input lane must hold at least `frames` samples.
163    pub fn process_offline(&mut self, input: &[Vec<f32>], frames: u32) -> Result<Vec<Vec<f32>>> {
164        let prepared = self.prepared.ok_or_else(|| {
165            Error::Eval("audio graph must be prepared before processing".to_owned())
166        })?;
167        if frames > prepared.max_block_frames {
168            return Err(Error::Eval(format!(
169                "process block has {frames} frames, max prepared block is {}",
170                prepared.max_block_frames
171            )));
172        }
173        if self.order.is_empty() {
174            return Err(Error::Eval("audio graph has no nodes".to_owned()));
175        }
176        let frames_len = frames as usize;
177        for (index, lane) in input.iter().enumerate() {
178            if lane.len() < frames_len {
179                return Err(Error::Eval(format!(
180                    "input audio lane {index} has {} frames, expected at least {frames_len}",
181                    lane.len()
182                )));
183            }
184        }
185
186        let mut node_outputs = BTreeMap::<String, Vec<Vec<f32>>>::new();
187        let order = self.order.clone();
188        for id in &order {
189            let (in_channels, out_channels) = self.node_channel_counts(id)?;
190            let incoming = self.incoming_edges(id)?;
191            let mut in_buffers = vec![vec![0.0; frames_len]; usize::from(in_channels)];
192            if incoming.is_empty() {
193                for (channel, buffer) in in_buffers.iter_mut().enumerate() {
194                    if let Some(source) = input.get(channel) {
195                        buffer.copy_from_slice(&source[..frames_len]);
196                    }
197                }
198            } else {
199                for edge in incoming {
200                    let source_outputs = node_outputs.get(&edge.source_node).ok_or_else(|| {
201                        Error::Eval(format!(
202                            "missing processed output for node {}",
203                            edge.source_node
204                        ))
205                    })?;
206                    let source = source_outputs.get(edge.source_index).ok_or_else(|| {
207                        Error::Eval(format!(
208                            "source output channel {} is out of range for node {}",
209                            edge.source_index, edge.source_node
210                        ))
211                    })?;
212                    let target = in_buffers.get_mut(edge.target_index).ok_or_else(|| {
213                        Error::Eval(format!(
214                            "target input channel {} is out of range for node {id}",
215                            edge.target_index
216                        ))
217                    })?;
218                    target.copy_from_slice(&source[..frames_len]);
219                }
220            }
221
222            let mut out_buffers = vec![vec![0.0; frames_len]; usize::from(out_channels)];
223            {
224                let in_audio = in_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
225                let mut out_audio = out_buffers
226                    .iter_mut()
227                    .map(Vec::as_mut_slice)
228                    .collect::<Vec<_>>();
229                let mut out_events = NullEventSink;
230                let in_events = [];
231                self.arena.reset();
232                let mut block = ProcessBlock {
233                    frames,
234                    in_audio: in_audio.as_slice(),
235                    out_audio: out_audio.as_mut_slice(),
236                    in_events: &in_events,
237                    out_events: &mut out_events,
238                    transport: Transport::default(),
239                    scratch: &mut self.arena,
240                };
241                block.validate_audio_lanes()?;
242                let processor = self
243                    .nodes
244                    .get_mut(id)
245                    .ok_or_else(|| Error::Eval(format!("missing audio graph node: {id}")))?;
246                processor.processor.process(&mut block);
247            }
248            node_outputs.insert(id.clone(), out_buffers);
249        }
250
251        let output_node = order
252            .last()
253            .ok_or_else(|| Error::Eval("audio graph has no output node".to_owned()))?;
254        node_outputs
255            .remove(output_node)
256            .ok_or_else(|| Error::Eval(format!("missing graph output for node {output_node}")))
257    }
258
259    /// Returns the graph as a portable [`Patch`] of nodes and cables.
260    pub fn to_patch(&self) -> Patch {
261        Patch {
262            nodes: self.patch_nodes(),
263            cables: self.cables.clone(),
264        }
265    }
266
267    /// Returns the graph's cables.
268    pub fn cables(&self) -> &[Cable] {
269        &self.cables
270    }
271
272    /// Returns the graph's nodes as portable [`PatchNode`] records.
273    pub fn patch_nodes(&self) -> Vec<PatchNode> {
274        self.nodes
275            .iter()
276            .map(|(id, node)| PatchNode {
277                id: id.clone(),
278                in_channels: node.in_channels,
279                out_channels: node.out_channels,
280            })
281            .collect()
282    }
283
284    /// Returns the node ids in a stable topological order, failing on a cycle.
285    pub fn topological_node_order(&self) -> Result<Vec<String>> {
286        self.topological_order()
287    }
288
289    /// Returns the processor descriptor for a node, or an error if unknown.
290    pub fn node_descriptor(&self, id: &str) -> Result<&ProcessorDescriptor> {
291        self.nodes
292            .get(id)
293            .map(|node| &node.descriptor)
294            .ok_or_else(|| Error::Eval(format!("unknown audio graph node: {id}")))
295    }
296
297    fn validate_endpoint(&self, uri: &PortUri, direction: PortDirection) -> Result<RateContract> {
298        let node_id = uri.node_id().ok_or_else(|| {
299            Error::Eval(format!("port URI does not reference a graph node: {uri}"))
300        })?;
301        let node = self
302            .nodes
303            .get(node_id)
304            .ok_or_else(|| Error::Eval(format!("unknown audio graph node: {node_id}")))?;
305        let port = descriptor_port(&node.descriptor, uri, direction)?;
306        if uri.index >= u32::from(port.channels) {
307            return Err(Error::Eval(format!(
308                "port index {} is out of range for node {node_id}",
309                uri.index
310            )));
311        }
312        Ok(port.rate_contract)
313    }
314
315    fn node_channel_counts(&self, id: &str) -> Result<(u16, u16)> {
316        self.nodes
317            .get(id)
318            .map(|node| (node.in_channels, node.out_channels))
319            .ok_or_else(|| Error::Eval(format!("missing audio graph node: {id}")))
320    }
321
322    fn incoming_edges(&self, node_id: &str) -> Result<Vec<Edge>> {
323        self.cables
324            .iter()
325            .filter(|cable| cable.to.node_id() == Some(node_id))
326            .map(|cable| {
327                Ok(Edge {
328                    source_node: cable_source_node(cable)?,
329                    source_index: cable.from.index as usize,
330                    target_index: cable.to.index as usize,
331                })
332            })
333            .collect()
334    }
335
336    fn topological_order(&self) -> Result<Vec<String>> {
337        let mut indegree = self
338            .nodes
339            .keys()
340            .map(|id| (id.clone(), 0usize))
341            .collect::<BTreeMap<_, _>>();
342        let mut outgoing = self
343            .nodes
344            .keys()
345            .map(|id| (id.clone(), Vec::<String>::new()))
346            .collect::<BTreeMap<_, _>>();
347
348        for cable in &self.cables {
349            let source = cable_source_node(cable)?;
350            let target = cable_target_node(cable)?;
351            if !self.nodes.contains_key(&source) {
352                return Err(Error::Eval(format!("unknown audio graph node: {source}")));
353            }
354            if !self.nodes.contains_key(&target) {
355                return Err(Error::Eval(format!("unknown audio graph node: {target}")));
356            }
357            *indegree
358                .get_mut(&target)
359                .ok_or_else(|| Error::Eval(format!("missing node indegree: {target}")))? += 1;
360            outgoing
361                .get_mut(&source)
362                .ok_or_else(|| Error::Eval(format!("missing node outputs: {source}")))?
363                .push(target);
364        }
365        for targets in outgoing.values_mut() {
366            targets.sort();
367        }
368
369        let mut ready = indegree
370            .iter()
371            .filter_map(|(id, count)| (*count == 0).then_some(id.clone()))
372            .collect::<VecDeque<_>>();
373        let mut order = Vec::with_capacity(self.nodes.len());
374        while let Some(id) = ready.pop_front() {
375            order.push(id.clone());
376            let targets = outgoing
377                .get(&id)
378                .ok_or_else(|| Error::Eval(format!("missing outgoing list for node {id}")))?;
379            for target in targets {
380                let count = indegree
381                    .get_mut(target)
382                    .ok_or_else(|| Error::Eval(format!("missing indegree for node {target}")))?;
383                *count = count.saturating_sub(1);
384                if *count == 0 {
385                    ready.push_back(target.clone());
386                }
387            }
388        }
389        if order.len() != self.nodes.len() {
390            return Err(Error::Eval("audio graph contains a cycle".to_owned()));
391        }
392        Ok(order)
393    }
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
397enum PortDirection {
398    In,
399    Out,
400}
401
402impl PortDirection {
403    fn port_dir(self) -> PortDir {
404        match self {
405            Self::In => PortDir::In,
406            Self::Out => PortDir::Out,
407        }
408    }
409}
410
411fn descriptor_port<'a>(
412    descriptor: &'a ProcessorDescriptor,
413    uri: &PortUri,
414    direction: PortDirection,
415) -> Result<&'a PortDecl> {
416    let port_name = uri
417        .node_port_name()
418        .ok_or_else(|| Error::Eval(format!("port URI does not name a graph node port: {uri}")))?;
419    let direction_ports = descriptor
420        .ports()
421        .iter()
422        .filter(|port| port.dir == direction.port_dir())
423        .collect::<Vec<_>>();
424    if let Some(port) = direction_ports
425        .iter()
426        .copied()
427        .find(|port| port.name == port_name)
428    {
429        return Ok(port);
430    }
431    if direction_ports.len() == 1 {
432        return Ok(direction_ports[0]);
433    }
434    Err(Error::Eval(format!(
435        "unknown audio graph node port {port_name}"
436    )))
437}
438
439#[derive(Clone, Debug, PartialEq, Eq)]
440struct Edge {
441    source_node: String,
442    source_index: usize,
443    target_index: usize,
444}
445
446fn cable_source_node(cable: &Cable) -> Result<String> {
447    cable
448        .from
449        .node_id()
450        .map(str::to_owned)
451        .ok_or_else(|| Error::Eval(format!("invalid source port URI: {}", cable.from)))
452}
453
454fn cable_target_node(cable: &Cable) -> Result<String> {
455    cable
456        .to
457        .node_id()
458        .map(str::to_owned)
459        .ok_or_else(|| Error::Eval(format!("invalid target port URI: {}", cable.to)))
460}