diff --git a/src/buffer.rs b/src/buffer.rs index 71824186..cd97c4ae 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -66,8 +66,8 @@ pub struct AudioBufferOptions { /// #[derive(Clone, Debug)] pub struct AudioBuffer { - pub(crate) channels: Vec, - pub(crate) sample_rate: f32, + channels: Vec, + sample_rate: f32, } impl AudioBuffer { @@ -91,6 +91,14 @@ impl AudioBuffer { } } + /// Creates an invalid, but non-allocating AudioBuffer to be used as placeholder + pub(crate) fn tombstone() -> Self { + Self { + channels: Default::default(), + sample_rate: Default::default(), + } + } + /// Convert raw samples to an AudioBuffer /// /// The outer Vec determine the channels. The inner Vecs should have the same length. diff --git a/src/node/audio_buffer_source.rs b/src/node/audio_buffer_source.rs index 2fad9a3c..c8a0a9d9 100644 --- a/src/node/audio_buffer_source.rs +++ b/src/node/audio_buffer_source.rs @@ -215,6 +215,7 @@ impl AudioBufferSourceNode { loop_state: loop_state.clone(), render_state: AudioBufferRendererState::default(), ended_triggered: false, + buffer_deallocator: Some(llq::Node::new(Box::new(AudioBuffer::tombstone()))), }; let inner_state = InnerState { @@ -379,6 +380,7 @@ struct AudioBufferSourceRenderer { loop_state: LoopState, render_state: AudioBufferRendererState, ended_triggered: bool, + buffer_deallocator: Option>>, } impl AudioBufferSourceRenderer { @@ -395,6 +397,26 @@ impl AudioBufferSourceRenderer { ControlMessage::LoopEnd(loop_end) => self.loop_state.end = *loop_end, } } + + fn on_end(&mut self, scope: &RenderScope) { + if self.ended_triggered { + return; // only run once + } + self.ended_triggered = true; + + // notify the control thread + scope.send_ended_event(); + + // deallocate the AudioBuffer asynchronously - not in the render thread + if let Some(buffer) = self.buffer.take() { + // the holder contains a tombstone AudioBuffer that can be dropped without deallocation + let mut holder = self.buffer_deallocator.take().unwrap(); + // replace the contents of the holder with the actual buffer + *holder.downcast_mut().unwrap() = buffer; + // ship the buffer to the deallocator thread + scope.deallocate_async(holder); + } + } } impl AudioProcessor for AudioBufferSourceRenderer { @@ -461,13 +483,7 @@ impl AudioProcessor for AudioBufferSourceRenderer { || self.render_state.buffer_time_elapsed >= self.duration { output.make_silent(); // also converts to mono - - // @note: we need this check because this is called a until the program - // ends, such as if the node was never removed from the graph - if !self.ended_triggered { - scope.send_ended_event(); - self.ended_triggered = true; - } + self.on_end(scope); return false; } @@ -479,19 +495,13 @@ impl AudioProcessor for AudioBufferSourceRenderer { if !is_looping { if computed_playback_rate > 0. && buffer_time >= buffer_duration { output.make_silent(); // also converts to mono - if !self.ended_triggered { - scope.send_ended_event(); - self.ended_triggered = true; - } + self.on_end(scope); return false; } if computed_playback_rate < 0. && buffer_time < 0. { output.make_silent(); // also converts to mono - if !self.ended_triggered { - scope.send_ended_event(); - self.ended_triggered = true; - } + self.on_end(scope); return false; } } @@ -772,11 +782,7 @@ impl AudioProcessor for AudioBufferSourceRenderer { std::mem::swap(current_buffer, buffer); } else { // Creating the tombstone buffer does not cause allocations. - let tombstone_buffer = AudioBuffer { - channels: Default::default(), - sample_rate: Default::default(), - }; - self.buffer = Some(std::mem::replace(buffer, tombstone_buffer)); + self.buffer = Some(std::mem::replace(buffer, AudioBuffer::tombstone())); } return; }; diff --git a/src/render/processor.rs b/src/render/processor.rs index 64d1d321..b92431db 100644 --- a/src/render/processor.rs +++ b/src/render/processor.rs @@ -8,6 +8,7 @@ use super::{graph::Node, AudioRenderQuantum}; use crossbeam_channel::Sender; use rustc_hash::FxHashMap; use std::cell::{Cell, RefCell}; +use std::rc::Rc; use std::any::Any; use std::ops::Deref; @@ -25,6 +26,7 @@ pub struct RenderScope { pub(crate) node_id: Cell, pub(crate) event_sender: Option>, + pub(crate) garbage_collector: Rc>>>, } impl RenderScope { @@ -62,6 +64,11 @@ impl RenderScope { let _ = sender.try_send(EventDispatch::processor_error(self.node_id.get(), event)); } } + + #[allow(dead_code)] + pub(crate) fn deallocate_async(&self, value: llq::Node>) { + self.garbage_collector.borrow_mut().push(value); + } } /// Interface for audio processing code that runs on the audio rendering thread. diff --git a/src/render/thread.rs b/src/render/thread.rs index cd97efc2..a3fe363f 100644 --- a/src/render/thread.rs +++ b/src/render/thread.rs @@ -1,7 +1,8 @@ //! Communicates with the control thread and ships audio samples to the hardware use std::any::Any; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -32,7 +33,7 @@ pub(crate) struct RenderThread { buffer_offset: Option<(usize, AudioRenderQuantum)>, load_value_sender: Option>, event_sender: Option>, - garbage_collector: llq::Producer>, + garbage_collector: Rc>>>, } // SAFETY: @@ -66,7 +67,7 @@ impl RenderThread { buffer_offset: None, load_value_sender, event_sender, - garbage_collector: gc_producer, + garbage_collector: Rc::new(RefCell::new(gc_producer)), } } @@ -129,7 +130,7 @@ impl RenderThread { } NodeMessage { id, mut msg } => { self.graph.as_mut().unwrap().route_message(id, msg.as_mut()); - self.garbage_collector.push(msg); + self.garbage_collector.borrow_mut().push(msg); } } } @@ -166,6 +167,7 @@ impl RenderThread { sample_rate: self.sample_rate, event_sender: self.event_sender.clone(), node_id: Cell::new(AudioNodeId(0)), // placeholder value + garbage_collector: Rc::clone(&self.garbage_collector), }; // render audio graph @@ -264,6 +266,7 @@ impl RenderThread { sample_rate: self.sample_rate, event_sender: self.event_sender.clone(), node_id: Cell::new(AudioNodeId(0)), // placeholder value + garbage_collector: Rc::clone(&self.garbage_collector), }; // render audio graph, clone it in case we need to mutate/store the value later @@ -302,6 +305,7 @@ impl RenderThread { impl Drop for RenderThread { fn drop(&mut self) { self.garbage_collector + .borrow_mut() .push(llq::Node::new(Box::new(TerminateGarbageCollectorThread))); log::info!("Audio render thread has been dropped"); } @@ -320,7 +324,7 @@ fn spawn_garbage_collector_thread(consumer: llq::Consumer>) } fn run_garbage_collector_thread(mut consumer: llq::Consumer>) { - log::info!("Entering garbage collector thread"); + log::debug!("Entering garbage collector thread"); loop { if let Some(node) = consumer.pop() { if node