diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f22d634..43407f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - name: Check no_std build run: | - cargo build --locked --no-default-features --features "libm, scheduled_events, musical_transport, all_nodes_no_std, pool, node_profiling, glam-29, glam-30, glam-31" + cargo build --locked --no-default-features --features "libm, scheduled_events, musical_transport, all_nodes_no_std, node_profiling, glam-29, glam-30, glam-31" # Check formatting. format: diff --git a/Cargo.toml b/Cargo.toml index b5b83453..3cbe8bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ std = [ "firewheel-core/std", "firewheel-graph/std", "firewheel-nodes/std", - "firewheel-pool?/std", ] # Enable this if "std" is disabled. libm = ["firewheel-core/libm", "firewheel-nodes/libm"] @@ -53,7 +52,6 @@ scheduled_events = [ "firewheel-core/scheduled_events", "firewheel-graph/scheduled_events", "firewheel-nodes/scheduled_events", - "firewheel-pool?/scheduled_events", ] # Enables the musical transport feature musical_transport = [ @@ -76,9 +74,6 @@ rtaudio = ["std", "firewheel-rtaudio"] symphonium = ["dep:firewheel-symphonium"] # Enables performance profiling for each individual node. node_profiling = ["firewheel-graph/node_profiling"] -# Enables the `AudioNodePool` helper type for constructing a pool of -# audio node chains that can dynamically be assigned work. -pool = ["dep:firewheel-pool"] # Enables all built-in factory nodes all_nodes = ["firewheel-nodes/all_nodes"] # Enables all built-in factory nodes which are no_std compatible @@ -88,11 +83,10 @@ beep_test_node = ["firewheel-nodes/beep_test"] # Enables the peak meter node peak_meter_node = ["firewheel-nodes/peak_meter"] # Enables the sampler node -sampler_node = ["firewheel-nodes/sampler", "firewheel-pool?/sampler"] +sampler_node = ["firewheel-nodes/sampler"] # Enables the basic 3D spatial positioning node spatial_basic_node = [ "firewheel-nodes/spatial_basic", - "firewheel-pool?/spatial_basic", ] # Enables the triple buffer node for sending raw audio data from the # audio graph to another thread. Useful for cases where you only care @@ -171,7 +165,6 @@ members = [ "crates/firewheel-graph", "crates/firewheel-nodes", "crates/firewheel-macros", - "crates/firewheel-pool", "crates/firewheel-rtaudio", "crates/firewheel-symphonium", "examples/beep_test", @@ -179,7 +172,6 @@ members = [ "examples/custom_nodes", "examples/play_sample", "examples/rtaudio_beep_test", - "examples/sampler_pool", "examples/sampler_test", "examples/spatial_basic", "examples/stream_nodes", @@ -197,7 +189,6 @@ ringbuf = { version = "0.4", default-features = false, features = [ "alloc", ] } triple_buffer = "9" -triple_buf_64 = { version = "0.1.1", features = ["portable-atomic"] } thiserror = { version = "2", default-features = false } smallvec = "1" arrayvec = { version = "0.7", default-features = false } @@ -227,11 +218,10 @@ eframe = { version = "0.33.3", default-features = false, features = [ ] } [dependencies] -firewheel-core = { path = "crates/firewheel-core", version = "0.10.0", default-features = false } -firewheel-graph = { path = "crates/firewheel-graph", version = "0.10.0", default-features = false } +firewheel-core = { path = "crates/firewheel-core", version = "0.10.1", default-features = false } +firewheel-graph = { path = "crates/firewheel-graph", version = "0.10.2", default-features = false } firewheel-cpal = { path = "crates/firewheel-cpal", version = "0.10.0", default-features = false, optional = true } firewheel-nodes = { path = "crates/firewheel-nodes", version = "0.10.0", default-features = false } -firewheel-pool = { path = "crates/firewheel-pool", version = "0.10.0", default-features = false, optional = true } firewheel-symphonium = { path = "crates/firewheel-symphonium", version = "0.10.0", default-features = false, optional = true } firewheel-rtaudio = { path = "crates/firewheel-rtaudio", version = "0.10.0", default-features = false, optional = true } thunderdome = { workspace = true, optional = true } diff --git a/crates/firewheel-core/src/diff/leaf.rs b/crates/firewheel-core/src/diff/leaf.rs index 26516c2f..73cd7046 100644 --- a/crates/firewheel-core/src/diff/leaf.rs +++ b/crates/firewheel-core/src/diff/leaf.rs @@ -4,7 +4,7 @@ use super::{Diff, EventQueue, Patch, PatchError, PathBuilder}; use crate::{ clock::{DurationSamples, DurationSeconds, InstantSamples, InstantSeconds}, collector::ArcGc, - diff::{Notify, RealtimeClone}, + diff::{Notify, RealtimeClone, notify::NotifyID}, dsp::volume::Volume, event::{NodeEventType, ParamData}, vector::{Vec2, Vec3}, @@ -221,7 +221,7 @@ impl Patch for Option { impl Diff for Notify<()> { fn diff(&self, baseline: &Self, path: PathBuilder, event_queue: &mut E) { if self != baseline { - event_queue.push_param(ParamData::U64(self.id()), path); + event_queue.push_param(ParamData::U64(self.id().0), path); } } } @@ -231,7 +231,7 @@ impl Patch for Notify<()> { fn patch(data: &ParamData, _: &[u32]) -> Result { match data { - ParamData::U64(counter) => Ok(Notify::from_raw((), *counter)), + ParamData::U64(id) => Ok(Notify::from_raw((), NotifyID(*id))), _ => Err(PatchError::InvalidData), } } @@ -245,8 +245,8 @@ impl Diff for Notify { fn diff(&self, baseline: &Self, path: PathBuilder, event_queue: &mut E) { if self != baseline { let mut bytes: [u8; 20] = [0; 20]; - bytes[0..8].copy_from_slice(&self.id().to_ne_bytes()); - bytes[8] = (**self) as u8; + bytes[0..size_of::()].copy_from_slice(&self.id().0.to_ne_bytes()); + bytes[size_of::()] = if **self { 1 } else { 0 }; event_queue.push_param(ParamData::CustomBytes(bytes), path); } @@ -259,12 +259,12 @@ impl Patch for Notify { fn patch(data: &ParamData, _path: &[u32]) -> Result { match data { ParamData::CustomBytes(bytes) => { - let (counter_bytes, rest_bytes) = bytes.split_at(size_of::()); - let counter = u64::from_ne_bytes(counter_bytes.try_into().unwrap()); + let (id_bytes, rest_bytes) = bytes.split_at(size_of::()); + let id = u64::from_ne_bytes(id_bytes.try_into().unwrap()); let value = rest_bytes[0] != 0; - Ok(Notify::from_raw(value, counter)) + Ok(Notify::from_raw(value, NotifyID(id))) } _ => Err(PatchError::InvalidData), } @@ -281,7 +281,7 @@ macro_rules! trivial_notify { fn diff(&self, baseline: &Self, path: PathBuilder, event_queue: &mut E) { if self != baseline { let mut bytes: [u8; 20] = [0; 20]; - bytes[0..8].copy_from_slice(&self.id().to_ne_bytes()); + bytes[0..8].copy_from_slice(&self.id().0.to_ne_bytes()); let value_bytes = self.to_ne_bytes(); bytes[8..8 + value_bytes.len()].copy_from_slice(&value_bytes); @@ -296,13 +296,13 @@ macro_rules! trivial_notify { fn patch(data: &ParamData, _path: &[u32]) -> Result { match data { ParamData::CustomBytes(bytes) => { - let (counter_bytes, rest_bytes) = bytes.split_at(size_of::()); - let counter = u64::from_ne_bytes(counter_bytes.try_into().unwrap()); + let (id_bytes, rest_bytes) = bytes.split_at(size_of::()); + let id = u64::from_ne_bytes(id_bytes.try_into().unwrap()); let (value_bytes, _) = rest_bytes.split_at(size_of::<$ty>()); let value = <$ty>::from_ne_bytes(value_bytes.try_into().unwrap()); - Ok(Notify::from_raw(value, counter)) + Ok(Notify::from_raw(value, NotifyID(id))) } _ => Err(PatchError::InvalidData), } diff --git a/crates/firewheel-core/src/diff/mod.rs b/crates/firewheel-core/src/diff/mod.rs index 0c974004..b5f63de1 100644 --- a/crates/firewheel-core/src/diff/mod.rs +++ b/crates/firewheel-core/src/diff/mod.rs @@ -219,7 +219,7 @@ mod memo; mod notify; pub use memo::Memo; -pub use notify::Notify; +pub use notify::{Notify, NotifyID}; /// Derive macros for diffing and patching. pub use firewheel_macros::{Diff, Patch, RealtimeClone}; diff --git a/crates/firewheel-core/src/diff/notify.rs b/crates/firewheel-core/src/diff/notify.rs index 59ae5c8b..4bfc7d95 100644 --- a/crates/firewheel-core/src/diff/notify.rs +++ b/crates/firewheel-core/src/diff/notify.rs @@ -8,20 +8,20 @@ use crate::{ // Increment a realtime-safe atomic counter. // // This is gauranteed to never return zero. -fn increment_counter() -> u64 { +fn increment_counter() -> NotifyID { portable_atomic::cfg_has_atomic_64! { use portable_atomic::AtomicU64; static NOTIFY_COUNTER: AtomicU64 = AtomicU64::new(1); portable_atomic::cfg_has_atomic_cas! { - NOTIFY_COUNTER.fetch_add(1, Ordering::Relaxed) + NotifyID(NOTIFY_COUNTER.fetch_add(1, Ordering::Relaxed)) } portable_atomic::cfg_no_atomic_cas! { let val = NOTIFY_COUNTER.load(Ordering::Relaxed) + 1; NOTIFY_COUNTER.store(val, Ordering::Relaxed); - val + NotifyID(val) } } @@ -44,7 +44,7 @@ fn increment_counter() -> u64 { NOTIFY_COUNTER_0.store((val & (u32::MAX as u64)) as u32, Ordering::Relaxed); NOTIFY_COUNTER_1.store((val >> 32) as u32, Ordering::Relaxed); - val + NotifyID(val) } portable_atomic::cfg_no_atomic_32! { @@ -52,11 +52,29 @@ fn increment_counter() -> u64 { // Just accept the locking behavior for these esoteric platforms. static NOTIFY_COUNTER: AtomicU64 = AtomicU64::new(1); - NOTIFY_COUNTER.fetch_add(1, Ordering::Relaxed) + NotifyID(NOTIFY_COUNTER.fetch_add(1, Ordering::Relaxed)) } } } +/// An identifier representing the "generation" of a [`Notify`] parameter. +/// +/// Whenever a `Notify` parameter is mutated, it will be assigned a new [`NotifyID`]. +/// For all practical purposes, the ID can be considered unique among all [`Notify`] +/// instances. +/// +/// Valid (non-dangling) [`NotifyID`]s are guaranteed to never be 0, so it can be +/// used as a sentinel value. +#[repr(transparent)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(opaque))] +pub struct NotifyID(pub u64); + +impl NotifyID { + pub const DANGLING: Self = Self(0); +} + /// A lightweight wrapper that guarantees an event /// will be generated every time the inner value is accessed mutably, /// even if the value doesn't change. @@ -70,7 +88,7 @@ fn increment_counter() -> u64 { #[derive(Debug, Clone)] pub struct Notify { value: T, - counter: u64, + id: NotifyID, } impl Notify { @@ -92,25 +110,25 @@ impl Notify { pub fn new(value: T) -> Self { Self { value, - counter: increment_counter(), + id: increment_counter(), } } - pub(crate) fn from_raw(value: T, counter: u64) -> Self { - Self { value, counter } + pub(crate) fn from_raw(value: T, id: NotifyID) -> Self { + Self { value, id } } - /// Get this instance's unique ID. + /// An identifier representing the "generation" of this [`Notify`] parameter. /// - /// After each mutable dereference, this ID will be replaced - /// with a new, unique value. For all practical purposes, - /// the ID can be considered unique among all [`Notify`] instances. + /// Whenever this parameter is mutated, it will be assigned a new [`NotifyID`]. + /// For all practical purposes, the ID can be considered unique among all [`Notify`] + /// instances. /// - /// [`Notify`] IDs are guaranteed to never be 0, so it can be + /// Valid (non-dangling) [`NotifyID`]s are guaranteed to never be 0, so it can be /// used as a sentinel value. #[inline(always)] - pub fn id(&self) -> u64 { - self.counter + pub fn id(&self) -> NotifyID { + self.id } /// Get mutable access to the inner value without updating the ID. @@ -120,7 +138,7 @@ impl Notify { /// Manually update the internal ID without modifying the internals. pub fn notify(&mut self) { - self.counter = increment_counter(); + self.id = increment_counter(); } } @@ -132,7 +150,7 @@ impl AsRef for Notify { impl AsMut for Notify { fn as_mut(&mut self) -> &mut T { - self.counter += 1; + self.id = increment_counter(); &mut self.value } @@ -154,7 +172,7 @@ impl core::ops::Deref for Notify { impl core::ops::DerefMut for Notify { fn deref_mut(&mut self) -> &mut Self::Target { - self.counter += 1; + self.id = increment_counter(); &mut self.value } @@ -162,6 +180,14 @@ impl core::ops::DerefMut for Notify { impl Copy for Notify {} +impl PartialEq for Notify { + fn eq(&self, other: &Self) -> bool { + // under normal usage, it is not possible that the inner value + // can change without incrementing the counter + self.id == other.id + } +} + impl Diff for Notify { fn diff( &self, @@ -169,7 +195,7 @@ impl Diff for Notify { path: super::PathBuilder, event_queue: &mut E, ) { - if self.counter != baseline.counter { + if self.id != baseline.id { event_queue.push_param(ParamData::any(self.clone()), path); } } @@ -189,14 +215,6 @@ impl Patch for Notify { } } -impl PartialEq for Notify { - fn eq(&self, other: &Self) -> bool { - // under normal usage, it is not possible that the inner value - // can change without incrementing the counter - self.counter == other.counter - } -} - #[cfg(test)] mod test { use crate::diff::PathBuilder; diff --git a/crates/firewheel-core/src/log.rs b/crates/firewheel-core/src/log.rs index c7808a39..c6b46c91 100644 --- a/crates/firewheel-core/src/log.rs +++ b/crates/firewheel-core/src/log.rs @@ -1,11 +1,10 @@ +use bevy_platform::sync::Arc; use core::sync::atomic::{AtomicBool, Ordering}; use ringbuf::traits::{Consumer, Observer, Producer, Split}; #[cfg(not(feature = "std"))] use bevy_platform::prelude::String; -use crate::collector::ArcGc; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -58,7 +57,7 @@ pub fn realtime_logger(config: RealtimeLoggerConfig) -> (RealtimeLogger, Realtim error_prod_1.try_push(slot).unwrap(); } - let shared_state = ArcGc::new(SharedState { + let shared_state = Arc::new(SharedState { message_too_long_occurred: AtomicBool::new(false), not_enough_slots_occurred: AtomicBool::new(false), }); @@ -71,7 +70,7 @@ pub fn realtime_logger(config: RealtimeLoggerConfig) -> (RealtimeLogger, Realtim debug_cons: debug_cons_1, error_prod: error_prod_2, error_cons: error_cons_1, - shared_state: ArcGc::clone(&shared_state), + shared_state: Arc::clone(&shared_state), max_msg_length: config.max_message_length, }, RealtimeLoggerMainThread { @@ -101,7 +100,7 @@ pub struct RealtimeLogger { error_prod: ringbuf::HeapProd, error_cons: ringbuf::HeapCons, - shared_state: ArcGc, + shared_state: Arc, max_msg_length: usize, } @@ -246,7 +245,7 @@ pub struct RealtimeLoggerMainThread { error_prod: ringbuf::HeapProd, error_cons: ringbuf::HeapCons, - shared_state: ArcGc, + shared_state: Arc, } impl RealtimeLoggerMainThread { diff --git a/crates/firewheel-core/src/node.rs b/crates/firewheel-core/src/node.rs index 7e581a2d..a8f92cc6 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -257,11 +257,12 @@ pub struct AudioNodeInfoInner { /// processor back to the audio thread for processing. /// /// 7c. If the Firewheel context is dropped before a new stream is started, then -/// both the node and the processor counterpart are dropped. +/// both the node and the processor counterpart are dropped on the main thread. /// 8. (Audio thread crashes or stops unexpectedly) - The node's processor counterpart /// may or may not be dropped. The user may try to create a new audio stream, in which /// case [`AudioNode::construct_processor`] might be called again. If a second processor -/// instance is not able to be created, then the node may panic. +/// instance is not able to be created, or if dropping the processor on the audio thread +/// is unacceptable behavior, then the node may panic. pub trait AudioNode { /// A type representing this constructor's configuration. /// @@ -272,7 +273,8 @@ pub trait AudioNode { /// Get information about this node. /// - /// This method is only called once after the node is added to the audio graph. + /// This method is only called once per instance after the node is added to the + /// audio graph. fn info(&self, configuration: &Self::Configuration) -> Result; /// Construct a realtime processor for this node. diff --git a/crates/firewheel-graph/Cargo.toml b/crates/firewheel-graph/Cargo.toml index 1d14dabf..3da15d76 100644 --- a/crates/firewheel-graph/Cargo.toml +++ b/crates/firewheel-graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "firewheel-graph" -version = "0.10.1" +version = "0.10.2" description = "Core audio graph algorithm and executor for Firewheel" homepage = "https://github.com/BillyDM/firewheel/blob/main/crates/firewheel-graph" repository.workspace = true diff --git a/crates/firewheel-graph/src/context.rs b/crates/firewheel-graph/src/context.rs index 9cf1bac1..2f2f782a 100644 --- a/crates/firewheel-graph/src/context.rs +++ b/crates/firewheel-graph/src/context.rs @@ -1421,7 +1421,7 @@ impl Drop for FirewheelContext { /// # } /// ``` pub struct ContextQueue<'a> { - context: &'a mut FirewheelContext, + pub context: &'a mut FirewheelContext, id: NodeID, #[cfg(feature = "scheduled_events")] time: Option, diff --git a/crates/firewheel-graph/src/processor/profiling.rs b/crates/firewheel-graph/src/processor/profiling.rs index a22adfe3..c5e32354 100644 --- a/crates/firewheel-graph/src/processor/profiling.rs +++ b/crates/firewheel-graph/src/processor/profiling.rs @@ -1,6 +1,6 @@ use bevy_platform::time::Instant; -#[cfg(not(feature = "std"))] +#[cfg(all(feature = "node_profiling", not(feature = "std")))] use bevy_platform::prelude::Vec; use crate::context::FirewheelBitFlags; diff --git a/crates/firewheel-nodes/Cargo.toml b/crates/firewheel-nodes/Cargo.toml index 3ffb96a1..60820e3b 100644 --- a/crates/firewheel-nodes/Cargo.toml +++ b/crates/firewheel-nodes/Cargo.toml @@ -64,7 +64,7 @@ beep_test = [] # Enables the peak meter node peak_meter = [] # Enables the sampler node -sampler = ["dep:smallvec", "dep:triple_buf_64"] +sampler = ["dep:smallvec", "dep:triple_buffer"] # Enables the basic 3D spatial positioning node spatial_basic = [] # Enables FastLowpassNode, FastHighpassNode, and FastBandpassNode @@ -112,5 +112,4 @@ bevy_reflect = { workspace = true, optional = true } serde = { workspace = true, optional = true } fft-convolver = { version = "0.3.0", optional = true } triple_buffer = { workspace = true, optional = true } -triple_buf_64 = { workspace = true, optional = true } thiserror.workspace = true diff --git a/crates/firewheel-nodes/src/fast_rms.rs b/crates/firewheel-nodes/src/fast_rms.rs index 1305c644..f54e9716 100644 --- a/crates/firewheel-nodes/src/fast_rms.rs +++ b/crates/firewheel-nodes/src/fast_rms.rs @@ -1,9 +1,11 @@ -use bevy_platform::sync::atomic::{AtomicU32, Ordering}; +use bevy_platform::sync::{ + Arc, + atomic::{AtomicU32, Ordering}, +}; use firewheel_core::{ StreamInfo, atomic_float::AtomicF32, channel_config::{ChannelConfig, ChannelCount}, - collector::ArcGc, diff::{Diff, Patch}, dsp::volume::amp_to_db, event::ProcEvents, @@ -48,13 +50,13 @@ impl Default for FastRmsNode { /// The state of a [`FastRmsNode`]. This contains the calculated RMS values. #[derive(Clone)] pub struct FastRmsState { - shared_state: ArcGc, + shared_state: Arc, } impl FastRmsState { fn new() -> Self { Self { - shared_state: ArcGc::new(SharedState { + shared_state: Arc::new(SharedState { rms_value: AtomicF32::new(0.0), read_count: AtomicU32::new(1), }), @@ -110,7 +112,7 @@ impl AudioNode for FastRmsNode { Ok(Processor { params: *self, - shared_state: ArcGc::clone(&custom_state.shared_state), + shared_state: Arc::clone(&custom_state.shared_state), squares: 0.0, num_squared_values: 0, window_frames, @@ -121,7 +123,7 @@ impl AudioNode for FastRmsNode { struct Processor { params: FastRmsNode, - shared_state: ArcGc, + shared_state: Arc, squares: f32, num_squared_values: usize, window_frames: usize, diff --git a/crates/firewheel-nodes/src/peak_meter.rs b/crates/firewheel-nodes/src/peak_meter.rs index a3e5232c..ca633b0f 100644 --- a/crates/firewheel-nodes/src/peak_meter.rs +++ b/crates/firewheel-nodes/src/peak_meter.rs @@ -1,12 +1,11 @@ #[cfg(not(feature = "std"))] use num_traits::Float; -use bevy_platform::sync::atomic::Ordering; +use bevy_platform::sync::{Arc, atomic::Ordering}; use firewheel_core::node::NodeError; use firewheel_core::{ atomic_float::AtomicF32, channel_config::{ChannelConfig, ChannelCount}, - collector::ArcGc, diff::{Diff, Patch}, dsp::volume::{DbMeterNormalizer, amp_to_db}, event::ProcEvents, @@ -181,7 +180,7 @@ pub type PeakMeterStereoState = PeakMeterState<2>; /// The state of a [`PeakMeterNode`]. This contains the calculated peak values. #[derive(Clone)] pub struct PeakMeterState { - shared_state: ArcGc>, + shared_state: Arc>, } impl PeakMeterState { @@ -190,7 +189,7 @@ impl PeakMeterState { assert!(NUM_CHANNELS <= 64); Self { - shared_state: ArcGc::new(SharedState { + shared_state: Arc::new(SharedState { peak_gains: core::array::from_fn(|_| AtomicF32::new(0.0)), }), } @@ -232,7 +231,7 @@ impl AudioNode for PeakMeterNode { ) -> Result { Ok(Processor { params: *self, - shared_state: ArcGc::clone( + shared_state: Arc::clone( &cx.custom_state::>() .unwrap() .shared_state, @@ -247,7 +246,7 @@ struct SharedState { struct Processor { params: PeakMeterNode, - shared_state: ArcGc>, + shared_state: Arc>, } impl Processor { diff --git a/crates/firewheel-nodes/src/sampler.rs b/crates/firewheel-nodes/src/sampler.rs index 1bb38a1d..0ad16888 100644 --- a/crates/firewheel-nodes/src/sampler.rs +++ b/crates/firewheel-nodes/src/sampler.rs @@ -18,8 +18,9 @@ use core::{ num::{NonZeroU32, NonZeroUsize}, ops::Range, }; -use firewheel_core::diff::{EventQueue, PatchError, PathBuilder, RealtimeClone}; +use firewheel_core::diff::{EventQueue, NotifyID, PatchError, PathBuilder, RealtimeClone}; use smallvec::SmallVec; +use triple_buffer::{Input, Output}; #[cfg(not(feature = "std"))] use bevy_platform::prelude::Box; @@ -55,14 +56,12 @@ pub const MIN_PLAYBACK_SPEED: f64 = 0.0000001; mod resampler; mod resource; -mod shared_state; pub use self::resource::{SamplerNodeResource, StreamedSample}; -use self::shared_state::{ - SharedChannelAudioThread, SharedChannelMainThread, SharedPlaybackState, SharedState, -}; -use resampler::Resampler; +use self::resampler::Resampler; + +pub type PlaybackID = NotifyID; /// The configuration of a [`SamplerNode`] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -231,8 +230,8 @@ impl SamplerNode { // Diff for Notify is defined here: // https://github.com/BillyDM/Firewheel/blob/380806ce61b3a417eb676a4fd8640da49905ec23/crates/firewheel-core/src/diff/leaf.rs#L247 let mut bytes: [u8; 20] = [0; 20]; - bytes[0..8].copy_from_slice(&self.play.id().to_ne_bytes()); - bytes[8] = *self.play as u8; + bytes[0..core::mem::size_of::()].copy_from_slice(&self.play.id().0.to_ne_bytes()); + bytes[core::mem::size_of::()] = if *self.play { 1 } else { 0 }; NodeEventType::Param { // TODO: This is not how `Patch` for `Notify` is implemented. @@ -241,6 +240,11 @@ impl SamplerNode { } } + /// Returns the current playback ID. + pub fn playback_id(&self) -> PlaybackID { + self.play.id() + } + /// Returns an event type to sync the `play_from` parameter. pub fn sync_play_from_event(&self) -> NodeEventType { NodeEventType::Param { @@ -346,29 +350,33 @@ impl SamplerNode { #[derive(Clone)] pub struct SamplerState { - shared_channel: Option>>, - shared_state: Arc, + channel: Arc>, } impl SamplerState { fn new() -> Self { Self { - shared_channel: None, - shared_state: Arc::new(SharedState::default()), + channel: Arc::new(Mutex::new(SharedChannel::new())), } } + /// Get the current state of this sampler node's processor at this instant + /// in time. + pub fn current_processor_state(&self) -> CurrentProcessorState { + *self.channel.lock().unwrap().proc_state_output.read() + } + /// Get the current position of the playhead in units of frames (samples of /// a single channel of audio). pub fn playhead_frames(&self) -> DurationSamples { - self.shared_channel - .as_ref() - .and_then(|s| { - s.lock() - .map(|mut s| DurationSamples(s.sample_playhead_frames() as i64)) - .ok() - }) - .unwrap_or(DurationSamples::ZERO) + DurationSamples( + self.channel + .lock() + .unwrap() + .proc_state_output + .read() + .playhead_frames as i64, + ) } /// Get the current position of the sample playhead in seconds. @@ -378,6 +386,51 @@ impl SamplerState { DurationSeconds(self.playhead_frames().0 as f64 / sample_rate.get() as f64) } + /// Get the current playback state of the processor at this instant in time. + pub fn playback_state(&self) -> PlaybackState { + self.channel + .lock() + .unwrap() + .proc_state_output + .read() + .playback_state + } + + /// Get the current playback state as well as the current [`PlaybackID`] at this + /// instant in time. + /// + /// The [`PlaybackID`] is equal to the ID of the latest [`SamplerNode::play`] + /// parameter that was set to `true`. + pub fn playback_state_and_id(&self) -> (PlaybackState, PlaybackID) { + let mut ch_guard = self.channel.lock().unwrap(); + let state = ch_guard.proc_state_output.read(); + (state.playback_state, state.playback_id) + } + + /// Returns `true` if the current [`PlaybackID`] is equal to the given playback ID + /// *and* the playback state is [`PlaybackState::Stopped`]. + pub fn playback_finished(&self, playback_id: PlaybackID) -> bool { + let (playback_state, id) = self.playback_state_and_id(); + id == playback_id && playback_state == PlaybackState::Stopped + } + + /// Returns `true` if the processor is currently playing a sample at this instant + /// in time. + pub fn currently_playing(&self) -> bool { + self.playback_state() == PlaybackState::Playing + } + + /// Returns `true` if the processor is currently paused at this instant in time. + pub fn currently_paused(&self) -> bool { + self.playback_state() == PlaybackState::Paused + } + + /// Returns `true` if the the processor has either not started playing a sample yet + /// or it has finished playing its sample at this instant in time. + pub fn currently_stopped(&self) -> bool { + self.playback_state() == PlaybackState::Stopped + } + /// Get the current position of the playhead in units of frames (samples of /// a single channel of audio), corrected with the delay between when the audio clock /// was last updated and now. @@ -389,21 +442,25 @@ impl SamplerState { update_instant: Option, sample_rate: NonZeroU32, ) -> DurationSamples { - let frames = self.playhead_frames(); + let (playhead_frames, playback_state) = { + let mut channel = self.channel.lock().unwrap(); + let s = channel.proc_state_output.read(); + (s.playhead_frames, s.playback_state) + }; let Some(update_instant) = update_instant else { - return frames; + return DurationSamples(playhead_frames as i64); }; - if self.shared_state.playback_state() == SharedPlaybackState::Playing { + if playback_state == PlaybackState::Playing { DurationSamples( - frames.0 + playhead_frames as i64 + InstantSeconds(update_instant.elapsed().as_secs_f64()) .to_samples(sample_rate) .0, ) } else { - frames + DurationSamples(playhead_frames as i64) } } @@ -423,95 +480,55 @@ impl SamplerState { / sample_rate.get() as f64, ) } +} - /// Returns `true` if the processor currently has a sample resource. - pub fn has_sample_resource(&self) -> bool { - self.shared_state.has_sample_resource() - } - - /// Returns `true` if the sample is currently playing. - pub fn playing(&self) -> bool { - self.shared_state.playback_state() == SharedPlaybackState::Playing - } - - /// Returns `true` if the sample is currently paused. - pub fn paused(&self) -> bool { - self.shared_state.playback_state() == SharedPlaybackState::Paused - } - - /// Returns `true` if the sample has either not started playing yet or has finished - /// playing. - pub fn stopped(&self) -> bool { - self.shared_state.playback_state() == SharedPlaybackState::Stopped - } - - /// Manually set the shared `playing` flag. This can be useful to account for the delay - /// between sending a play event and the node's processor receiving that event. - pub fn mark_playing(&self) { - self.shared_state - .set_playback_state(SharedPlaybackState::Playing); - } - - /// Manually set the shared `paused` flag. This can be useful to account for the delay - /// between sending a play event and the node's processor receiving that event. - pub fn mark_paused(&self) { - self.shared_state - .set_playback_state(SharedPlaybackState::Paused); - } - - /// Manually set the shared `stopped` flag. This can be useful to account for the delay - /// between sending a play event and the node's processor receiving that event. - pub fn mark_stopped(&self) { - self.shared_state - .set_playback_state(SharedPlaybackState::Stopped); - } - - /// Returns the ID stored in the "finished" flag. - pub fn finished(&self) -> u64 { - self.shared_channel - .as_ref() - .and_then(|s| s.lock().map(|mut s| s.finished().unwrap_or(0)).ok()) - .unwrap_or(0) - } +struct SharedChannel { + proc_state_output: Output, + proc_state_input: Option>, +} - /// Clears the "finished" flag. - pub fn clear_finished(&self) { - self.shared_state.clear_finished(); - } +impl SharedChannel { + fn new() -> Self { + let (proc_state_input, proc_state_output) = triple_buffer::triple_buffer::< + CurrentProcessorState, + >(&CurrentProcessorState::default()); - /// A score of how suitable this node is to start new work (Play a new sample). The - /// higher the score, the better the candidate. - pub fn worker_score(&self, params: &SamplerNode) -> u64 { - if !self.has_sample_resource() { - return u64::MAX; + Self { + proc_state_input: Some(proc_state_input), + proc_state_output, } + } +} - let playback_state = self.shared_state.playback_state(); - - if *params.play { - let playhead_frames = self.playhead_frames(); +/// The current state of a [`SamplerNode`]'s processor. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CurrentProcessorState { + /// The current position of the playhead in frames (samples in a single + /// channel of audio). + pub playhead_frames: u64, + /// The current [`PlaybackID`]. This is equal to the ID of the latest + /// [`SamplerNode::play`] parameter that was set to `true`. + pub playback_id: PlaybackID, + /// The current playback state. + pub playback_state: PlaybackState, + /// The age of the current playback in frames (samples in a single channel + /// of audio). + pub playback_age_frames: u64, + /// Whether or not the processor currently has a sample resource. + pub has_sample_resource: bool, +} - if playback_state == SharedPlaybackState::Stopped { - if playhead_frames.0 > 0 { - // Sequence has likely finished playing. - u64::MAX - 4 - } else { - // Sequence has likely not started playing yet. - u64::MAX - 5 - } - } else { - // The older the sample is, the better it is as a candidate to steal - // work from. - playhead_frames.0 as u64 - } - } else { - match playback_state { - SharedPlaybackState::Stopped => u64::MAX - 1, - SharedPlaybackState::Paused => u64::MAX - 2, - SharedPlaybackState::Playing => u64::MAX - 3, - } - } - } +/// The current playback state of a [`SamplerNode`]'s processor. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PlaybackState { + #[default] + /// The processor has either not started playing a sample yet or it has finished + /// playing its sample. + Stopped, + /// The processor is currently paused. + Paused, + /// The processor is currently playing a sample. + Playing, } /// Defines where the sampler should start playing from when @@ -646,15 +663,52 @@ impl AudioNode for SamplerNode { )) }; - let custom_state = cx.custom_state_mut::().unwrap(); - let (channel_main_thread, channel_audio_thread) = - self::shared_state::shared_channel(Arc::clone(&custom_state.shared_state)); - custom_state.shared_channel = Some(Arc::new(Mutex::new(channel_main_thread))); + let max_block_frames = cx.stream_info.max_block_frames.get() as usize; + + let playing = *self.play; + let paused = !*self.play && self.play_from == PlayFrom::Resume; + let playback_state = if playing { + PlaybackState::Playing + } else if paused { + PlaybackState::Paused + } else { + PlaybackState::Stopped + }; + let playback_id = if playing { + self.play.id() + } else { + PlaybackID::DANGLING + }; + + let proc_state = CurrentProcessorState { + playback_id, + playback_state, + playhead_frames: self + .play_from + .as_frames(cx.stream_info.sample_rate) + .unwrap_or_default(), + ..Default::default() + }; + let mut channel = cx + .custom_state_mut::() + .unwrap() + .channel + .lock() + .unwrap(); + let mut shared_proc_state = if let Some(proc_state_input) = channel.proc_state_input.take() + { + proc_state_input + } else { + *channel = SharedChannel::new(); + channel.proc_state_input.take().unwrap() + }; + shared_proc_state.write(proc_state); Ok(SamplerProcessor { config: *config, params: *self, - shared_channel: channel_audio_thread, + proc_state, + shared_proc_state, loaded_sample_state: None, declicker: Declicker::SettledAt1, stop_declicker_buffers, @@ -662,12 +716,12 @@ impl AudioNode for SamplerNode { num_active_stop_declickers: 0, resampler: Some(Resampler::new(config.speed_quality)), speed: self.speed.max(MIN_PLAYBACK_SPEED), - playing: *self.play, - paused: !*self.play && self.play_from == PlayFrom::Resume, + playing, + paused, #[cfg(feature = "scheduled_events")] queued_playback_instant: None, min_gain: self.min_gain.max(0.0), - max_block_frames: cx.stream_info.max_block_frames.get() as usize, + max_block_frames, num_out_channels: config.channels.get().get() as usize, is_first_process: true, }) @@ -677,7 +731,8 @@ impl AudioNode for SamplerNode { struct SamplerProcessor { config: SamplerConfig, params: SamplerNode, - shared_channel: SharedChannelAudioThread, + proc_state: CurrentProcessorState, + shared_proc_state: Input, loaded_sample_state: Option, @@ -704,6 +759,10 @@ struct SamplerProcessor { } impl SamplerProcessor { + fn sync_proc_state(&mut self) { + self.shared_proc_state.write(self.proc_state); + } + /// Returns `true` if the sample has finished playing, and also /// returns the number of channels that were filled. fn process_internal( @@ -952,6 +1011,7 @@ impl AudioNodeProcessor for SamplerProcessor { let mut repeat_mode_changed = false; let mut speed_changed = false; let mut volume_changed = false; + let mut proc_state_changed = false; #[cfg(feature = "scheduled_events")] let mut playback_instant: Option = None; @@ -961,6 +1021,7 @@ impl AudioNodeProcessor for SamplerProcessor { let mut s = None; if event.downcast_swap::>(&mut s) { new_sample = Some(s); + continue; } if let Some(patch) = SamplerNode::patch_event(&event) { @@ -969,6 +1030,11 @@ impl AudioNodeProcessor for SamplerProcessor { SamplerNodePatch::Play(play) => { playback_instant = timestamp; new_playing = Some(*play); + + if *play { + self.proc_state.playback_id = play.id(); + proc_state_changed = true; + } } SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, SamplerNodePatch::Speed(_) => speed_changed = true, @@ -987,6 +1053,7 @@ impl AudioNodeProcessor for SamplerProcessor { let mut s = None; if event.downcast_swap::>(&mut s) { new_sample = Some(s); + continue; } if let Some(patch) = SamplerNode::patch_event(&event) { @@ -994,6 +1061,11 @@ impl AudioNodeProcessor for SamplerProcessor { SamplerNodePatch::Volume(_) => volume_changed = true, SamplerNodePatch::Play(play) => { new_playing = Some(*play); + + if *play { + self.proc_state.playback_id = play.id(); + proc_state_changed = true; + } } SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, SamplerNodePatch::Speed(_) => speed_changed = true, @@ -1027,9 +1099,8 @@ impl AudioNodeProcessor for SamplerProcessor { } if let Some(maybe_sample) = new_sample { - self.shared_channel - .shared_state - .set_has_sample_resource(maybe_sample.is_some()); + self.proc_state.has_sample_resource = maybe_sample.is_some(); + proc_state_changed = true; self.stop(extra); @@ -1051,6 +1122,8 @@ impl AudioNodeProcessor for SamplerProcessor { if let Some(mut new_playing) = new_playing { self.paused = false; + self.proc_state.playback_age_frames = 0; + proc_state_changed = true; if new_playing { let mut playhead_frames_at_play_instant = None; @@ -1138,15 +1211,13 @@ impl AudioNodeProcessor for SamplerProcessor { self.loaded_sample_state.as_mut().unwrap().playhead_frames = new_playhead_frames; - self.shared_channel - .set_sample_playhead_frames(new_playhead_frames); + self.proc_state.playhead_frames = new_playhead_frames; } if new_playhead_frames == self.loaded_sample_state.as_ref().unwrap().sample_len_frames { - self.shared_channel - .set_finished(Some(self.params.play.id())); + self.proc_state.playhead_frames = new_playhead_frames; new_playing = false; } else if new_playhead_frames != 0 @@ -1170,22 +1241,22 @@ impl AudioNodeProcessor for SamplerProcessor { } else { // Stop self.stop(extra); - self.shared_channel - .set_finished(Some(self.params.play.id())); } self.playing = new_playing; - } - self.shared_channel - .shared_state - .set_playback_state(if self.playing { - SharedPlaybackState::Playing + self.proc_state.playback_state = if self.playing { + PlaybackState::Playing } else if self.paused { - SharedPlaybackState::Paused + PlaybackState::Paused } else { - SharedPlaybackState::Stopped - }); + PlaybackState::Stopped + }; + } + + if proc_state_changed { + self.sync_proc_state(); + } } fn bypassed(&mut self, _bypassed: bool) { @@ -1199,16 +1270,6 @@ impl AudioNodeProcessor for SamplerProcessor { buffers: ProcBuffers, extra: &mut ProcExtra, ) -> ProcessStatus { - self.shared_channel - .shared_state - .set_playback_state(if self.playing { - SharedPlaybackState::Playing - } else if self.paused { - SharedPlaybackState::Paused - } else { - SharedPlaybackState::Stopped - }); - let currently_processing_sample = self.currently_processing_sample(); if !currently_processing_sample && self.num_active_stop_declickers == 0 { @@ -1230,19 +1291,20 @@ impl AudioNodeProcessor for SamplerProcessor { num_filled_channels = n_channels; - self.shared_channel.set_sample_playhead_frames( - self.loaded_sample_state.as_ref().unwrap().playhead_frames, - ); + self.proc_state.playhead_frames = + self.loaded_sample_state.as_ref().unwrap().playhead_frames; if finished { self.playing = false; - - self.shared_channel - .shared_state - .set_playback_state(SharedPlaybackState::Stopped); - self.shared_channel - .set_finished(Some(self.params.play.id())); + self.proc_state.playback_state = PlaybackState::Stopped; + } else { + self.proc_state.playback_age_frames = self + .proc_state + .playback_age_frames + .saturating_add(info.frames as u64); } + + self.sync_proc_state(); } for (i, out_buf) in buffers @@ -1320,10 +1382,8 @@ impl AudioNodeProcessor for SamplerProcessor { self.loaded_sample_state = None; self.playing = false; self.paused = false; - self.shared_channel - .shared_state - .set_playback_state(SharedPlaybackState::Stopped); - self.shared_channel.set_finished(None); + self.proc_state.playback_state = PlaybackState::Stopped; + self.sync_proc_state(); } } } diff --git a/crates/firewheel-nodes/src/sampler/shared_state.rs b/crates/firewheel-nodes/src/sampler/shared_state.rs deleted file mode 100644 index 105e24fd..00000000 --- a/crates/firewheel-nodes/src/sampler/shared_state.rs +++ /dev/null @@ -1,126 +0,0 @@ -use bevy_platform::sync::{ - Arc, - atomic::{AtomicBool, AtomicU32, Ordering}, -}; -use triple_buf_64::{Input64, Output64, triple_buffer_64}; - -pub(super) fn shared_channel( - shared_state: Arc, -) -> (SharedChannelMainThread, SharedChannelAudioThread) { - let (sample_playhead_frames_tx, sample_playhead_frames_rx) = triple_buffer_64(0); - let (finished_tx, finished_rx) = triple_buffer_64(0); - - shared_state.finished_cleared.store(true, Ordering::Relaxed); - shared_state.set_playback_state(SharedPlaybackState::Stopped); - - ( - SharedChannelMainThread { - sample_playhead_frames: sample_playhead_frames_rx, - finished: finished_rx, - shared_state: Arc::clone(&shared_state), - }, - SharedChannelAudioThread { - sample_playhead_frames: sample_playhead_frames_tx, - finished: finished_tx, - shared_state, - }, - ) -} - -pub(super) struct SharedChannelMainThread { - pub shared_state: Arc, - sample_playhead_frames: Output64, - finished: Output64, -} - -impl SharedChannelMainThread { - pub fn sample_playhead_frames(&mut self) -> u64 { - self.sample_playhead_frames.read() - } - - pub fn finished(&mut self) -> Option { - if self.shared_state.finished_cleared.load(Ordering::Relaxed) { - None - } else { - Some(self.finished.read()) - } - } -} - -pub(super) struct SharedChannelAudioThread { - pub shared_state: Arc, - sample_playhead_frames: Input64, - finished: Input64, -} - -impl SharedChannelAudioThread { - pub fn set_sample_playhead_frames(&mut self, frames: u64) { - self.sample_playhead_frames.write(frames); - } - - pub fn set_finished(&mut self, finished: Option) { - let (f, cleared) = finished.map(|f| (f, false)).unwrap_or((0, true)); - - self.finished.write(f); - self.shared_state - .finished_cleared - .store(cleared, Ordering::Relaxed); - } -} - -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(super) enum SharedPlaybackState { - Stopped = 0, - Paused, - Playing, -} - -impl SharedPlaybackState { - pub fn from_u32(val: u32) -> Self { - match val { - 1 => Self::Paused, - 2 => Self::Playing, - _ => Self::Stopped, - } - } -} - -pub(super) struct SharedState { - has_sample_resource: AtomicBool, - playback_state: AtomicU32, - finished_cleared: AtomicBool, -} - -impl SharedState { - pub fn has_sample_resource(&self) -> bool { - self.has_sample_resource.load(Ordering::Relaxed) - } - - pub fn set_has_sample_resource(&self, val: bool) { - self.has_sample_resource.store(val, Ordering::Relaxed) - } - - pub fn playback_state(&self) -> SharedPlaybackState { - SharedPlaybackState::from_u32(self.playback_state.load(Ordering::Relaxed)) - } - - pub fn set_playback_state(&self, playback_state: SharedPlaybackState) { - self.playback_state - .store(playback_state as u32, Ordering::Relaxed); - } - - pub fn clear_finished(&self) { - self.finished_cleared.store(true, Ordering::Relaxed); - } -} - -impl Default for SharedState { - fn default() -> Self { - Self { - has_sample_resource: AtomicBool::new(false), - playback_state: AtomicU32::new(SharedPlaybackState::Stopped as u32), - finished_cleared: AtomicBool::new(true), - } - } -} diff --git a/crates/firewheel-pool/Cargo.toml b/crates/firewheel-pool/Cargo.toml deleted file mode 100644 index 3126cb97..00000000 --- a/crates/firewheel-pool/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "firewheel-pool" -version = "0.10.0" -description = "FX chain pools for Firewheel" -homepage = "https://github.com/BillyDM/firewheel/blob/main/crates/firewheel-pool" -repository.workspace = true -edition.workspace = true -license.workspace = true -authors.workspace = true -keywords.workspace = true -categories.workspace = true -exclude.workspace = true - -# Show documentation with all features enabled on docs.rs -[package.metadata.docs.rs] -all-features = true - -[features] -default = ["std", "sampler"] -std = [ - "firewheel-core/std", - "firewheel-graph/std", - "firewheel-nodes/std", - "bevy_platform/std", -] -scheduled_events = [ - "firewheel-core/scheduled_events", - "firewheel-graph/scheduled_events", -] -sampler = ["firewheel-nodes/sampler"] -spatial_basic = ["firewheel-nodes/spatial_basic"] - -[dependencies] -firewheel-core = { path = "../firewheel-core", version = "0.10.0", default-features = false } -firewheel-graph = { path = "../firewheel-graph", version = "0.10.0", default-features = false } -firewheel-nodes = { path = "../firewheel-nodes", version = "0.10.0", default-features = false } -smallvec.workspace = true -thunderdome.workspace = true -thiserror.workspace = true -bevy_platform.workspace = true diff --git a/crates/firewheel-pool/src/lib.rs b/crates/firewheel-pool/src/lib.rs deleted file mode 100644 index f757c73a..00000000 --- a/crates/firewheel-pool/src/lib.rs +++ /dev/null @@ -1,837 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -use core::num::NonZeroUsize; - -#[cfg(not(feature = "std"))] -use bevy_platform::prelude::Vec; - -use firewheel_core::{ - channel_config::NonZeroChannelCount, - node::{AudioNode, NodeID}, -}; -use firewheel_graph::{ - ContextQueue, FirewheelContext, - error::{AddEdgeError, CompileGraphError, RemoveNodeError, UpdateError}, - graph::Edge, -}; -use smallvec::SmallVec; -use thunderdome::Arena; - -#[cfg(feature = "scheduled_events")] -use firewheel_core::clock::EventInstant; -use firewheel_core::node::NodeError; - -#[cfg(feature = "sampler")] -mod sampler; -#[cfg(feature = "sampler")] -pub use sampler::SamplerPool; - -mod volume; -mod volume_pan; -pub use volume::VolumeChain; -pub use volume_pan::VolumePanChain; - -#[cfg(feature = "spatial_basic")] -mod spatial_basic; -#[cfg(feature = "spatial_basic")] -pub use spatial_basic::SpatialBasicChain; - -#[cfg(feature = "sampler")] -pub type SamplerPoolVolumePan = AudioNodePool; -#[cfg(all(feature = "sampler", feature = "spatial_basic"))] -pub type SamplerPoolSpatialBasic = AudioNodePool; - -/// Information about the input/output nodes for an [`FxChain`] instance. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct FxChainIo { - /// The ID of the first node in this worker instance. (i.e. the sampler node) - pub first_node_id: NodeID, - /// The number of output channels in the first node. - pub first_node_out_channels: NonZeroChannelCount, - /// The ID of the node that the last node in this FX chain instance should connect - /// to. - pub dst_node_id: NodeID, - /// The number of input channels in `dst_node_id`. - pub dst_node_in_channels: NonZeroChannelCount, -} - -/// A trait describing an "FX chain" for use in an [`AudioNodePool`]. -pub trait FxChain: Default { - /// The one-time configuration for constructing a new instance of this fx chain. - /// - /// When no configuration is required, [`EmptyConfig`](firewheel_core::node::EmptyConfig) - /// should be used. - type Configuration: Default; - - /// Construct the nodes in the FX chain and connect them, returning a list of the - /// new node ids. - /// - /// * `config` - The configuration of this fx chain instance. - /// * `io` Information about the input/output nodes for this fx chain instance. - /// * `cx` - The firewheel context. - fn construct_and_connect( - &mut self, - configuration: &Self::Configuration, - io: &FxChainIo, - cx: &mut FirewheelContext, - ) -> Result, ModifyNodePoolError>; -} - -struct Worker { - first_node_params: N::AudioNode, - first_node_id: NodeID, - - fx_state: FxChainState, - - assigned_worker_id: Option, -} - -impl Worker { - fn remove_nodes( - self, - cx: &mut FirewheelContext, - removed_nodes: &mut Vec, - removed_edges: &mut Vec, - ) { - if let Ok(edges) = cx.remove_node(self.first_node_id) { - removed_nodes.push(self.first_node_id); - removed_edges.extend_from_slice(&edges); - } - - for node_id in self.fx_state.node_ids { - if let Ok(edges) = cx.remove_node(node_id) { - removed_nodes.push(node_id); - removed_edges.extend_from_slice(&edges); - } - } - } -} - -#[derive(Debug)] -pub struct FxChainState { - pub fx_chain: FX, - pub node_ids: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WorkerID(thunderdome::Index); - -impl WorkerID { - pub const DANGLING: Self = Self(thunderdome::Index::DANGLING); -} - -impl Default for WorkerID { - fn default() -> Self { - Self::DANGLING - } -} - -/// A trait describing the first node in an [`AudioNodePool`]. -pub trait PoolableNode { - /// The node parameters - type AudioNode: AudioNode + Clone + 'static; - - /// Return `true` if the given parameters signify that the sequence is stopped, - /// `false` otherwise. - fn params_stopped(params: &Self::AudioNode) -> bool; - /// Return `true` if the node state of the given node is stopped. - /// - /// Return an error if the given `node_id` is invalid. - fn node_is_stopped(node_id: NodeID, cx: &FirewheelContext) -> Result; - - /// Return a score of how ready this node is to accept new work. - /// - /// The worker with the highest worker score will be chosen for the new work. - /// - /// Return an error if the given `node_id` is invalid. - fn worker_score( - params: &Self::AudioNode, - node_id: NodeID, - cx: &mut FirewheelContext, - ) -> Result; - - /// Diff the new parameters and push the changes into the event queue. - fn diff(baseline: &Self::AudioNode, new: &Self::AudioNode, event_queue: &mut ContextQueue); - - /// Notify the node state that a sequence is playing. - /// - /// This is used to account for the delay between sending an event to the node - /// and the node receiving the event. - /// - /// Return an error if the given `node_id` is invalid. - fn mark_playing(node_id: NodeID, cx: &mut FirewheelContext) -> Result<(), PoolError>; - - /// Pause the sequence in the node parameters - fn pause(params: &mut Self::AudioNode); - /// Resume the sequence in the node parameters - fn resume(params: &mut Self::AudioNode); - /// Stop the sequence in the node parameters - fn stop(params: &mut Self::AudioNode); -} - -/// A pool of audio node chains that can dynamically be assigned work. -pub struct AudioNodePool { - workers: Vec>, - worker_ids: Arena, - num_active_workers: usize, - dst_node_id: NodeID, -} - -impl AudioNodePool -where - ::Configuration: Clone, -{ - /// Construct a new sampler pool. - /// - /// * `num_workers` - The total number of workers that can work in parallel. More workers - /// will allow more samples to be played concurrently, but will also increase processing - /// overhead. A value of `16` is a good place to start. - /// * `first_node` - The state of the first node in each FX chain instance. - /// * `first_node_config` - The configuration of the first node in each FX chain instance. - /// Set to `None` to use the default configuration. - /// * `fx_chain_config` - The configuration of each fx chain instance. Set to `None` to - /// use the default configuration. - /// * `dst_node_id` - The ID of the node that the last effect in each fx chain instance - /// will connect to. - /// * `call_update_when_done` - If `true`, then the [`FirewheelContext::update()`] will - /// be called after all new nodes have been added. This can be used to ensure that all - /// nodes activate without errors before committing the changes to the audio graph. - /// * `cx` - The firewheel context. - /// - /// If an error is returned, then the audio graph has not been modified. - pub fn new( - num_workers: NonZeroUsize, - first_node: N::AudioNode, - first_node_config: Option<::Configuration>, - fx_chain_config: Option, - dst_node_id: NodeID, - call_update_when_done: bool, - cx: &mut FirewheelContext, - ) -> Result { - let dst_node_in_channels = NonZeroChannelCount::new( - cx.node_channel_config(dst_node_id) - .ok_or(ModifyNodePoolError::DstNodeNotFound(dst_node_id))? - .num_inputs - .get(), - ) - .ok_or(ModifyNodePoolError::DstNodeNoInputs(dst_node_id))?; - - let mut workers: Vec> = Vec::with_capacity(num_workers.get()); - - cx.try_modify_graph(|cx| -> Result<(), ModifyNodePoolError> { - let fx_chain_config = fx_chain_config.unwrap_or_default(); - - for _ in 0..num_workers.get() { - let first_node_id = cx.add_node(first_node.clone(), first_node_config.clone())?; - let first_node_out_channels = NonZeroChannelCount::new( - cx.node_channel_config(first_node_id) - .unwrap() - .num_outputs - .get(), - ) - .ok_or(ModifyNodePoolError::FirstNodeNoOutput)?; - - let io = FxChainIo { - first_node_id, - first_node_out_channels, - dst_node_id, - dst_node_in_channels, - }; - let mut fx_chain = FX::default(); - let node_ids = fx_chain.construct_and_connect(&fx_chain_config, &io, cx)?; - - workers.push(Worker { - first_node_params: first_node.clone(), - first_node_id, - fx_state: FxChainState { fx_chain, node_ids }, - assigned_worker_id: None, - }); - } - - if call_update_when_done { - cx.update()?; - } - - Ok(()) - })?; - - Ok(Self { - workers, - worker_ids: Arena::with_capacity(num_workers.get()), - num_active_workers: 0, - dst_node_id, - }) - } - - pub fn num_workers(&self) -> usize { - self.workers.len() - } - - /// Queue new work to play a sequence. - /// - /// * `params` - The parameters of the first node. - /// * `time` - The instant these new parameters should take effect. If this - /// is `None`, then the parameters will take effect as soon as the node receives - /// the event. - /// * `steal` - If this is `true`, then if there are no more workers left in - /// in the pool, the oldest one will be stopped and replaced with this new - /// one. If this is `false`, then an error will be returned if no more workers - /// are left. - /// * `cx` - The Firewheel context. - /// * `first_node` - A closure to send additional events to the first node, such - /// as setting the sample resource. - /// * `fx_chain` - A closure to send events to the fx chain in this worker instance. - /// - /// This will return an error if `params.playback == PlaybackState::Stop`. - pub fn new_worker( - &mut self, - params: &N::AudioNode, - #[cfg(feature = "scheduled_events")] time: Option, - steal: bool, - cx: &mut FirewheelContext, - first_node: impl FnOnce(&mut ContextQueue), - fx_chain: impl FnOnce(&mut FxChainState, &mut FirewheelContext), - ) -> Result { - if N::params_stopped(params) { - return Err(NewWorkerError::ParameterStateIsStop); - } - - if !steal && self.num_active_workers == self.workers.len() { - return Err(NewWorkerError::NoMoreWorkers); - } - - let mut idx = 0; - let mut max_score = 0; - for (i, worker) in self.workers.iter().enumerate() { - if worker.assigned_worker_id.is_none() { - idx = i; - break; - } - - let score = - N::worker_score(&worker.first_node_params, worker.first_node_id, cx).unwrap(); - - if score == u64::MAX { - idx = i; - break; - } - - if score > max_score { - max_score = score; - idx = i; - } - } - - let worker_id = WorkerID(self.worker_ids.insert(idx)); - - let worker = &mut self.workers[idx]; - - let old_worker_id = worker.assigned_worker_id.take(); - let was_playing_sequence = if let Some(old_worker_id) = old_worker_id { - self.worker_ids.remove(old_worker_id.0); - - !(N::params_stopped(params) || N::node_is_stopped(worker.first_node_id, cx).unwrap()) - } else { - false - }; - - worker.assigned_worker_id = Some(worker_id); - self.num_active_workers += 1; - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, params, &mut event_queue); - - (first_node)(&mut event_queue); - - worker.first_node_params = params.clone(); - - N::mark_playing(worker.first_node_id, cx).unwrap(); - - (fx_chain)(&mut worker.fx_state, cx); - - Ok(NewWorkerResult { - worker_id, - old_worker_id, - first_node_id: worker.first_node_id, - was_playing_sequence, - }) - } - - /// Sync the parameters for the given worker. - /// - /// * `worker_id` - The ID of the worker - /// * `params` - The new parameter state to sync - /// * `time` - The instant these new parameters should take effect. If this - /// is `None`, then the parameters will take effect as soon as the node receives - /// the event. - /// * `cx` - The Firewheel context - /// * `first_node` - A closure to send additional events to the first node, such - /// as setting the sample resource. - /// * `fx_chain` - A closure to send events to the fx chain in this worker instance. - /// - /// If the parameters signify that the sequence is stopped, then this worker - /// will be removed and the `worker_id` will be invalidated. - /// - /// Returns `true` if a worker with the given ID exists, `false` otherwise. - pub fn sync_worker_params( - &mut self, - worker_id: WorkerID, - params: &N::AudioNode, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - first_node: impl FnOnce(&mut ContextQueue), - fx_chain: impl FnOnce(&mut FxChainState, &mut FirewheelContext), - ) -> bool { - let Some(idx) = self.worker_ids.get(worker_id.0).copied() else { - return false; - }; - - let worker = &mut self.workers[idx]; - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, params, &mut event_queue); - - (first_node)(&mut event_queue); - - worker.first_node_params = params.clone(); - - (fx_chain)(&mut worker.fx_state, cx); - - if N::params_stopped(params) { - self.worker_ids.remove(worker_id.0); - worker.assigned_worker_id = None; - self.num_active_workers -= 1; - } - - true - } - - /// Modify the list of nodes and connections in each FX chain instance. - /// - /// * `new_dst_node_id` - The ID of the new node that the last effect in each - /// fx chain instance should connect to. Set to `None` to use the previously - /// set destination node. - /// * `call_update_when_done` - If `true`, then the [`FirewheelContext::update()`] will - /// be called after all new nodes have been added. This can be used to ensure that all - /// nodes activate without errors before committing the changes to the audio graph. - /// * `cx` - The Firewheel context - /// * `f` - A closure that is called on each FX chain instance. If nodes have - /// been added or removed, then the third argument `&mut Vec` must - /// be modified with the new list of node IDs. - /// - /// If an error is returned, then the audio graph has not been modified. - pub fn modify_fx_chain( - &mut self, - new_dst_node_id: Option, - call_update_when_done: bool, - cx: &mut FirewheelContext, - mut f: impl FnMut(&FxChainIo, &mut FX, &mut Vec) -> Result<(), ModifyNodePoolError>, - ) -> Result<(), ModifyNodePoolError> { - let dst_node_id = new_dst_node_id.unwrap_or(self.dst_node_id); - let dst_node_in_channels = NonZeroChannelCount::new( - cx.node_channel_config(dst_node_id) - .ok_or(ModifyNodePoolError::DstNodeNotFound(dst_node_id))? - .num_inputs - .get(), - ) - .ok_or(ModifyNodePoolError::DstNodeNoInputs(dst_node_id))?; - - cx.try_modify_graph(|cx| -> Result<(), ModifyNodePoolError> { - for worker in self.workers.iter_mut() { - let io = FxChainIo { - first_node_id: worker.first_node_id, - first_node_out_channels: NonZeroChannelCount::new( - cx.node_channel_config(worker.first_node_id) - .unwrap() - .num_outputs - .get(), - ) - .unwrap(), - dst_node_id, - dst_node_in_channels, - }; - - (f)( - &io, - &mut worker.fx_state.fx_chain, - &mut worker.fx_state.node_ids, - )?; - } - - if call_update_when_done { - cx.update()?; - } - - Ok(()) - }) - } - - /// Pause the given worker. - /// - /// * `worker_id` - The ID of the worker - /// * `time` - The instant that the pause should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - /// * `cx` - The Firewheel context - /// - /// Returns `true` if a worker with the given ID exists, `false` otherwise. - pub fn pause( - &mut self, - worker_id: WorkerID, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) -> bool { - let Some(idx) = self.worker_ids.get(worker_id.0).copied() else { - return false; - }; - - let worker = &mut self.workers[idx]; - - let mut new_params = worker.first_node_params.clone(); - N::pause(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - - true - } - - /// Resume the given worker. - /// - /// * `worker_id` - The ID of the worker - /// * `time` - The instant that the resume should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - /// * `cx` - The Firewheel context - /// - /// Returns `true` if a worker with the given ID exists, `false` otherwise. - pub fn resume( - &mut self, - worker_id: WorkerID, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) -> bool { - let Some(idx) = self.worker_ids.get(worker_id.0).copied() else { - return false; - }; - - let worker = &mut self.workers[idx]; - - let mut new_params = worker.first_node_params.clone(); - N::resume(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - - true - } - - /// Stop the given worker. - /// - /// * `worker_id` - The ID of the worker - /// * `time` - The instant that the stop should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - /// * `cx` - The Firewheel context - /// - /// This will remove the worker and invalidate the given `worker_id`. - /// - /// Returns `true` if a worker with the given ID exists and was stopped. - pub fn stop( - &mut self, - worker_id: WorkerID, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) -> bool { - let Some(idx) = self.worker_ids.get(worker_id.0).copied() else { - return false; - }; - - let worker = &mut self.workers[idx]; - - let mut new_params = worker.first_node_params.clone(); - N::stop(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - - self.worker_ids.remove(worker_id.0); - worker.assigned_worker_id = None; - self.num_active_workers -= 1; - - true - } - - /// Pause all workers. - /// - /// * `time` - The instant that the stop should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn pause_all( - &mut self, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) { - for worker in self.workers.iter_mut() { - if worker.assigned_worker_id.is_some() { - let mut new_params = worker.first_node_params.clone(); - N::pause(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - } - } - } - - /// Resume all workers. - /// - /// * `time` - The instant that the stop should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn resume_all( - &mut self, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) { - for worker in self.workers.iter_mut() { - if worker.assigned_worker_id.is_some() { - let mut new_params = worker.first_node_params.clone(); - N::resume(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - } - } - } - - /// Stop all workers. - /// - /// * `time` - The instant that the stop should take effect. If this is - /// `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn stop_all( - &mut self, - #[cfg(feature = "scheduled_events")] time: Option, - cx: &mut FirewheelContext, - ) { - for worker in self.workers.iter_mut() { - if worker.assigned_worker_id.is_some() { - let mut new_params = worker.first_node_params.clone(); - N::stop(&mut new_params); - - #[cfg(not(feature = "scheduled_events"))] - let mut event_queue = cx.event_queue(worker.first_node_id); - #[cfg(feature = "scheduled_events")] - let mut event_queue = cx.event_queue_scheduled(worker.first_node_id, time); - - N::diff(&worker.first_node_params, &new_params, &mut event_queue); - - worker.assigned_worker_id = None; - } - } - - self.worker_ids.clear(); - self.num_active_workers = 0; - } - - /// Get the first node parameters of the given worker. - pub fn first_node(&self, worker_id: WorkerID) -> Option<&N::AudioNode> { - self.worker_ids - .get(worker_id.0) - .map(|idx| &self.workers[*idx].first_node_params) - } - - /// Get an immutable reference to the state of the first node of the given worker. - pub fn first_node_state<'a, T: 'static>( - &self, - worker_id: WorkerID, - cx: &'a FirewheelContext, - ) -> Option<&'a T> { - self.worker_ids - .get(worker_id.0) - .and_then(|idx| cx.node_state::(self.workers[*idx].first_node_id)) - } - - /// Get a mutable reference to the state of the first node of the given worker. - pub fn first_node_state_mut<'a, T: 'static>( - &self, - worker_id: WorkerID, - cx: &'a mut FirewheelContext, - ) -> Option<&'a mut T> { - self.worker_ids - .get(worker_id.0) - .and_then(|idx| cx.node_state_mut::(self.workers[*idx].first_node_id)) - } - - pub fn fx_chain(&self, worker_id: WorkerID) -> Option<&FxChainState> { - self.worker_ids - .get(worker_id.0) - .map(|idx| &self.workers[*idx].fx_state) - } - - pub fn fx_chain_mut(&mut self, worker_id: WorkerID) -> Option<&mut FxChainState> { - self.worker_ids - .get(worker_id.0) - .map(|idx| &mut self.workers[*idx].fx_state) - } - - /// The ID of the node that all fx chain outputs are connected to. - pub fn dst_node_id(&self) -> NodeID { - self.dst_node_id - } - - /// Returns `true` if the sequence has either not started playing yet or has finished - /// playing. - pub fn has_stopped(&self, worker_id: WorkerID, cx: &FirewheelContext) -> bool { - self.worker_ids - .get(worker_id.0) - .map(|idx| N::node_is_stopped(self.workers[*idx].first_node_id, cx).unwrap()) - .unwrap_or(true) - } - - /// Poll for the current number of active workers, and return a list of - /// workers which have finished playing. - /// - /// Calling this method is optional. - pub fn poll(&mut self, cx: &FirewheelContext) -> PollResult { - self.num_active_workers = 0; - let mut finished_workers = SmallVec::new(); - - for worker in self.workers.iter_mut() { - if worker.assigned_worker_id.is_some() { - if N::node_is_stopped(worker.first_node_id, cx).unwrap() { - let id = worker.assigned_worker_id.take().unwrap(); - self.worker_ids.remove(id.0); - finished_workers.push(id); - } else { - self.num_active_workers += 1; - } - } - } - - PollResult { finished_workers } - } - - /// The total number of active workers. - pub fn num_active_workers(&self) -> usize { - self.num_active_workers - } - - /// Consume this audio node pool and remove all of its nodes from the audio graph. - /// - /// Returns a list of all node IDs and edges that were removed from the graph. - pub fn remove_all_nodes(mut self, cx: &mut FirewheelContext) -> (Vec, Vec) { - let mut removed_nodes = Vec::new(); - let mut removed_edges = Vec::new(); - - for worker in self.workers.drain(..) { - worker.remove_nodes(cx, &mut removed_nodes, &mut removed_edges); - } - - (removed_nodes, removed_edges) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct PollResult { - /// The worker IDs which have finished playing. These IDs are now - /// invalidated. - pub finished_workers: SmallVec<[WorkerID; 4]>, -} - -/// The result of calling [`AudioNodePool::new_worker`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NewWorkerResult { - /// The new ID of the worker assigned to play this sequence. - pub worker_id: WorkerID, - - /// The ID that was previously assigned to this worker. - pub old_worker_id: Option, - - /// The ID of the first node in this worker. - pub first_node_id: NodeID, - - /// If this is `true`, then this worker was already playing a sequence, and that - /// sequence has been stopped. - pub was_playing_sequence: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -pub enum NewWorkerError { - #[error( - "Could not create new audio node pool worker: the given parameters signify a stopped sequence" - )] - ParameterStateIsStop, - #[error("Could not create new audio node pool worker: the worker pool is full")] - NoMoreWorkers, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -pub enum PoolError { - #[error("A node with ID {0:?} does not exist in this pool")] - InvalidNodeID(NodeID), -} - -/// An error occured while creating or modify a [`AudioNodePool`]. -#[derive(Debug, thiserror::Error)] -pub enum ModifyNodePoolError { - /// The destination node was not found in the audio graph. - #[error("The destination node {0:?} was not found")] - DstNodeNotFound(NodeID), - /// The destination node node has no input ports. - #[error("The destination node {0:?} has no input ports")] - DstNodeNoInputs(NodeID), - /// The first node has no output ports. - #[error("The first node has no output ports")] - FirstNodeNoOutput, - /// An error occured while adding a new node to the graph. - #[error("{0}")] - NodeError(NodeError), - /// An error occured while removing a node from the graph. - #[error("{0}")] - RemoveNodeError(#[from] RemoveNodeError), - /// An error occured while adding a new edge to the graph. - #[error("{0}")] - AddEdgeError(#[from] AddEdgeError), - /// An error occurred while updating a Firewheel context. - #[error("{0}")] - UpdateError(#[from] UpdateError), - /// An error while trying to compile the graph, i.e. a - /// cycle was detected. - #[error("{0}")] - CompileGraphError(#[from] CompileGraphError), -} - -impl From for ModifyNodePoolError { - fn from(e: NodeError) -> Self { - Self::NodeError(e) - } -} diff --git a/crates/firewheel-pool/src/sampler.rs b/crates/firewheel-pool/src/sampler.rs deleted file mode 100644 index 341c2887..00000000 --- a/crates/firewheel-pool/src/sampler.rs +++ /dev/null @@ -1,77 +0,0 @@ -use firewheel_core::{ - diff::{Diff, PathBuilder}, - node::NodeID, -}; -use firewheel_graph::{ContextQueue, FirewheelContext}; -use firewheel_nodes::sampler::{SamplerNode, SamplerState}; - -use crate::{PoolError, PoolableNode}; - -/// A struct which uses a [`SamplerNode`] as the first node in an -/// [`AudioNodePool`](crate::AudioNodePool). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SamplerPool; - -impl PoolableNode for SamplerPool { - type AudioNode = SamplerNode; - - /// Return `true` if the given parameters signify that the sequence is stopped, - /// `false` otherwise. - fn params_stopped(params: &SamplerNode) -> bool { - params.stop_requested() - } - - /// Return `true` if the node state of the given node is stopped. - /// - /// Return an error if the given `node_id` is invalid. - fn node_is_stopped(node_id: NodeID, cx: &FirewheelContext) -> Result { - cx.node_state::(node_id) - .map(|s| s.stopped()) - .ok_or(PoolError::InvalidNodeID(node_id)) - } - - /// Return a score of how ready this node is to accept new work. - /// - /// The worker with the highest worker score will be chosen for the new work. - /// - /// Return an error if the given `node_id` is invalid. - fn worker_score( - params: &SamplerNode, - node_id: NodeID, - cx: &mut FirewheelContext, - ) -> Result { - cx.node_state::(node_id) - .map(|s| s.worker_score(params)) - .ok_or(PoolError::InvalidNodeID(node_id)) - } - - /// Diff the new parameters and push the changes into the event queue. - fn diff(baseline: &SamplerNode, new: &SamplerNode, event_queue: &mut ContextQueue) { - new.diff(baseline, PathBuilder::default(), event_queue); - } - - /// Notify the node state that a sequence is playing. - /// - /// This is used to account for the delay between sending an event to the node - /// and the node receiving the event. - /// - /// Return an error if the given `node_id` is invalid. - fn mark_playing(node_id: NodeID, cx: &mut FirewheelContext) -> Result<(), PoolError> { - cx.node_state_mut::(node_id) - .map(|s| s.mark_playing()) - .ok_or(PoolError::InvalidNodeID(node_id)) - } - - /// Pause the sequence in the node parameters - fn pause(params: &mut SamplerNode) { - params.pause(); - } - /// Resume the sequence in the node parameters - fn resume(params: &mut SamplerNode) { - params.resume(); - } - /// Stop the sequence in the node parameters - fn stop(params: &mut SamplerNode) { - params.stop(); - } -} diff --git a/crates/firewheel-pool/src/spatial_basic.rs b/crates/firewheel-pool/src/spatial_basic.rs deleted file mode 100644 index e71a6ce7..00000000 --- a/crates/firewheel-pool/src/spatial_basic.rs +++ /dev/null @@ -1,88 +0,0 @@ -#[cfg(not(feature = "std"))] -use bevy_platform::prelude::{Vec, vec}; - -#[cfg(feature = "scheduled_events")] -use firewheel_core::clock::EventInstant; - -use firewheel_core::{ - diff::Diff, - node::{EmptyConfig, NodeID}, -}; -use firewheel_graph::FirewheelContext; -use firewheel_nodes::spatial_basic::SpatialBasicNode; - -use crate::{FxChain, FxChainIo, ModifyNodePoolError}; - -/// A default [`FxChain`] for 3D game audio. -/// -/// This chain contains a single [`SpatialBasicNode`] -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct SpatialBasicChain { - pub spatial_basic: SpatialBasicNode, -} - -impl SpatialBasicChain { - /// Set the parameters of the spatial basic node. - /// - /// * `params` - The new parameters. - /// * `time` - The instant these new parameters should take effect. If this - /// is `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn set_params( - &mut self, - params: &SpatialBasicNode, - #[cfg(feature = "scheduled_events")] time: Option, - node_ids: &[NodeID], - cx: &mut FirewheelContext, - ) { - use firewheel_core::diff::PathBuilder; - - let node_id = node_ids[0]; - - #[cfg(not(feature = "scheduled_events"))] - let event_queue = &mut cx.event_queue(node_id); - #[cfg(feature = "scheduled_events")] - let event_queue = &mut cx.event_queue_scheduled(node_id, time); - - params.diff(&self.spatial_basic, PathBuilder::default(), event_queue); - self.spatial_basic = *params; - } -} - -impl FxChain for SpatialBasicChain { - type Configuration = EmptyConfig; - - fn construct_and_connect( - &mut self, - _configuration: &Self::Configuration, - io: &FxChainIo, - cx: &mut FirewheelContext, - ) -> Result, ModifyNodePoolError> { - let spatial_basic_params = firewheel_nodes::spatial_basic::SpatialBasicNode::default(); - let spatial_basic_node_id = cx.add_node(spatial_basic_params, None)?; - - cx.connect( - io.first_node_id, - spatial_basic_node_id, - if io.first_node_out_channels.get().get() == 1 { - &[(0, 0), (0, 1)] - } else { - &[(0, 0), (1, 1)] - }, - false, - )?; - - cx.connect( - spatial_basic_node_id, - io.dst_node_id, - if io.dst_node_in_channels.get().get() == 1 { - &[(0, 0), (1, 0)] - } else { - &[(0, 0), (1, 1)] - }, - false, - )?; - - Ok(vec![spatial_basic_node_id]) - } -} diff --git a/crates/firewheel-pool/src/volume.rs b/crates/firewheel-pool/src/volume.rs deleted file mode 100644 index e2aabb76..00000000 --- a/crates/firewheel-pool/src/volume.rs +++ /dev/null @@ -1,72 +0,0 @@ -#[cfg(not(feature = "std"))] -use bevy_platform::prelude::{Vec, vec}; - -#[cfg(feature = "scheduled_events")] -use firewheel_core::clock::EventInstant; - -use firewheel_core::{ - diff::{Diff, PathBuilder}, - node::{EmptyConfig, NodeID}, -}; -use firewheel_graph::FirewheelContext; -use firewheel_nodes::volume::{VolumeNode, VolumeNodeConfig}; - -use crate::{FxChain, FxChainIo, ModifyNodePoolError}; - -/// A default [`FxChain`] for with a single [`VolumeNode`]. -/// -/// This works with any number of channels. -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct VolumeChain { - pub volume_node: VolumeNode, -} - -impl VolumeChain { - /// Set the parameters of the volume pan node. - /// - /// * `params` - The new parameters. - /// * `time` - The instant these new parameters should take effect. If this - /// is `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn set_params( - &mut self, - params: &VolumeNode, - #[cfg(feature = "scheduled_events")] time: Option, - node_ids: &[NodeID], - cx: &mut FirewheelContext, - ) { - let node_id = node_ids[0]; - - #[cfg(not(feature = "scheduled_events"))] - let event_queue = &mut cx.event_queue(node_id); - #[cfg(feature = "scheduled_events")] - let event_queue = &mut cx.event_queue_scheduled(node_id, time); - - params.diff(&self.volume_node, PathBuilder::default(), event_queue); - self.volume_node = *params; - } -} - -impl FxChain for VolumeChain { - type Configuration = EmptyConfig; - - fn construct_and_connect( - &mut self, - _configuration: &Self::Configuration, - io: &FxChainIo, - cx: &mut FirewheelContext, - ) -> Result, ModifyNodePoolError> { - let volume_params = VolumeNode::default(); - let volume_node_id = cx.add_node( - volume_params, - Some(VolumeNodeConfig { - channels: io.first_node_out_channels, - }), - )?; - - cx.auto_connect(io.first_node_id, volume_node_id, false)?; - cx.auto_connect(volume_node_id, io.dst_node_id, false)?; - - Ok(vec![volume_node_id]) - } -} diff --git a/crates/firewheel-pool/src/volume_pan.rs b/crates/firewheel-pool/src/volume_pan.rs deleted file mode 100644 index 6f6208e7..00000000 --- a/crates/firewheel-pool/src/volume_pan.rs +++ /dev/null @@ -1,92 +0,0 @@ -#[cfg(not(feature = "std"))] -use bevy_platform::prelude::{Vec, vec}; - -#[cfg(feature = "scheduled_events")] -use firewheel_core::clock::EventInstant; - -use firewheel_core::{ - channel_config::NonZeroChannelCount, - diff::{Diff, PathBuilder}, - node::{EmptyConfig, NodeID}, -}; -use firewheel_graph::FirewheelContext; -use firewheel_nodes::{volume::VolumeNodeConfig, volume_pan::VolumePanNode}; - -use crate::{FxChain, FxChainIo, ModifyNodePoolError}; - -/// A default [`FxChain`] for 2D game audio. -/// -/// This chain contains a single [`VolumePanNode`]. -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct VolumePanChain { - pub volume_pan: VolumePanNode, -} - -impl VolumePanChain { - /// Set the parameters of the volume pan node. - /// - /// * `params` - The new parameters. - /// * `time` - The instant these new parameters should take effect. If this - /// is `None`, then the parameters will take effect as soon as the node receives - /// the event. - pub fn set_params( - &mut self, - params: &firewheel_nodes::volume_pan::VolumePanNode, - #[cfg(feature = "scheduled_events")] time: Option, - node_ids: &[NodeID], - cx: &mut FirewheelContext, - ) { - let node_id = node_ids[0]; - - #[cfg(not(feature = "scheduled_events"))] - let event_queue = &mut cx.event_queue(node_id); - #[cfg(feature = "scheduled_events")] - let event_queue = &mut cx.event_queue_scheduled(node_id, time); - - params.diff(&self.volume_pan, PathBuilder::default(), event_queue); - self.volume_pan = *params; - } -} - -impl FxChain for VolumePanChain { - type Configuration = EmptyConfig; - - fn construct_and_connect( - &mut self, - _configuration: &Self::Configuration, - io: &FxChainIo, - cx: &mut FirewheelContext, - ) -> Result, ModifyNodePoolError> { - let volume_pan_params = VolumePanNode::default(); - let volume_pan_node_id = cx.add_node( - volume_pan_params, - Some(VolumeNodeConfig { - channels: NonZeroChannelCount::STEREO, - }), - )?; - - cx.connect( - io.first_node_id, - volume_pan_node_id, - if io.first_node_out_channels.get().get() == 1 { - &[(0, 0), (0, 1)] - } else { - &[(0, 0), (1, 1)] - }, - false, - )?; - - cx.connect( - volume_pan_node_id, - io.dst_node_id, - if io.dst_node_in_channels.get().get() == 1 { - &[(0, 0), (1, 0)] - } else { - &[(0, 0), (1, 1)] - }, - false, - )?; - - Ok(vec![volume_pan_node_id]) - } -} diff --git a/examples/custom_nodes/src/nodes/rms.rs b/examples/custom_nodes/src/nodes/rms.rs index 5d04912e..dfc159ea 100644 --- a/examples/custom_nodes/src/nodes/rms.rs +++ b/examples/custom_nodes/src/nodes/rms.rs @@ -3,12 +3,14 @@ // The use of `bevy_platform` is optional, but it is recommended for better // compatibility with webassembly, no_std, and platforms without 64 bit atomics. -use bevy_platform::sync::atomic::{AtomicU32, Ordering}; +use bevy_platform::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; use firewheel::node::NodeError; use firewheel::{ atomic_float::AtomicF32, channel_config::{ChannelConfig, ChannelCount}, - collector::ArcGc, diff::{Diff, Patch}, event::ProcEvents, node::{ @@ -68,16 +70,13 @@ impl Default for FastRmsNode { // it using `FirewheelCtx::node_state` and `FirewheelCtx::node_state_mut`. #[derive(Clone)] pub struct FastRmsState { - // `ArcGc` is a simple wrapper around `Arc` that automatically collects - // dropped resources from the audio thread and drops them on another - // thread. - shared_state: ArcGc, + shared_state: Arc, } impl FastRmsState { fn new() -> Self { Self { - shared_state: ArcGc::new(SharedState { + shared_state: Arc::new(SharedState { rms_value: AtomicF32::new(0.0), read_count: AtomicU32::new(1), }), @@ -140,11 +139,11 @@ impl AudioNode for FastRmsNode { Ok(Processor { params: *self, - shared_state: ArcGc::clone(&custom_state.shared_state), squares: 0.0, num_squared_values: 0, window_frames, last_read_count: 0, + shared_state: Arc::clone(&custom_state.shared_state), }) } } @@ -152,11 +151,19 @@ impl AudioNode for FastRmsNode { // The realtime processor counterpart to your node. struct Processor { params: FastRmsNode, - shared_state: ArcGc, squares: f32, num_squared_values: usize, window_frames: usize, last_read_count: u32, + + // Note, in this case it is realtime safe to use `Arc` in the processor like + // this because the processor is always sent back to the main thread before + // it is dropped. + // + // If instead you had shared state that could be dropped while the processor + // is still running, prefer to use `ArcGc` or `OwnedGc` instead to avoid + // deallocating on the audio thread (because it may cause audio glitches). + shared_state: Arc, } impl AudioNodeProcessor for Processor { diff --git a/examples/play_sample/src/main.rs b/examples/play_sample/src/main.rs index 6da94b2b..c4402fe7 100644 --- a/examples/play_sample/src/main.rs +++ b/examples/play_sample/src/main.rs @@ -67,26 +67,35 @@ fn main() { sampler_node.start_or_restart(); cx.queue_event_for(sampler_id, sampler_node.sync_play_event()); - // Manually set the shared playback flag. This is needed to account for the delay - // between sending a play event and the node's processor receiving that event. - cx.node_state::(sampler_id) - .unwrap() - .mark_playing(); + // Get the playback ID after calling `start_or_restart()` to detect when + // this specific playback sequence has finished playing. + // + // Note, this ID becomes invalidated once the sampler node receives a + // new "play" event. + let playback_id = sampler_node.playback_id(); // --- Simulated update loop --------------------------------------------------------- loop { - if cx.node_state::(sampler_id).unwrap().stopped() { - // Sample has finished playing. - break; - } - // Update the firewheel context. // This must be called regularly (i.e. once every frame). if let Err(e) = cx.update() { tracing::error!("{:?}", &e); } + // Using `playback_finished()` is more reliable than using + // `currently_stopped()` since it takes into account the delay + // between when the play event is created and when the sampler + // node receives the event. + if cx + .node_state::(sampler_id) + .unwrap() + .playback_finished(playback_id) + { + // Sample has finished playing. + break; + } + // Log any stream errors/warnings that have occurred. stream.log_status(); diff --git a/examples/sampler_pool/Cargo.toml b/examples/sampler_pool/Cargo.toml deleted file mode 100644 index acd34054..00000000 --- a/examples/sampler_pool/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "sampler_pool" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -# Note, the "scheduled_events" feature is not needed to use the pool feature. -# Rust analyzer just gets confused without it in this example. -firewheel = { path = "../../", features = ["pool", "scheduled_events"] } -tracing.workspace = true -tracing-subscriber.workspace = true -egui.workspace = true -eframe.workspace = true -symphonium = { workspace = true, default-features = true, features = ["mp3", "flac"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = "0.4" diff --git a/examples/sampler_pool/src/main.rs b/examples/sampler_pool/src/main.rs deleted file mode 100644 index 5130ce9e..00000000 --- a/examples/sampler_pool/src/main.rs +++ /dev/null @@ -1,51 +0,0 @@ -mod system; -mod ui; - -// When compiling natively: -#[cfg(not(target_arch = "wasm32"))] -fn main() -> eframe::Result<()> { - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::DEBUG) - .finish(), - ) - .unwrap(); - - let native_options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_inner_size([575.0, 300.0]) - .with_min_inner_size([575.0, 220.0]), - vsync: true, - ..Default::default() - }; - - eframe::run_native( - "firewheel sampler pool test", - native_options, - Box::new(|_| Ok(Box::new(ui::DemoApp::new()))), - ) -} - -// When compiling to web using trunk: -#[cfg(target_arch = "wasm32")] -fn main() { - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::DEBUG) - .finish(), - ) - .unwrap(); - - let web_options = eframe::WebOptions::default(); - - wasm_bindgen_futures::spawn_local(async { - eframe::WebRunner::new() - .start( - "firewheel sampler pool test", - web_options, - Box::new(|cx| Ok(Box::new(ui::DemoApp::new(cx)))), - ) - .await - .expect("failed to start eframe"); - }); -} diff --git a/examples/sampler_pool/src/system.rs b/examples/sampler_pool/src/system.rs deleted file mode 100644 index f0a93eb3..00000000 --- a/examples/sampler_pool/src/system.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::num::NonZeroUsize; - -use firewheel::{ - channel_config::NonZeroChannelCount, - collector::ArcGc, - cpal::CpalStream, - diff::Memo, - node::EmptyConfig, - nodes::{ - sampler::SamplerNode, - volume::{VolumeNode, VolumeNodeConfig}, - StereoToMonoNode, - }, - pool::{ - AudioNodePool, FxChain, FxChainIo, ModifyNodePoolError, SamplerPool, SamplerPoolVolumePan, - }, - sample_resource::SampleResource, - FirewheelContext, -}; - -/// The maximum number of samples that can be played in parallel in the `SamplerPool`. -/// -/// A lower number was chosen to better showcase how work is stolen from the oldest -/// playing sample in the case when there are no more free workers left. A typical -/// game would probably want something a bit higher like `16` (Though keep in mind -/// that the higher the number, the more processing overhead there will be.) -pub const NUM_WORKERS: usize = 4; - -pub struct AudioSystem { - pub cx: FirewheelContext, - pub stream: CpalStream, - - // `SamplerPoolVolumePan` is an alias for `AudioNodePool`. - pub sampler_pool_1: SamplerPoolVolumePan, - pub sampler_pool_2: AudioNodePool, - pub sampler_node: SamplerNode, - pub sample: ArcGc, -} - -impl AudioSystem { - pub fn new() -> Self { - let mut cx = FirewheelContext::new(Default::default()); - let stream = CpalStream::new(&mut cx, Default::default()).unwrap(); - - let graph_out = cx.graph_out_node_id(); - - let sampler_pool_1 = SamplerPoolVolumePan::new( - NonZeroUsize::new(NUM_WORKERS).unwrap(), // The number of workers to create in this pool. - SamplerNode::default(), // Use the default sampler node parameters. - None, // Use the default sampler node configuration. - None, // Use the default fx chain configuration. - graph_out, // The ID of the node that the last effect in each fx chain instance will connect to. - false, // Call cx.update() when done to check if all nodes activate without error. - &mut cx, // The firewheel context. - ) - .expect("Sampler pool should construct without error"); - - let sampler_pool_2 = AudioNodePool::new( - NonZeroUsize::new(NUM_WORKERS).unwrap(), // The number of workers to create in this pool. - SamplerNode::default(), // Use the default sampler node parameters. - None, // Use the default sampler node configuration. - None, // Use the default fx chain configuration. - graph_out, // The ID of the node that the last effect in each fx chain instance will connect to. - false, // Call cx.update() when done to check if all nodes activate without error. - &mut cx, // The firewheel context. - ) - .expect("Sampler pool should construct without error"); - - let sample_rate = cx.stream_info().unwrap().sample_rate; - - let probed = symphonium::probe_from_file( - "assets/test_files/bird-sound.wav", - None, // Custom container probe - ) - .unwrap(); - let sample = firewheel::dyn_symphonium_resource( - symphonium::decode( - probed, - &symphonium::DecodeConfig::default(), - Some(sample_rate), // target sample rate - None, // An optional cache - None, // Custom codec registry - ) - .unwrap(), - ); - - let sampler_node = SamplerNode::default(); - - // Note, you can get the playhead and other state of a worker like this: - // let playhead = sampler_pool_1 - // .first_node_state::(worker_id, &mut cx) - // .unwrap() - // .playhead_seconds(sample_rate); - - Self { - cx, - stream, - sampler_pool_1, - sampler_pool_2, - sampler_node, - sample, - } - } - - pub fn update(&mut self) { - // Update the firewheel context. - // This must be called regularly (i.e. once every frame). - if let Err(e) = self.cx.update() { - tracing::error!("{:?}", &e); - } - - // Log any stream errors/warnings that have occurred. - self.stream.log_status(); - - // The stream has stopped unexpectedly (i.e the user has - // unplugged their headphones.) - // - // Typically you should start a new stream as soon as - // possible to resume processing (even if it's a dummy - // output device). - // - // In this example we just quit the application. - if !self.stream.all_streams_ok() { - panic!("Stream stopped unexpectedly!"); - } - } -} - -/// An example of a custom FX chain for a sampler pool. -#[derive(Default)] -pub struct MyCustomChain { - pub _stereo_to_mono: StereoToMonoNode, - pub volume: Memo, -} - -impl FxChain for MyCustomChain { - /// The one-time configuration for constructing a new instance of this fx chain. - /// - /// When no configuration is required, `EmptyConfig` should be used. - type Configuration = EmptyConfig; - - fn construct_and_connect( - &mut self, - _configuration: &Self::Configuration, - // Information about the input/output nodes for this fx chain instance. - io: &FxChainIo, - // The firewheel context. - cx: &mut FirewheelContext, - ) -> Result, ModifyNodePoolError> { - // In this example we only support stereo, but you can have your FX - // chain support multiple channel configurations. - assert_eq!(io.first_node_out_channels, NonZeroChannelCount::STEREO); - assert_eq!(io.dst_node_in_channels, NonZeroChannelCount::STEREO); - - let stereo_to_mono_node_id = cx.add_node(StereoToMonoNode, None)?; - - let volume_params = VolumeNode::default(); - let volume_node_id = cx.add_node( - volume_params, - Some(VolumeNodeConfig { - channels: NonZeroChannelCount::MONO, - }), - )?; - - // Connect the sampler node to the stereo_to_mono node. - cx.connect( - io.first_node_id, - stereo_to_mono_node_id, - &[(0, 0), (1, 1)], - false, - )?; - - // Connect the stereo_to_mono node to the volume node. - cx.connect(stereo_to_mono_node_id, volume_node_id, &[(0, 0)], false)?; - - // Connect the volume node to the destination node. - cx.connect(volume_node_id, io.dst_node_id, &[(0, 0), (0, 1)], false)?; - - // Return the list of node IDs in this FX chain. - Ok(vec![stereo_to_mono_node_id, volume_node_id]) - } -} diff --git a/examples/sampler_pool/src/ui.rs b/examples/sampler_pool/src/ui.rs deleted file mode 100644 index a807a091..00000000 --- a/examples/sampler_pool/src/ui.rs +++ /dev/null @@ -1,121 +0,0 @@ -use eframe::App; -use firewheel::{ - diff::EventQueue, - nodes::{sampler::SamplerNode, volume_pan::VolumePanNode}, - Volume, -}; - -use crate::system::AudioSystem; - -pub struct DemoApp { - audio_system: AudioSystem, -} - -impl DemoApp { - pub fn new() -> Self { - Self { - audio_system: AudioSystem::new(), - } - } -} - -impl App for DemoApp { - fn update(&mut self, cx: &egui::Context, _frame: &mut eframe::Frame) { - egui::TopBottomPanel::top("top_panel").show(cx, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - #[cfg(not(target_arch = "wasm32"))] - { - ui.menu_button("Menu", |ui| { - if ui.button("Quit").clicked() { - cx.send_viewport_cmd(egui::ViewportCommand::Close) - } - }); - ui.add_space(16.0); - } - - egui::widgets::global_theme_preference_switch(ui); - }); - }); - - egui::CentralPanel::default().show(cx, |ui| { - ui.label("Default VolumePan FX Chain"); - - if ui.button("Play").clicked() { - self.audio_system.sampler_node.start_or_restart(); - - // The `worker_id` can be later used to reference this piece of work being done. - let _worker_id = self.audio_system.sampler_pool_1.new_worker( - &self.audio_system.sampler_node, - None, // Apply the changes immediately - true, // Steal worker if pool is full - &mut self.audio_system.cx, - |event_queue| { - // Additional events to send to the first node in the worker. - event_queue.push(SamplerNode::set_dyn_sample_event( - self.audio_system.sample.clone(), - )); - }, - |fx_chain_state, cx| { - // While we don't change these parameters in this example, in a typical app - // you would want to reset the parameters to the desired state when playing - // a new sample. - fx_chain_state.fx_chain.set_params( - &VolumePanNode::default(), - None, // Apply the changes immediately - &fx_chain_state.node_ids, - cx, - ); - }, - ); - } - - self.audio_system.sampler_pool_1.poll(&self.audio_system.cx); - - let num_active_works = self.audio_system.sampler_pool_1.num_active_workers(); - ui.label(format!("Num active workers: {}", num_active_works)); - - ui.separator(); - - ui.label("Custom FX Chain"); - - if ui.button("Play").clicked() { - self.audio_system.sampler_node.start_or_restart(); - - // The `worker_id` can be later used to reference this piece of work being done. - let _worker_id = self.audio_system.sampler_pool_2.new_worker( - &self.audio_system.sampler_node, - None, // Apply the changes immediately - true, // Steal worker if pool is full - &mut self.audio_system.cx, - |event_queue| { - // Additional events to send to the first node in the worker. - event_queue.push(SamplerNode::set_dyn_sample_event( - self.audio_system.sample.clone(), - )); - }, - |fx_chain_state, cx| { - // While we don't change these parameters in this example, in a typical app - // you would want to reset the parameters to the desired state when playing - // a new sample. - fx_chain_state.fx_chain.volume.volume = Volume::UNITY_GAIN; - - // The nodes IDs appear in the same order as what was returned in - // [`MyCustomChain::construct_and_connect`]. - fx_chain_state.fx_chain.volume.update_memo( - &mut cx.event_queue_scheduled(fx_chain_state.node_ids[1], None), - ); - }, - ); - } - - self.audio_system.sampler_pool_2.poll(&self.audio_system.cx); - - let num_active_works = self.audio_system.sampler_pool_2.num_active_workers(); - ui.label(format!("Num active workers: {}", num_active_works)); - }); - - self.audio_system.update(); - - cx.request_repaint(); - } -} diff --git a/examples/sampler_test/src/system.rs b/examples/sampler_test/src/system.rs index b425df7e..dc858fdc 100644 --- a/examples/sampler_test/src/system.rs +++ b/examples/sampler_test/src/system.rs @@ -176,14 +176,14 @@ impl AudioSystem { self.cx .node_state::(self.samplers[sampler_i].node_id) .unwrap() - .playing() + .currently_playing() } pub fn is_paused(&self, sampler_i: usize) -> bool { self.cx .node_state::(self.samplers[sampler_i].node_id) .unwrap() - .paused() + .currently_paused() } pub fn update(&mut self) { diff --git a/src/lib.rs b/src/lib.rs index b4c5f45e..734d7195 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,5 @@ pub use firewheel_cpal as cpal; #[cfg(feature = "rtaudio")] pub use firewheel_rtaudio as rtaudio; -#[cfg(feature = "pool")] -pub use firewheel_pool as pool; - #[cfg(feature = "symphonium")] pub use firewheel_symphonium::*;