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}