Skip to main content

servo_media_audio/
graph.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{RefCell, RefMut};
6use std::{cmp, fmt, hash};
7
8use malloc_size_of::MallocSizeOf as MallocSizeOfTrait;
9use malloc_size_of_derive::MallocSizeOf;
10use petgraph::Direction;
11use petgraph::algo::tarjan_scc;
12use petgraph::graph::DefaultIx;
13use petgraph::stable_graph::{NodeIndex, StableGraph};
14use petgraph::visit::{DfsPostOrder, EdgeRef, Reversed};
15use rustc_hash::FxHashSet;
16use smallvec::SmallVec;
17
18use crate::block::{Block, Chunk};
19use crate::destination_node::DestinationNode;
20use crate::listener::AudioListenerNode;
21use crate::node::{AudioNodeEngine, BlockInfo, ChannelCountMode, ChannelInterpretation};
22use crate::param::ParamType;
23
24#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Debug, MallocSizeOf)]
25/// A unique identifier for nodes in the graph. Stable
26/// under graph mutation.
27pub struct NodeId(#[ignore_malloc_size_of = "External Type"] NodeIndex<DefaultIx>);
28
29impl NodeId {
30    pub fn input(self, port: u32) -> PortId<InputPort> {
31        PortId(self, PortIndex::Port(port))
32    }
33    pub fn param(self, param: ParamType) -> PortId<InputPort> {
34        PortId(self, PortIndex::Param(param))
35    }
36    pub fn output(self, port: u32) -> PortId<OutputPort> {
37        PortId(self, PortIndex::Port(port))
38    }
39    pub(crate) fn listener(self) -> PortId<InputPort> {
40        PortId(self, PortIndex::Listener(()))
41    }
42}
43
44/// A zero-indexed "port" for a node. Most nodes have one
45/// input and one output port, but some may have more.
46/// For example, a channel splitter node will have one output
47/// port for each channel.
48///
49/// These are essentially indices into the Chunks
50///
51/// Kind is a zero sized type and is useful for distinguishing
52/// between input and output ports (which may otherwise share indices)
53#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Debug, MallocSizeOf)]
54pub enum PortIndex<Kind: PortKind> {
55    Port(u32),
56    Param(Kind::ParamId),
57    /// special variant only used for the implicit connection
58    /// from listeners to params
59    Listener(Kind::Listener),
60}
61
62impl<Kind: PortKind> PortId<Kind> {
63    pub fn node(&self) -> NodeId {
64        self.0
65    }
66}
67
68pub trait PortKind {
69    type ParamId: Copy
70        + Eq
71        + PartialEq
72        + Ord
73        + PartialOrd
74        + hash::Hash
75        + fmt::Debug
76        + MallocSizeOfTrait;
77    type Listener: Copy
78        + Eq
79        + PartialEq
80        + Ord
81        + PartialOrd
82        + hash::Hash
83        + fmt::Debug
84        + MallocSizeOfTrait;
85}
86
87/// An identifier for a port.
88#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Debug, MallocSizeOf)]
89pub struct PortId<Kind: PortKind>(NodeId, PortIndex<Kind>);
90
91#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, MallocSizeOf)]
92/// Marker type for denoting that the port is an input port
93/// of the node it is connected to
94pub struct InputPort;
95#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, MallocSizeOf)]
96/// Marker type for denoting that the port is an output port
97/// of the node it is connected to
98pub struct OutputPort;
99
100impl PortKind for InputPort {
101    type ParamId = ParamType;
102    type Listener = ();
103}
104
105#[derive(Debug, Hash, PartialOrd, Ord, PartialEq, Eq, Copy, Clone, MallocSizeOf)]
106pub enum Void {}
107
108impl PortKind for OutputPort {
109    // Params are only a feature of input ports. By using an empty type here
110    // we ensure that the PortIndex enum has zero overhead for outputs,
111    // taking up no extra discriminant space and eliminating PortIndex::Param
112    // branches entirely from the compiled code
113    type ParamId = Void;
114    type Listener = Void;
115}
116
117pub struct AudioGraph {
118    graph: StableGraph<Node, Edge>,
119    dest_id: NodeId,
120    dests: Vec<NodeId>,
121    listener_id: NodeId,
122}
123
124pub(crate) struct Node {
125    node: RefCell<Box<dyn AudioNodeEngine>>,
126}
127
128/// An edge in the graph
129///
130/// This connects one or more pair of ports between two
131/// nodes, each connection represented by a `Connection`.
132/// WebAudio allows for multiple connections to/from the same port
133/// however it does not allow for duplicate connections between pairs
134/// of ports
135pub(crate) struct Edge {
136    connections: SmallVec<[Connection; 1]>,
137}
138
139impl Edge {
140    /// Find if there are connections between two given ports, return the index
141    fn has_between(
142        &self,
143        output_idx: PortIndex<OutputPort>,
144        input_idx: PortIndex<InputPort>,
145    ) -> bool {
146        self.connections
147            .iter()
148            .any(|e| e.input_idx == input_idx && e.output_idx == output_idx)
149    }
150
151    fn remove_by_output(&mut self, output_idx: PortIndex<OutputPort>) {
152        self.connections.retain(|i| i.output_idx != output_idx)
153    }
154
155    fn remove_by_input(&mut self, input_idx: PortIndex<InputPort>) {
156        self.connections.retain(|i| i.input_idx != input_idx)
157    }
158
159    fn remove_by_pair(
160        &mut self,
161        output_idx: PortIndex<OutputPort>,
162        input_idx: PortIndex<InputPort>,
163    ) {
164        self.connections
165            .retain(|i| i.output_idx != output_idx || i.input_idx != input_idx)
166    }
167}
168
169/// A single connection between ports
170struct Connection {
171    /// The index of the port on the input node
172    /// This is actually the /output/ of this edge
173    input_idx: PortIndex<InputPort>,
174    /// The index of the port on the output node
175    /// This is actually the /input/ of this edge
176    output_idx: PortIndex<OutputPort>,
177    /// When the from node finishes processing, it will push
178    /// its data into this cache for the input node to read
179    cache: RefCell<Option<Block>>,
180}
181
182impl AudioGraph {
183    pub fn new(channel_count: u8) -> Self {
184        let mut graph = StableGraph::new();
185        let dest_id =
186            NodeId(graph.add_node(Node::new(Box::new(DestinationNode::new(channel_count)))));
187        let listener_id = NodeId(graph.add_node(Node::new(Box::new(AudioListenerNode::new()))));
188        AudioGraph {
189            graph,
190            dest_id,
191            dests: vec![dest_id],
192            listener_id,
193        }
194    }
195
196    /// Create a node, obtain its id
197    pub(crate) fn add_node(&mut self, node: Box<dyn AudioNodeEngine>) -> NodeId {
198        NodeId(self.graph.add_node(Node::new(node)))
199    }
200
201    /// Connect an output port to an input port
202    ///
203    /// The edge goes *from* the output port *to* the input port, connecting two nodes
204    pub fn add_edge(&mut self, out: PortId<OutputPort>, inp: PortId<InputPort>) {
205        let edge = self
206            .graph
207            .edges(out.node().0)
208            .find(|e| e.target() == inp.node().0)
209            .map(|e| e.id());
210        if let Some(e) = edge {
211            // .find(|e| e.weight().has_between(out.1, inp.1));
212            let w = self
213                .graph
214                .edge_weight_mut(e)
215                .expect("This edge is known to exist");
216            if w.has_between(out.1, inp.1) {
217                return;
218            }
219            w.connections.push(Connection::new(inp.1, out.1))
220        } else {
221            // add a new edge
222            self.graph
223                .add_edge(out.node().0, inp.node().0, Edge::new(inp.1, out.1));
224        }
225    }
226
227    /// Disconnect all outgoing connections from a node
228    ///
229    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect>
230    pub fn disconnect_all_from(&mut self, node: NodeId) {
231        let edges = self.graph.edges(node.0).map(|e| e.id()).collect::<Vec<_>>();
232        for edge in edges {
233            self.graph.remove_edge(edge);
234        }
235    }
236
237    /// Disconnect all outgoing connections from a node's output
238    ///
239    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-output>
240    pub fn disconnect_output(&mut self, out: PortId<OutputPort>) {
241        let candidates: Vec<_> = self
242            .graph
243            .edges(out.node().0)
244            .map(|e| (e.id(), e.target()))
245            .collect();
246        for (edge, to) in candidates {
247            let mut e = self
248                .graph
249                .remove_edge(edge)
250                .expect("Edge index is known to exist");
251            e.remove_by_output(out.1);
252            if !e.connections.is_empty() {
253                self.graph.add_edge(out.node().0, to, e);
254            }
255        }
256    }
257
258    /// Disconnect connections from a node to another node
259    ///
260    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode>
261    pub fn disconnect_between(&mut self, from: NodeId, to: NodeId) {
262        let edge = self
263            .graph
264            .edges(from.0)
265            .find(|e| e.target() == to.0)
266            .map(|e| e.id());
267        if let Some(i) = edge {
268            self.graph.remove_edge(i);
269        }
270    }
271
272    /// Disconnect all outgoing connections from a node's output to another node
273    ///
274    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output>
275    pub fn disconnect_output_between(&mut self, out: PortId<OutputPort>, to: NodeId) {
276        let edge = self
277            .graph
278            .edges(out.node().0)
279            .find(|e| e.target() == to.0)
280            .map(|e| e.id());
281        if let Some(edge) = edge {
282            let mut e = self
283                .graph
284                .remove_edge(edge)
285                .expect("Edge index is known to exist");
286            e.remove_by_output(out.1);
287            if !e.connections.is_empty() {
288                self.graph.add_edge(out.node().0, to.0, e);
289            }
290        }
291    }
292
293    /// Disconnect all outgoing connections from a node to another node's input
294    ///
295    /// Only used in WebAudio for disconnecting audio params
296    ///
297    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationparam>
298    pub fn disconnect_to(&mut self, node: NodeId, inp: PortId<InputPort>) {
299        let edge = self
300            .graph
301            .edges(node.0)
302            .find(|e| e.target() == inp.node().0)
303            .map(|e| e.id());
304        if let Some(edge) = edge {
305            let mut e = self
306                .graph
307                .remove_edge(edge)
308                .expect("Edge index is known to exist");
309            e.remove_by_input(inp.1);
310            if !e.connections.is_empty() {
311                self.graph.add_edge(node.0, inp.node().0, e);
312            }
313        }
314    }
315
316    /// Disconnect all outgoing connections from a node's output to another node's input
317    ///
318    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationnode-output-input>
319    /// <https://webaudio.github.io/web-audio-api/#dom-audionode-disconnect-destinationparam-output>
320    pub fn disconnect_output_between_to(
321        &mut self,
322        out: PortId<OutputPort>,
323        inp: PortId<InputPort>,
324    ) {
325        let edge = self
326            .graph
327            .edges(out.node().0)
328            .find(|e| e.target() == inp.node().0)
329            .map(|e| e.id());
330        if let Some(edge) = edge {
331            let mut e = self
332                .graph
333                .remove_edge(edge)
334                .expect("Edge index is known to exist");
335            e.remove_by_pair(out.1, inp.1);
336            if !e.connections.is_empty() {
337                self.graph.add_edge(out.node().0, inp.node().0, e);
338            }
339        }
340    }
341
342    /// Get the id of the destination node in this graph
343    ///
344    /// All graphs have a destination node, with one input port
345    pub fn dest_id(&self) -> NodeId {
346        self.dest_id
347    }
348
349    /// Add additional terminator nodes
350    pub fn add_extra_dest(&mut self, dest: NodeId) {
351        self.dests.push(dest);
352    }
353
354    /// Get the id of the AudioListener in this graph
355    ///
356    /// All graphs have a single listener, with no ports (but nine AudioParams)
357    ///
358    /// N.B. The listener actually has a single output port containing
359    /// its position data for the block, however this should
360    /// not be exposed to the DOM.
361    pub fn listener_id(&self) -> NodeId {
362        self.listener_id
363    }
364
365    /// For a given block, process all the data on this graph
366    ///
367    /// This implements steps 4.2 and parts of 4.4 from
368    /// <https://webaudio.github.io/web-audio-api/#rendering-loop>
369    pub fn process(&mut self, info: &BlockInfo) -> Chunk {
370        // Step 4.2. Order the AudioNodes of the BaseAudioContext to be processed.
371        let cycle_nodes = self.detect_nodes_in_cycles();
372
373        // DFS post order: Children are processed before their parent,
374        // which is exactly what we need since the parent depends on the
375        // children's output
376        //
377        // This will only visit each node once
378        let reversed = Reversed(&self.graph);
379
380        let mut blocks: SmallVec<[SmallVec<[Block; 1]>; 1]> = SmallVec::new();
381        let mut output_counts: SmallVec<[u32; 1]> = SmallVec::new();
382
383        let mut visit = DfsPostOrder::empty(reversed);
384
385        for dest in &self.dests {
386            visit.move_to(dest.0);
387
388            while let Some(ix) = visit.next(reversed) {
389                let mut curr = self.graph[ix].node.borrow_mut();
390
391                if cycle_nodes.contains(&ix) {
392                    // Step 4.2.7. If nodes contains cycles, mute all the AudioNodes
393                    // that are part of this cycle, and remove them from nodes.
394                    let chunk = curr.as_mut().mute_node();
395                    if curr.output_count() == 0 {
396                        // Step 4.4.5. If this AudioNode is a destination node, record
397                        // the input of this AudioNode.
398                        curr.process(chunk, info);
399                    } else {
400                        // Step 4.4.6. Else, process the input buffer, and make
401                        // available for reading the resulting buffer.
402                        self.fill_node_cache_with_silence(ix, curr.output_count());
403                    }
404                    continue;
405                }
406
407                let mut chunk = Chunk::default();
408                chunk
409                    .blocks
410                    .resize(curr.input_count() as usize, Default::default());
411
412                // if we have inputs, collect all the computed blocks
413                // and construct a Chunk
414
415                // set up scratch space to store all the blocks
416                blocks.clear();
417                blocks.resize(curr.input_count() as usize, Default::default());
418
419                let mode = curr.channel_count_mode();
420                let count = curr.channel_count();
421                let interpretation = curr.channel_interpretation();
422
423                // all edges to this node are from its dependencies
424                for edge in self.graph.edges_directed(ix, Direction::Incoming) {
425                    let edge = edge.weight();
426                    for connection in &edge.connections {
427                        let mut block = connection
428                            .cache
429                            .borrow_mut()
430                            .take()
431                            .expect("Cache should have been filled from traversal");
432
433                        match connection.input_idx {
434                            PortIndex::Port(idx) => {
435                                blocks[idx as usize].push(block);
436                            },
437                            PortIndex::Param(param) => {
438                                // param inputs are downmixed to mono
439                                // https://webaudio.github.io/web-audio-api/#dom-audionode-connect-destinationparam-output
440                                block.mix(1, ChannelInterpretation::Speakers);
441                                curr.get_param(param).add_block(block)
442                            },
443                            PortIndex::Listener(_) => curr.set_listenerdata(block),
444                        }
445                    }
446                }
447
448                for (i, mut blocks) in blocks.drain(..).enumerate() {
449                    if blocks.is_empty() {
450                        if mode == ChannelCountMode::Explicit {
451                            // It's silence, but mix it anyway
452                            chunk.blocks[i].mix(count, interpretation);
453                        }
454                    } else if blocks.len() == 1 {
455                        chunk.blocks[i] = blocks.pop().expect("`blocks` had length 1");
456                        match mode {
457                            ChannelCountMode::Explicit => {
458                                chunk.blocks[i].mix(count, interpretation);
459                            },
460                            ChannelCountMode::ClampedMax => {
461                                if chunk.blocks[i].chan_count() > count {
462                                    chunk.blocks[i].mix(count, interpretation);
463                                }
464                            },
465                            // It's one channel, it maxes itself
466                            ChannelCountMode::Max => (),
467                        }
468                    } else {
469                        let mix_count = match mode {
470                            ChannelCountMode::Explicit => count,
471                            _ => {
472                                let mut max = 0; // max channel count
473                                for block in &blocks {
474                                    max = cmp::max(max, block.chan_count());
475                                }
476                                if mode == ChannelCountMode::ClampedMax {
477                                    max = cmp::min(max, count);
478                                }
479                                max
480                            },
481                        };
482                        let block = blocks.into_iter().fold(Block::default(), |acc, mut block| {
483                            block.mix(mix_count, interpretation);
484                            acc.sum(block)
485                        });
486                        chunk.blocks[i] = block;
487                    }
488                }
489
490                // actually run the node engine
491                let mut out = curr.process(chunk, info);
492
493                assert_eq!(out.len(), curr.output_count() as usize);
494                if curr.output_count() == 0 {
495                    continue;
496                }
497
498                // Count how many output connections fan out from each port
499                // This is so that we don't have to needlessly clone audio buffers
500                //
501                // If this is inefficient, we can instead maintain this data
502                // cached on the node
503                output_counts.clear();
504                output_counts.resize(curr.output_count() as usize, 0);
505                for edge in self.graph.edges(ix) {
506                    let edge = edge.weight();
507                    for conn in &edge.connections {
508                        if let PortIndex::Port(idx) = conn.output_idx {
509                            output_counts[idx as usize] += 1;
510                        } else {
511                            unreachable!()
512                        }
513                    }
514                }
515
516                // all the edges from this node go to nodes which depend on it,
517                // i.e. the nodes it outputs to. Store the blocks for retrieval.
518                for edge in self.graph.edges(ix) {
519                    let edge = edge.weight();
520                    for conn in &edge.connections {
521                        if let PortIndex::Port(idx) = conn.output_idx {
522                            output_counts[idx as usize] -= 1;
523                            // if there are no consumers left after this, take the data
524                            let block = if output_counts[idx as usize] == 0 {
525                                out[conn.output_idx].take()
526                            } else {
527                                out[conn.output_idx].clone()
528                            };
529                            *conn.cache.borrow_mut() = Some(block);
530                        } else {
531                            unreachable!()
532                        }
533                    }
534                }
535            }
536        }
537        // The destination node stores its output on itself, extract it.
538        self.graph[self.dest_id.0]
539            .node
540            .borrow_mut()
541            .destination_data()
542            .expect("Destination node should have data cached")
543    }
544
545    /// Obtain a mutable reference to a node
546    pub(crate) fn node_mut(&self, ix: NodeId) -> RefMut<'_, Box<dyn AudioNodeEngine>> {
547        self.graph[ix.0].node.borrow_mut()
548    }
549
550    /// Detect cycles in this graph and return the indices of their nodes.
551    ///
552    /// This covers the cycle-related part of step 4.2. Tarjan's algorithm finds
553    /// the cycles, and [`Self::process`] mutes the affected nodes as required by
554    /// step 4.2.7.
555    ///
556    /// The outer render-loop steps, including returning `render_result`, are
557    /// outside this method because [`Self::process`] returns audio data.
558    ///
559    /// <https://webaudio.github.io/web-audio-api/#rendering-loop>
560    ///
561    /// > 4. Process a render quantum.
562    /// >    2. Order the AudioNodes of the BaseAudioContext to be processed.
563    /// >       4. Let cycle breakers be an empty set of DelayNodes. It will contain all the
564    /// >          DelayNodes that are part of a cycle.
565    /// >       5. For each AudioNode node in nodes:
566    /// >          1. If node is a DelayNode that is part of a cycle, add it to cycle breakers
567    /// >             and remove it from nodes.
568    /// >       6. For each DelayNode delay in cycle breakers:
569    /// >          1. Let delayWriter and delayReader respectively be a DelayWriter and a
570    /// >             DelayReader, for delay. Add delayWriter and delayReader to nodes. Disconnect
571    /// >             delay from all its input and outputs.
572    /// >             Note: This breaks the cycle: if a DelayNode is in a cycle, its two ends can be
573    /// >             considered separately, because delay lines cannot be smaller than one render
574    /// >             quantum when in a cycle.
575    /// >       7. If nodes contains cycles, mute all the AudioNodes that are part of this cycle, and
576    /// >          remove them from nodes.
577    ///
578    /// TODO: Implement steps 4.2.4–4.2.6 for cyclic `DelayNode`s by replacing
579    /// each with a `DelayWriter` and `DelayReader`.
580    fn detect_nodes_in_cycles(&self) -> FxHashSet<NodeIndex<DefaultIx>> {
581        let mut cycle_nodes = FxHashSet::default();
582        for component in tarjan_scc(&self.graph) {
583            // Tarjan's algorithm groups the graph into strongly connected components.
584            // A component with multiple nodes is a cycle; a single-node component is a
585            // cycle only when the node has an edge to itself.
586            let is_cycle = component.len() > 1 ||
587                component.first().is_some_and(|node_index| {
588                    self.graph
589                        .edges(*node_index)
590                        .any(|edge| edge.target() == *node_index)
591                });
592            if is_cycle {
593                cycle_nodes.extend(component);
594            }
595        }
596        cycle_nodes
597    }
598
599    /// <https://webaudio.github.io/web-audio-api/#available-for-reading>
600    ///
601    /// Making a buffer available for reading from an AudioNode means putting
602    /// it in a state where other AudioNodes connected to this AudioNode can
603    /// safely read from it.
604    ///
605    /// The specification does not require these buffers to be silent. This helper
606    /// runs after [`AudioNodeEngine::mute_node`], so it fills the caches with
607    /// silence for downstream nodes.
608    fn fill_node_cache_with_silence(&self, node_index: NodeIndex<DefaultIx>, output_count: u32) {
609        for edge in self.graph.edges(node_index) {
610            let edge = edge.weight();
611            for connection in &edge.connections {
612                if let PortIndex::Port(index) = connection.output_idx {
613                    if index < output_count {
614                        *connection.cache.borrow_mut() = Some(Block::default());
615                    }
616                } else {
617                    unreachable!()
618                }
619            }
620        }
621    }
622}
623
624impl Node {
625    pub fn new(node: Box<dyn AudioNodeEngine>) -> Self {
626        Node {
627            node: RefCell::new(node),
628        }
629    }
630}
631
632impl Edge {
633    pub fn new(input_idx: PortIndex<InputPort>, output_idx: PortIndex<OutputPort>) -> Self {
634        Edge {
635            connections: SmallVec::from_buf([Connection::new(input_idx, output_idx)]),
636        }
637    }
638}
639
640impl Connection {
641    pub fn new(input_idx: PortIndex<InputPort>, output_idx: PortIndex<OutputPort>) -> Self {
642        Connection {
643            input_idx,
644            output_idx,
645            cache: RefCell::new(None),
646        }
647    }
648}