diff --git a/src/context/concrete_base.rs b/src/context/concrete_base.rs index af6d3079..01b014af 100644 --- a/src/context/concrete_base.rs +++ b/src/context/concrete_base.rs @@ -203,6 +203,17 @@ impl ConcreteBaseAudioContext { } } + /// Inform render thread that this node can act as a cycle breaker + #[doc(hidden)] + pub fn mark_cycle_breaker(&self, reg: &AudioContextRegistration) { + let id = reg.id().0; + let message = ControlMessage::MarkCycleBreaker { id }; + + // Sending the message will fail when the render thread has already shut down. + // This is fine + let _r = self.inner.render_channel.send(message); + } + /// `ChannelConfig` of the `AudioDestinationNode` pub(super) fn destination_channel_config(&self) -> ChannelConfig { self.inner.destination_channel_config.clone() diff --git a/src/message.rs b/src/message.rs index 5d28d755..86f637fe 100644 --- a/src/message.rs +++ b/src/message.rs @@ -26,17 +26,28 @@ pub(crate) enum ControlMessage { }, /// Clear the connection between two given nodes in the audio graph - DisconnectNode { from: u64, to: u64 }, + DisconnectNode { + from: u64, + to: u64, + }, /// Disconnect this node from the audio graph (drop all its connections) - DisconnectAll { from: u64 }, + DisconnectAll { + from: u64, + }, /// Notify the render thread this node is dropped in the control thread - FreeWhenFinished { id: u64 }, + FreeWhenFinished { + id: u64, + }, /// Pass an AudioParam AutomationEvent to the relevant node AudioParamEvent { to: Sender, event: AudioParamEvent, }, + + MarkCycleBreaker { + id: u64, + }, } diff --git a/src/node/constant_source.rs b/src/node/constant_source.rs index 6c23d3d3..d42abcde 100644 --- a/src/node/constant_source.rs +++ b/src/node/constant_source.rs @@ -162,11 +162,6 @@ impl AudioProcessor for ConstantSourceRenderer { return true; } - if stop_time < scope.current_time { - output.make_silent(); - return false; - } - output.force_mono(); let offset_values = params.get(&self.offset); @@ -186,7 +181,8 @@ impl AudioProcessor for ConstantSourceRenderer { current_time += dt; } - true + // tail_time false when output has ended this quantum + stop_time >= next_block_time } } diff --git a/src/node/delay.rs b/src/node/delay.rs index c9917e3c..c147f723 100644 --- a/src/node/delay.rs +++ b/src/node/delay.rs @@ -7,6 +7,7 @@ use super::{AudioNode, ChannelConfig, ChannelConfigOptions, ChannelInterpretatio use std::cell::{Cell, RefCell, RefMut}; use std::rc::Rc; +use std::sync::atomic::{AtomicU64, Ordering}; /// Options for constructing a [`DelayNode`] // dictionary DelayOptions : AudioNodeOptions { @@ -30,7 +31,7 @@ impl Default for DelayOptions { } } -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] struct PlaybackInfo { prev_block_index: usize, prev_frame_index: usize, @@ -213,7 +214,12 @@ impl DelayNode { let last_written_index = Rc::new(Cell::>::new(None)); let last_written_index_clone = last_written_index.clone(); - context.register(move |writer_registration| { + // shared value for reader/writer to determine who was rendered first, + // this will indicate if the delay node acts as a cycle breaker + let latest_frame_written = Rc::new(AtomicU64::new(u64::MAX)); + let latest_frame_written_clone = latest_frame_written.clone(); + + let node = context.register(move |writer_registration| { let node = context.register(move |reader_registration| { let param_opts = AudioParamDescriptor { min_value: 0., @@ -230,7 +236,9 @@ impl DelayNode { ring_buffer: shared_ring_buffer_clone, index: 0, last_written_index: last_written_index_clone, + in_cycle: false, last_written_index_checked: None, + latest_frame_written: latest_frame_written_clone, }; let node = DelayNode { @@ -247,10 +255,21 @@ impl DelayNode { ring_buffer: shared_ring_buffer, index: 0, last_written_index, + latest_frame_written, }; (node, Box::new(writer_render)) - }) + }); + + let writer_id = node.writer_registration.id(); + let reader_id = node.reader_registration.id(); + // connect Writer to Reader to guarantee order of processing and enable + // sub-quantum delay. If found in cycle this connection will be deleted + // by the graph and the minimum delay clamped to one render quantum + context.base().mark_cycle_breaker(&node.writer_registration); + context.base().connect(writer_id, reader_id, 0, 0); + + node } /// A-rate [`AudioParam`] representing the amount of delay (in seconds) to apply. @@ -262,6 +281,7 @@ impl DelayNode { struct DelayWriter { ring_buffer: Rc>>, index: usize, + latest_frame_written: Rc, last_written_index: Rc>>, } @@ -269,6 +289,8 @@ struct DelayReader { delay_time: AudioParamId, ring_buffer: Rc>>, index: usize, + latest_frame_written: Rc, + in_cycle: bool, last_written_index: Rc>>, // local copy of shared `last_written_index` so as to avoid render ordering issues last_written_index_checked: Option, @@ -327,7 +349,7 @@ impl AudioProcessor for DelayWriter { inputs: &[AudioRenderQuantum], outputs: &mut [AudioRenderQuantum], _params: AudioParamValues, - _scope: &RenderScope, + scope: &RenderScope, ) -> bool { // single input/output node let input = inputs[0].clone(); @@ -344,13 +366,16 @@ impl AudioProcessor for DelayWriter { let mut buffer = self.ring_buffer.borrow_mut(); buffer[self.index] = input; - // increment cursor + // increment cursor and last written frame self.index = (self.index + 1) % buffer.capacity(); + self.latest_frame_written + .store(scope.current_frame, Ordering::SeqCst); + // The writer end does not produce output, // clear the buffer so that it can be re-used output.make_silent(); - // let the node be decommisionned if it has no input left + // let the node be decommisioned if it has no input left false } } @@ -413,6 +438,15 @@ impl AudioProcessor for DelayReader { let delay_param = params.get(&self.delay_time); + if !self.in_cycle { + // check the latest written frame by the delay writer + let latest_frame_written = self.latest_frame_written.load(Ordering::SeqCst); + // if the delay writer has not rendered before us, the cycle breaker has been applied + self.in_cycle = latest_frame_written != scope.current_frame; + // once we store in_cycle = true, we do not want to go back to false + // https://github.com/orottier/web-audio-api-rs/pull/198#discussion_r945326200 + } + let ring_size = ring_buffer.len() as i32; let ring_index = self.index as i32; @@ -432,11 +466,17 @@ impl AudioProcessor for DelayReader { .zip(playback_infos.iter_mut()) .for_each(|(((index, o), delay), infos)| { if channel_number == 0 { - // param is already clamped to max_delay_time internally, so it is - // safe to only check lower boundary - let clamped_delay = (*delay as f64).max(quantum_duration); - let num_samples = clamped_delay * sample_rate; + // param is already clamped to max_delay_time internally + let mut delay = f64::from(*delay); + // if delay is in cycle, minimum delay is one quantum + if self.in_cycle { + delay = delay.max(quantum_duration); + } + + let num_samples = delay * sample_rate; // negative position of the playhead relative to this block start + // @note: in sub-quantum delays, position can be a positive number + // because it is then positive according to the block start let position = index as f64 - num_samples; // find address of the frame in the ring buffer just before `position` @@ -500,7 +540,7 @@ impl AudioProcessor for DelayReader { return false; } - // check if the writer has been decommisionned + // check if the writer has been decommissioned // we need this local copy because if the writer has been processed // before the reader, the direct check against `self.last_written_index` // would be true earlier than we want @@ -541,7 +581,12 @@ impl DelayReader { if frame_offset == 0 { frame_offset = -num_frames; } - let frame_index = num_frames + frame_offset; + let frame_index = if frame_offset <= 0 { + num_frames + frame_offset + } else { + // sub-quantum delay + frame_offset + }; (block_index as usize, frame_index as usize) } @@ -666,12 +711,12 @@ mod tests { let channel_left = result.get_channel_data(0); let mut expected_left = vec![0.; 256]; expected_left[128] = 1.; - assert_float_eq!(channel_left[..], expected_left[..], abs_all <= 0.); + assert_float_eq!(channel_left[..], expected_left[..], abs_all <= 1e-5); let channel_right = result.get_channel_data(1); let mut expected_right = vec![0.; 256]; expected_right[128 + 1] = 1.; - assert_float_eq!(channel_right[..], expected_right[..], abs_all <= 0.); + assert_float_eq!(channel_right[..], expected_right[..], abs_all <= 1e-5); } #[test] @@ -708,13 +753,13 @@ mod tests { let mut expected_left = vec![0.; 3 * 128]; expected_left[128] = 1.; expected_left[256] = 1.; - assert_float_eq!(channel_left[..], expected_left[..], abs_all <= 0.); + assert_float_eq!(channel_left[..], expected_left[..], abs_all <= 1e-5); let channel_right = result.get_channel_data(1); let mut expected_right = vec![0.; 3 * 128]; expected_right[128] = 1.; expected_right[256 + 1] = 1.; - assert_float_eq!(channel_right[..], expected_right[..], abs_all <= 0.); + assert_float_eq!(channel_right[..], expected_right[..], abs_all <= 1e-5); } #[test] @@ -749,24 +794,18 @@ mod tests { // source starts after 2 * 128 samples, then is delayed another 128 expected[4 * 128] = 1.; - assert_float_eq!(result.get_channel_data(0), &expected[..], abs_all <= 0.); + assert_float_eq!(result.get_channel_data(0), &expected[..], abs_all <= 1e-5); } } #[test] - fn test_max_delay_multiple_of_quantum_size() { - // regression test that delay node has always enough internal buffer size when - // max_delay is a multiple of quantum size and delay == max_delay. We need - // to test multiple times since (currently) the topological sort of the - // graph depends on randomized hash values. This bug only occurs when the - // Writer is called earlier than the Reader. 10 times should do: - for _ in 0..10 { - // set delay and max delay time exactly 1 render quantum + fn test_subquantum_delay() { + for i in 0..128 { let sample_rate = 48000.; - let context = OfflineAudioContext::new(1, 256, sample_rate); + let context = OfflineAudioContext::new(1, 128, sample_rate); let delay = context.create_delay(1.); - delay.delay_time.set_value(128. / sample_rate); + delay.delay_time.set_value(i as f32 / sample_rate); delay.connect(&context.destination()); let mut dirac = context.create_buffer(1, 1, sample_rate); @@ -780,21 +819,67 @@ mod tests { let result = context.start_rendering_sync(); let channel = result.get_channel_data(0); - let mut expected = vec![0.; 256]; - expected[128] = 1.; + let mut expected = vec![0.; 128]; + expected[i] = 1.; - assert_float_eq!(channel[..], expected[..], abs_all <= 0.); + assert_float_eq!(channel[..], expected[..], abs_all <= 1e-5); } + } + + #[test] + fn test_min_delay_when_in_loop() { + let sample_rate = 480000.; + let context = OfflineAudioContext::new(1, 256, sample_rate); + + let delay = context.create_delay(1.); + delay.delay_time.set_value(1. / sample_rate); + delay.connect(&context.destination()); + // create a loop with a gain at 0 to avoid feedback + // therefore delay_time will be clamped to 128 * sample_rate by the Reader + let gain = context.create_gain(); + gain.gain().set_value(0.); + delay.connect(&gain); + gain.connect(&delay); + + let mut dirac = context.create_buffer(1, 1, sample_rate); + dirac.copy_to_channel(&[1.], 0); + + let src = context.create_buffer_source(); + src.connect(&delay); + src.set_buffer(dirac); + src.start_at(0.); + + let result = context.start_rendering_sync(); + let channel = result.get_channel_data(0); + + let mut expected = vec![0.; 256]; + expected[128] = 1.; + + assert_float_eq!(channel[..], expected[..], abs_all <= 0.); + } + #[test] + fn test_max_delay_smaller_than_quantum_size() { + // regression test that even if the declared max_delay_time is smaller than + // a quantum duration, the node internally clamps it to quantum duration so + // that everything works even if order of processing is not garanteed + // (i.e. when delay is in a loop) for _ in 0..10 { - // set delay and max delay time exactly 2 render quantum - let sample_rate = 48000.; - let context = OfflineAudioContext::new(1, 3 * 128, sample_rate); + let sample_rate = 480000.; + let context = OfflineAudioContext::new(1, 256, sample_rate); - let delay = context.create_delay(2.); - delay.delay_time.set_value(128. * 2. / sample_rate); + // this will be internally clamped to 128 * sample_rate + let delay = context.create_delay((64. / sample_rate).into()); + // this will be clamped to 128 * sample_rate by the Reader + delay.delay_time.set_value(64. / sample_rate); delay.connect(&context.destination()); + // create a loop with a gain at 0 to avoid feedback + let gain = context.create_gain(); + gain.gain().set_value(0.); + delay.connect(&gain); + gain.connect(&delay); + let mut dirac = context.create_buffer(1, 1, sample_rate); dirac.copy_to_channel(&[1.], 0); @@ -806,27 +891,27 @@ mod tests { let result = context.start_rendering_sync(); let channel = result.get_channel_data(0); - let mut expected = vec![0.; 3 * 128]; - expected[256] = 1.; + let mut expected = vec![0.; 256]; + expected[128] = 1.; - assert_float_eq!(channel[..], expected[..], abs_all <= 0.00001); + assert_float_eq!(channel[..], expected[..], abs_all <= 0.); } } #[test] - fn test_max_delay_smaller_than_quantum_size() { - // regression test that even if the declared max_delay_time is smaller than - // a quantum duration (which is not allowed for now), the node internally - // clamp it to quantum duration so that everything works even if order - // of processing is not garanteed (which is the default behavior for now). - // When allowing sub quantum delay, this will also guarantees that the node - // gracefully fallback to min - for _ in 0..10 { - let sample_rate = 480000.; + fn test_max_delay_multiple_of_quantum_size() { + // regression test that delay node has always enough internal buffer size + // when max_delay is a multiple of quantum size and delay == max_delay. + // This bug only occurs when the Writer is called before than the Reader, + // which is the case when not in a loop + + // set delay and max delay time exactly 1 render quantum + { + let sample_rate = 48000.; let context = OfflineAudioContext::new(1, 256, sample_rate); - let delay = context.create_delay(0.5); // this will be internally clamped to 1. - delay.delay_time.set_value(0.5 / sample_rate); // this will be clamped to 1. by the Reader + let delay = context.create_delay(1.); + delay.delay_time.set_value(128. / sample_rate); delay.connect(&context.destination()); let mut dirac = context.create_buffer(1, 1, sample_rate); @@ -843,7 +928,63 @@ mod tests { let mut expected = vec![0.; 256]; expected[128] = 1.; - assert_float_eq!(channel[..], expected[..], abs_all <= 0.); + assert_float_eq!(channel[..], expected[..], abs_all <= 1e-5); + } + + // set delay and max delay time exactly 2 render quantum + { + let sample_rate = 48000.; + let context = OfflineAudioContext::new(1, 3 * 128, sample_rate); + + let delay = context.create_delay(2.); + delay.delay_time.set_value(128. * 2. / sample_rate); + delay.connect(&context.destination()); + + let mut dirac = context.create_buffer(1, 1, sample_rate); + dirac.copy_to_channel(&[1.], 0); + + let src = context.create_buffer_source(); + src.connect(&delay); + src.set_buffer(dirac); + src.start_at(0.); + + let result = context.start_rendering_sync(); + let channel = result.get_channel_data(0); + + let mut expected = vec![0.; 3 * 128]; + expected[256] = 1.; + + assert_float_eq!(channel[..], expected[..], abs_all <= 1e-5); } } + + #[test] + fn test_subquantum_delay_dynamic_lifetime() { + let sample_rate = 48000.; + let context = OfflineAudioContext::new(1, 3 * 128, sample_rate); + + // Setup a source that emits for 120 frames, so it deallocates after the first render + // quantum. Delay the signal with 64 frames. Deallocation of the delay writer might trick + // the delay reader into thinking it is part of a cycle, and would clamp the delay to a + // full render quantum. + { + let delay = context.create_delay(1.); + delay.delay_time.set_value(64_f32 / sample_rate); + delay.connect(&context.destination()); + + // emit 120 samples + let src = context.create_constant_source(); + src.connect(&delay); + src.start_at(0.); + src.stop_at(120. / sample_rate as f64); + } // drop all nodes, trigger dynamic lifetimes + + let result = context.start_rendering_sync(); + let channel = result.get_channel_data(0); + + let mut expected = vec![0.; 3 * 128]; + expected[64..64 + 120].fill(1.); + + assert_float_eq!(channel[..], expected[..], abs_all <= 1e-5); + } } diff --git a/src/render/graph.rs b/src/render/graph.rs index 0b0312de..aa85be97 100644 --- a/src/render/graph.rs +++ b/src/render/graph.rs @@ -34,6 +34,8 @@ pub struct Node { free_when_finished: bool, /// Indicates if the node has any incoming connections (for lifecycle management) has_inputs_connected: bool, + /// Indicates if the node can act as a cycle breaker (only DelayNode for now) + cycle_breaker: bool, } impl Node { @@ -83,6 +85,8 @@ pub(crate) struct Graph { marked_temp: Vec, /// Topological sorting helper in_cycle: Vec, + /// Topological sorting helper + cycle_breakers: Vec, } impl Graph { @@ -93,6 +97,7 @@ impl Graph { marked: vec![], marked_temp: vec![], in_cycle: vec![], + cycle_breakers: vec![], alloc: Alloc::with_capacity(64), } } @@ -122,15 +127,16 @@ impl Graph { outgoing_edges: smallvec![], free_when_finished: false, has_inputs_connected: false, + cycle_breaker: false, }), ); } pub fn add_edge(&mut self, source: (NodeIndex, usize), dest: (NodeIndex, usize)) { self.nodes - .get(&source.0) + .get_mut(&source.0) .unwrap_or_else(|| panic!("cannot connect {:?} to {:?}", source, dest)) - .borrow_mut() + .get_mut() .outgoing_edges .push(OutgoingEdge { self_index: source.1, @@ -143,9 +149,9 @@ impl Graph { pub fn remove_edge(&mut self, source: NodeIndex, dest: NodeIndex) { self.nodes - .get(&source) + .get_mut(&source) .unwrap_or_else(|| panic!("cannot remove the edge from {:?} to {:?}", source, dest)) - .borrow_mut() + .get_mut() .outgoing_edges .retain(|edge| edge.other_id != dest); @@ -154,14 +160,14 @@ impl Graph { pub fn remove_edges_from(&mut self, source: NodeIndex) { self.nodes - .get(&source) + .get_mut(&source) .unwrap_or_else(|| panic!("cannot remove edges from {:?}", source)) - .borrow_mut() + .get_mut() .outgoing_edges .clear(); - self.nodes.values().for_each(|node| { - node.borrow_mut() + self.nodes.values_mut().for_each(|node| { + node.get_mut() .outgoing_edges .retain(|edge| edge.other_id != source); }); @@ -173,12 +179,20 @@ impl Graph { // Issue #92, a race condition can occur for AudioParams. They may have already been // removed from the audio graph if the node they feed into was dropped. // Therefore, do not assume this node still exists: - if let Some(node) = self.nodes.get(&index) { - node.borrow_mut().free_when_finished = true; + if let Some(node) = self.nodes.get_mut(&index) { + node.get_mut().free_when_finished = true; } } + pub fn mark_cycle_breaker(&mut self, index: NodeIndex) { + self.nodes.get_mut(&index).unwrap().get_mut().cycle_breaker = true; + } + /// Helper function for `order_nodes` - traverse node and outgoing edges + /// + /// The return value indicates `cycle_breaker_applied`: + /// - true: a cycle was found and a cycle breaker was applied, current ordering is invalidated + /// - false: visiting this leg was successful and no topological changes were applied fn visit( &self, node_id: NodeIndex, @@ -186,18 +200,35 @@ impl Graph { marked_temp: &mut Vec, ordered: &mut Vec, in_cycle: &mut Vec, - ) { + cycle_breakers: &mut Vec, + ) -> bool { // If this node is in the cycle detection list, it is part of a cycle! if let Some(pos) = marked_temp.iter().position(|&m| m == node_id) { - // Mark all nodes in the cycle - in_cycle.extend_from_slice(&marked_temp[pos..]); - // Do not continue, as we already have visited all these nodes - return; + // check if we can find some node that can break the cycle + let cycle_breaker_node = marked_temp + .iter() + .skip(pos) + .find(|node_id| self.nodes.get(node_id).unwrap().borrow().cycle_breaker); + + match cycle_breaker_node { + Some(&node_id) => { + // store node id to clear the node outgoing edges + cycle_breakers.push(node_id); + + return true; + } + None => { + // Mark all nodes in the cycle + in_cycle.extend_from_slice(&marked_temp[pos..]); + // Do not continue, as we already have visited all these nodes + return false; + } + } } // Do not visit nodes multiple times if marked.contains(&node_id) { - return; + return false; } // Add node to the visited list @@ -206,19 +237,34 @@ impl Graph { marked_temp.push(node_id); // Visit outgoing nodes, and call `visit` on them recursively - self.nodes + for edge in self + .nodes .get(&node_id) .unwrap() .borrow() .outgoing_edges .iter() - .for_each(|edge| self.visit(edge.other_id, marked, marked_temp, ordered, in_cycle)); + { + let cycle_breaker_applied = self.visit( + edge.other_id, + marked, + marked_temp, + ordered, + in_cycle, + cycle_breakers, + ); + if cycle_breaker_applied { + return true; + } + } // Then add this node to the ordered list ordered.push(node_id); // Finished visiting all nodes in this leg, clear the current cycle detection list marked_temp.retain(|marked| *marked != node_id); + + false } /// Determine the order of the audio nodes for rendering @@ -232,7 +278,8 @@ impl Graph { /// /// The goals are: /// - Perform a topological sort of the graph - /// - Mute nodes that are in a cycle + /// - Break cycles when possible (if there is a DelayNode present) + /// - Mute nodes that are still in a cycle /// - For performance: no new allocations (reuse Vecs) fn order_nodes(&mut self) { // For borrowck reasons, we need the `visit` call to be &self. @@ -241,27 +288,51 @@ impl Graph { let mut marked = std::mem::take(&mut self.marked); let mut marked_temp = std::mem::take(&mut self.marked_temp); let mut in_cycle = std::mem::take(&mut self.in_cycle); + let mut cycle_breakers = std::mem::take(&mut self.cycle_breakers); + + // When a cycle breaker is applied, the graph topology changes and we need to run the + // ordering again + loop { + // Clear previous administration + ordered.clear(); + marked.clear(); + marked_temp.clear(); + in_cycle.clear(); + cycle_breakers.clear(); + + // Visit all registered nodes, and perform a depth first traversal. + // + // We cannot just start from the AudioDestinationNode and visit all nodes connecting to it, + // since the audio graph could contain legs detached from the destination and those should + // still be rendered. + let mut cycle_breaker_applied = false; + for &node_id in self.nodes.keys() { + cycle_breaker_applied = self.visit( + node_id, + &mut marked, + &mut marked_temp, + &mut ordered, + &mut in_cycle, + &mut cycle_breakers, + ); + + if cycle_breaker_applied { + break; + } + } - // Clear previous administration - ordered.clear(); - marked.clear(); - marked_temp.clear(); - in_cycle.clear(); - - // Visit all registered nodes, and perform a depth first traversal. - // - // We cannot just start from the AudioDestinationNode and visit all nodes connecting to it, - // since the audio graph could contain legs detached from the destination and those should - // still be rendered. - self.nodes.keys().for_each(|&node_id| { - self.visit( - node_id, - &mut marked, - &mut marked_temp, - &mut ordered, - &mut in_cycle, - ); - }); + if cycle_breaker_applied { + // clear the outgoing edges of the nodes that have been recognized as cycle breaker + cycle_breakers.iter().for_each(|node_id| { + let node = self.nodes.get_mut(node_id).unwrap(); + node.get_mut().outgoing_edges.clear(); + }); + + continue; + } + + break; + } // Remove nodes from the ordering if they are part of a cycle. The spec mandates that their // outputs should be silenced, but with our rendering algorithm that is not necessary. @@ -276,6 +347,7 @@ impl Graph { self.marked = marked; self.marked_temp = marked_temp; self.in_cycle = in_cycle; + self.cycle_breakers = cycle_breakers; } /// Render a single audio quantum by traversing the node list @@ -293,7 +365,7 @@ impl Graph { // process every node, in topological sorted order self.ordered.iter().for_each(|index| { - // remove node from graph, re-insert later (for borrowck reasons) + // acquire a mutable borrow of the current processing node let mut node = nodes.get(index).unwrap().borrow_mut(); // make sure all input buffers have the correct number of channels, this might not be @@ -369,7 +441,7 @@ impl Graph { } // Return the output buffer of destination node - self.nodes.get(&NodeIndex(0)).unwrap().borrow().outputs[0].clone() + self.nodes.get_mut(&NodeIndex(0)).unwrap().get_mut().outputs[0].clone() } } diff --git a/src/render/thread.rs b/src/render/thread.rs index 8452b554..dc968505 100644 --- a/src/render/thread.rs +++ b/src/render/thread.rs @@ -86,6 +86,9 @@ impl RenderThread { AudioParamEvent { to, event } => { to.send(event).expect("Audioparam disappeared unexpectedly") } + MarkCycleBreaker { id } => { + self.graph.mark_cycle_breaker(NodeIndex(id)); + } } } } diff --git a/tests/graph.rs b/tests/graph.rs new file mode 100644 index 00000000..977dc928 --- /dev/null +++ b/tests/graph.rs @@ -0,0 +1,385 @@ +use web_audio_api::context::{AudioContextRegistration, BaseAudioContext, OfflineAudioContext}; +use web_audio_api::node::{AudioNode, ChannelConfig}; +use web_audio_api::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, RenderScope}; +use web_audio_api::RENDER_QUANTUM_SIZE; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use rand::seq::SliceRandom; +use rand::thread_rng; + +type Label = u32; + +fn test_ordering(nodes: &[Label], edges: &[[Label; 2]], test: impl Fn(Vec