diff --git a/src/node/audio_buffer_source.rs b/src/node/audio_buffer_source.rs index 52bd8878..5a89575c 100644 --- a/src/node/audio_buffer_source.rs +++ b/src/node/audio_buffer_source.rs @@ -771,6 +771,13 @@ impl AudioProcessor for AudioBufferSourceRenderer { log::warn!("AudioBufferSourceRenderer: Dropping incoming message {msg:?}"); } + + fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) { + if !self.ended_triggered && scope.current_time >= self.start_time { + scope.send_ended_event(); + self.ended_triggered = true; + } + } } #[cfg(test)] @@ -778,6 +785,9 @@ mod tests { use float_eq::assert_float_eq; use std::f32::consts::PI; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use crate::context::{BaseAudioContext, OfflineAudioContext}; use crate::RENDER_QUANTUM_SIZE; @@ -1458,4 +1468,72 @@ mod tests { src.stop(); src.stop(); } + + #[test] + fn test_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_buffer_source(); + src.start_at(0.); + src.stop_at(0.5); + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_no_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let src = context.create_constant_source(); + + // do not start the node + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(!ended.load(Ordering::Relaxed)); // should not have triggered + } + + #[test] + fn test_exact_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_buffer_source(); + src.start_at(0.); + src.stop_at(1.); // end right at the end of the offline buffer + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_implicit_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_buffer_source(); + src.start_at(0.); + // no explicit stop, so we stop at end of offline context + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } } diff --git a/src/node/constant_source.rs b/src/node/constant_source.rs index 4417277e..2da969fb 100644 --- a/src/node/constant_source.rs +++ b/src/node/constant_source.rs @@ -255,6 +255,13 @@ impl AudioProcessor for ConstantSourceRenderer { log::warn!("ConstantSourceRenderer: Dropping incoming message {msg:?}"); } + + fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) { + if !self.ended_triggered && scope.current_time >= self.start_time { + scope.send_ended_event(); + self.ended_triggered = true; + } + } } #[cfg(test)] @@ -264,6 +271,9 @@ mod tests { use float_eq::assert_float_eq; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use super::*; #[test] @@ -369,4 +379,72 @@ mod tests { src.stop(); src.stop(); } + + #[test] + fn test_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_constant_source(); + src.start_at(0.); + src.stop_at(0.5); + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_no_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let src = context.create_constant_source(); + + // do not start the node + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(!ended.load(Ordering::Relaxed)); // should not have triggered + } + + #[test] + fn test_exact_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_constant_source(); + src.start_at(0.); + src.stop_at(1.); // end right at the end of the offline buffer + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_implicit_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_constant_source(); + src.start_at(0.); + // no explicit stop, so we stop at end of offline context + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } } diff --git a/src/node/oscillator.rs b/src/node/oscillator.rs index 001b809b..1fc5b5e9 100644 --- a/src/node/oscillator.rs +++ b/src/node/oscillator.rs @@ -457,6 +457,13 @@ impl AudioProcessor for OscillatorRenderer { log::warn!("OscillatorRenderer: Dropping incoming message {msg:?}"); } + + fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) { + if !self.ended_triggered && scope.current_time >= self.start_time { + scope.send_ended_event(); + self.ended_triggered = true; + } + } } impl OscillatorRenderer { @@ -608,6 +615,9 @@ mod tests { use float_eq::assert_float_eq; use std::f64::consts::PI; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use crate::context::{BaseAudioContext, OfflineAudioContext}; use crate::node::{AudioNode, AudioScheduledSourceNode}; use crate::periodic_wave::{PeriodicWave, PeriodicWaveOptions}; @@ -1247,4 +1257,72 @@ mod tests { osc.stop(); osc.stop(); } + + #[test] + fn test_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_oscillator(); + src.start_at(0.); + src.stop_at(0.5); + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_no_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let src = context.create_oscillator(); + + // do not start the node + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(!ended.load(Ordering::Relaxed)); // should not have triggered + } + + #[test] + fn test_exact_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_oscillator(); + src.start_at(0.); + src.stop_at(1.); // end right at the end of the offline buffer + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } + + #[test] + fn test_implicit_ended_event() { + let mut context = OfflineAudioContext::new(2, 44_100, 44_100.); + let mut src = context.create_oscillator(); + src.start_at(0.); + // no explicit stop, so we stop at end of offline context + + let ended = Arc::new(AtomicBool::new(false)); + let ended_clone = Arc::clone(&ended); + src.set_onended(move |_event| { + ended_clone.store(true, Ordering::Relaxed); + }); + + let _ = context.start_rendering_sync(); + assert!(ended.load(Ordering::Relaxed)); + } } diff --git a/src/render/graph.rs b/src/render/graph.rs index 0db8b609..1d03ba31 100644 --- a/src/render/graph.rs +++ b/src/render/graph.rs @@ -503,6 +503,7 @@ impl Graph { let mut node = self.nodes.remove(*index).into_inner(); self.reclaim_id_channel .push(node.reclaim_id.take().unwrap()); + node.processor.before_drop(scope); drop(node); // And remove it from the ordering after we have processed all nodes @@ -536,6 +537,13 @@ impl Graph { // Return the output buffer of destination node &self.nodes.get_unchecked_mut(AudioNodeId(0)).outputs[0] } + + pub fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) { + self.nodes.iter_mut().for_each(|(id, node)| { + scope.node_id.set(id); + node.get_mut().processor.before_drop(scope); + }); + } } #[cfg(test)] diff --git a/src/render/node_collection.rs b/src/render/node_collection.rs index 33b49fac..592eeb7a 100644 --- a/src/render/node_collection.rs +++ b/src/render/node_collection.rs @@ -57,6 +57,14 @@ impl NodeCollection { self.nodes.iter_mut().filter_map(Option::as_mut) } + #[inline(always)] + pub fn iter_mut(&mut self) -> impl Iterator)> { + self.nodes + .iter_mut() + .enumerate() + .filter_map(|(i, v)| v.as_mut().map(|m| (AudioNodeId(i as u64), m))) + } + #[inline(always)] pub fn contains(&self, index: AudioNodeId) -> bool { self.nodes[index.0 as usize].is_some() diff --git a/src/render/processor.rs b/src/render/processor.rs index dfc20cc5..3376dea6 100644 --- a/src/render/processor.rs +++ b/src/render/processor.rs @@ -172,6 +172,8 @@ pub trait AudioProcessor: Send { fn has_side_effects(&self) -> bool { false } + + fn before_drop(&mut self, _scope: &AudioWorkletGlobalScope) {} } impl std::fmt::Debug for dyn AudioProcessor { diff --git a/src/render/thread.rs b/src/render/thread.rs index 621fa64c..ba6eb862 100644 --- a/src/render/thread.rs +++ b/src/render/thread.rs @@ -239,6 +239,7 @@ impl RenderThread { event_loop: &EventLoop, ) -> AudioBuffer { let length = context.length(); + let sample_rate = self.sample_rate; // construct a properly sized output buffer let mut buffer = Vec::with_capacity(self.number_of_channels); @@ -268,7 +269,11 @@ impl RenderThread { } } - AudioBuffer::from(buffer, self.sample_rate) + // call destructors of all alive nodes and handle any resulting events + self.unload_graph(); + event_loop.handle_pending_events(); + + AudioBuffer::from(buffer, sample_rate) } // Render method of the `OfflineAudioContext::start_rendering` @@ -283,6 +288,8 @@ impl RenderThread { mut resume_receiver: mpsc::Receiver<()>, event_loop: &EventLoop, ) -> AudioBuffer { + let sample_rate = self.sample_rate; + // construct a properly sized output buffer let mut buffer = Vec::with_capacity(self.number_of_channels); buffer.resize_with(buffer.capacity(), || Vec::with_capacity(length)); @@ -312,7 +319,11 @@ impl RenderThread { } } - AudioBuffer::from(buffer, self.sample_rate) + // call destructors of all alive nodes and handle any resulting events + self.unload_graph(); + event_loop.handle_pending_events(); + + AudioBuffer::from(buffer, sample_rate) } /// Render a single quantum into an AudioBuffer @@ -320,7 +331,7 @@ impl RenderThread { // Update time let current_frame = self .frames_played - .fetch_add(RENDER_QUANTUM_SIZE as u64, Ordering::SeqCst); + .fetch_add(RENDER_QUANTUM_SIZE as u64, Ordering::Relaxed); let current_time = current_frame as f64 / self.sample_rate as f64; let scope = AudioWorkletGlobalScope { @@ -354,6 +365,21 @@ impl RenderThread { }); } + /// Run destructors of all alive nodes in the audio graph + fn unload_graph(mut self) { + let current_frame = self.frames_played.load(Ordering::Relaxed); + let current_time = current_frame as f64 / self.sample_rate as f64; + + let scope = AudioWorkletGlobalScope { + current_frame, + current_time, + sample_rate: self.sample_rate, + event_sender: self.event_sender.clone(), + node_id: Cell::new(AudioNodeId(0)), // placeholder value + }; + self.graph.take().unwrap().before_drop(&scope); + } + pub fn render + Clone>(&mut self, output_buffer: &mut [S]) { // Collect timing information let render_start = Instant::now(); @@ -372,7 +398,7 @@ impl RenderThread { let max_duration = RENDER_QUANTUM_SIZE as f64 / self.sample_rate as f64; let load_value = duration / max_duration; let render_timestamp = - self.frames_played.load(Ordering::SeqCst) as f64 / self.sample_rate as f64; + self.frames_played.load(Ordering::Relaxed) as f64 / self.sample_rate as f64; let load_value_data = AudioRenderCapacityLoad { render_timestamp, load_value, @@ -431,7 +457,7 @@ impl RenderThread { // update time let current_frame = self .frames_played - .fetch_add(RENDER_QUANTUM_SIZE as u64, Ordering::SeqCst); + .fetch_add(RENDER_QUANTUM_SIZE as u64, Ordering::Relaxed); let current_time = current_frame as f64 / self.sample_rate as f64; let scope = AudioWorkletGlobalScope {