Skip to main content

audio_graph_bsd/
lib.rs

1//! Real-time-safe directed node-graph audio processing engine for Rust.
2//!
3//! `audio-graph` schedules and drives a directed acyclic graph (DAG) of
4//! [`AudioNode`][audio_core_bsd::AudioNode]s on a dedicated real-time (RT) audio
5//! thread. The engine performs topological sorting at compile time, pre-allocates
6//! every scratch buffer, and then runs [`Graph::process_cycle`] over the
7//! pre-scheduled order without ever allocating, locking, or panicking.
8//!
9//! # Architecture
10//!
11//! 1. **Build phase** (non-RT): the caller adds nodes with [`Graph::add_node`]
12//!    and wires output ports to input ports with [`Graph::link`].
13//! 2. **Compile phase** (non-RT): [`Graph::compile`] runs Kahn's topological
14//!    sort, rejects cycles, fixes the execution order, and pre-allocates a
15//!    per-port scratch [`AudioFrame`][audio_core_bsd::AudioFrame] for every node.
16//! 3. **RT phase**: [`Graph::process_cycle`] is invoked once per audio cycle
17//!    on the RT thread. It copies upstream outputs into downstream inputs
18//!    (bounded, alloc-free) and calls each node's `process` in dependency order.
19//!
20//! # Real-time safety boundary — CRITICAL
21//!
22//! [`Graph::process_cycle`] is the RT entry point. It **MUST NOT** allocate,
23//! lock, panic, or perform any system call. This is achieved by:
24//!
25//! - Running the topological sort in [`Graph::compile`] (never in `process_cycle`).
26//! - Pre-allocating **all** scratch frames in `compile()` so `process_cycle`
27//!   never grows a `Vec`.
28//! - Using only bounded `for` loops and slice indexing over pre-sized buffers.
29//!
30//! Conformance can be verified at test time with a counting allocator that
31//! fails on any allocation observed inside `process_cycle`.
32//!
33//! # Example
34//!
35//! A minimal two-node graph (source → gain) driven for one cycle. The source
36//! is a no-op node whose output is seeded via [`Graph::feed`]; the gain node
37//! scales it by 0.5:
38//!
39//! ```
40//! use audio_core_bsd::{
41//!     AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
42//! };
43//! use audio_graph_bsd::{Graph, GraphConfig};
44//!
45//! /// A source node with one output and a no-op `process`: its output scratch
46//! /// is seeded externally via `Graph::feed` and left untouched each cycle.
47//! struct SourceNode {
48//!     out_p: [PortDescriptor; 1],
49//! }
50//! impl SourceNode {
51//!     fn new() -> Self {
52//!         Self {
53//!             out_p: [PortDescriptor::output(1, SampleFormat::F32)],
54//!         }
55//!     }
56//! }
57//! impl AudioNode for SourceNode {
58//!     fn inputs(&self) -> &[PortDescriptor] { &[] }
59//!     fn outputs(&self) -> &[PortDescriptor] { &self.out_p }
60//!     fn process(&mut self, _ctx: &mut ProcessContext, _i: &[AudioFrame], _o: &mut [AudioFrame]) {}
61//! }
62//!
63//! /// Scales its single mono input by a fixed gain.
64//! struct GainNode {
65//!     gain: f32,
66//!     in_p: [PortDescriptor; 1],
67//!     out_p: [PortDescriptor; 1],
68//! }
69//! impl GainNode {
70//!     fn new(gain: f32) -> Self {
71//!         Self {
72//!             gain,
73//!             in_p: [PortDescriptor::input(1, SampleFormat::F32)],
74//!             out_p: [PortDescriptor::output(1, SampleFormat::F32)],
75//!         }
76//!     }
77//! }
78//! impl AudioNode for GainNode {
79//!     fn inputs(&self) -> &[PortDescriptor] { &self.in_p }
80//!     fn outputs(&self) -> &[PortDescriptor] { &self.out_p }
81//!     fn process(&mut self, _ctx: &mut ProcessContext, i: &[AudioFrame], o: &mut [AudioFrame]) {
82//!         let (Some(inp), Some(out)) = (i.first(), o.get_mut(0)) else { return };
83//!         let n = inp.samples.len().min(out.samples.len());
84//!         for k in 0..n {
85//!             out.samples[k] = inp.samples[k] * self.gain;
86//!         }
87//!     }
88//! }
89//!
90//! let mut g = Graph::new();
91//! let src = g.add_node(Box::new(SourceNode::new()));
92//! let dst = g.add_node(Box::new(GainNode::new(0.5)));
93//! g.link((src, 0), (dst, 0)).unwrap();
94//! g.compile(GraphConfig::new(8, 48_000, 1)).unwrap();
95//!
96//! // Seed the source output with all-ones (survives process_cycle because
97//! // SourceNode::process is a no-op), run one cycle.
98//! g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 8]));
99//! let mut ctx = ProcessContext::new(8, 0, 48_000);
100//! g.process_cycle(&mut ctx).unwrap();
101//! assert!(g
102//!     .read_output(dst, 0)
103//!     .unwrap()
104//!     .samples
105//!     .iter()
106//!     .all(|&x| (x - 0.5).abs() < 1e-6));
107//! ```
108
109#![cfg_attr(docsrs, feature(doc_cfg))]
110#![warn(missing_docs)]
111#![warn(clippy::all, clippy::pedantic)]
112
113mod error;
114mod graph;
115mod ring;
116mod topology;
117
118// The serializable topology model — only present under the `topology` feature.
119// When the feature is off the module, the serde dependency, and every type
120// below are entirely absent, preserving the 0.1.0 serde-free contract.
121#[cfg(feature = "topology")]
122mod topology_pub;
123
124// The distributed-prep abstractions — only present under the `distributed`
125// feature (which implies `topology`). Provides serializable partition models,
126// the network-link node contract, and the partition-hint generator. No sockets,
127// Raft, or netmap — those live in sonicbrew (M07/M09).
128#[cfg(feature = "distributed")]
129mod distributed;
130
131// Hot-reload handle (Phase C, ROADMAP §5 strategy B): atomic snapshot swap of a
132// compiled Graph via `arc-swap`. The control thread builds a new Graph and
133// publishes it; the RT thread does a wait-free load + alloc-free process_cycle.
134#[cfg(feature = "distributed")]
135mod hot_reload;
136
137pub use error::GraphError;
138pub use graph::{Graph, GraphConfig, LinkId, NodeId, PortIdx};
139pub use ring::{RingSink, RingSource};
140
141#[cfg(feature = "topology")]
142pub use topology_pub::{
143    Mutation, NodeSnapshot, PortDir, PortMeta, SampleFmt, SnapshotEdge, SnapshotSource,
144    TopologyEvent, TopologyObserver, TopologySnapshot,
145};
146
147#[cfg(feature = "distributed")]
148pub use distributed::{
149    BoundaryPort, GraphPartition, NetworkLinkNode, PartitionHint, PortKind, RemoteNode,
150    TransportHint,
151};
152
153#[cfg(feature = "distributed")]
154pub use hot_reload::RtHandle;