diff --git a/crates/firewheel-core/src/diff/leaf.rs b/crates/firewheel-core/src/diff/leaf.rs index ae9fc7a3..4a178392 100644 --- a/crates/firewheel-core/src/diff/leaf.rs +++ b/crates/firewheel-core/src/diff/leaf.rs @@ -13,6 +13,23 @@ use crate::{ #[cfg(feature = "musical_transport")] use crate::clock::{DurationMusical, InstantMusical}; +impl Diff for () { + fn diff(&self, _baseline: &Self, _path: PathBuilder, _event_queue: &mut E) {} +} + +impl Patch for () { + type Patch = (); + + fn patch(data: &ParamData, _path: &[u32]) -> Result { + match data { + ParamData::None => Ok(()), + _ => Err(PatchError::InvalidData), + } + } + + fn apply(&mut self, _patch: Self::Patch) {} +} + macro_rules! primitive_diff { ($ty:ty, $variant:ident) => { impl Diff for $ty { diff --git a/crates/firewheel-core/src/diff/mod.rs b/crates/firewheel-core/src/diff/mod.rs index 3be6cc2b..f94b1aef 100644 --- a/crates/firewheel-core/src/diff/mod.rs +++ b/crates/firewheel-core/src/diff/mod.rs @@ -100,7 +100,7 @@ //! # use firewheel_core::diff::{Diff, Patch, PathBuilder}; //! #[derive(Diff, Patch, Clone, PartialEq)] //! enum SoundSource { -//! Sample(ArcGc), // Will _not_ cause allocations in `Patch`. +//! Sample(ArcGc), // Will _not_ cause allocations in `Patch`. //! Frequency(f32), //! } //! ``` diff --git a/crates/firewheel-core/src/node.rs b/crates/firewheel-core/src/node.rs index 6c87aff5..94d91918 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -52,6 +52,12 @@ impl Default for NodeID { #[derive(Debug)] pub struct NodeError(Box); +impl NodeError { + pub const fn from_boxed(error: Box) -> Self { + Self(error) + } +} + impl From for NodeError where E: Error + 'static, diff --git a/crates/firewheel-core/src/sample_resource.rs b/crates/firewheel-core/src/sample_resource.rs index a7030b79..72aecb9d 100644 --- a/crates/firewheel-core/src/sample_resource.rs +++ b/crates/firewheel-core/src/sample_resource.rs @@ -15,7 +15,7 @@ use bevy_platform::prelude::Vec; use crate::collector::ArcGc; /// Trait returning information about a resource of audio samples -pub trait SampleResourceInfo: Send + Sync + 'static { +pub trait SampleResourceInfo { /// The number of channels in this resource. fn num_channels(&self) -> NonZeroUsize; @@ -45,12 +45,20 @@ pub trait SampleResource: SampleResourceInfo { /// * `start_frame` - The sample (of a single channel of audio) in the /// resource at which to start copying from. Not to be confused with video /// frames. + /// + /// If the length of `out_buffer_range` is all or partly out of bounds of + /// the resource, then the frames which are out of bounds will be left + /// untouched. + /// + /// Returns the number of frames that were successfully filled. This may + /// be less than the length of `out_buffer_range` if the range is all or + /// partly out of bounds of the resource fn fill_buffers( &self, out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, - ); + ) -> usize; } /// A resource of audio samples stored as de-interleaved f32 values. @@ -59,18 +67,24 @@ pub trait SampleResourceF32: SampleResourceInfo { fn channel(&self, i: usize) -> Option<&[f32]>; } -impl From for ArcGc { +impl From + for ArcGc +{ fn from(value: T) -> Self { ArcGc::new_unsized(|| { - bevy_platform::sync::Arc::new(value) as bevy_platform::sync::Arc + bevy_platform::sync::Arc::new(value) + as bevy_platform::sync::Arc }) } } -impl From for ArcGc { +impl From + for ArcGc +{ fn from(value: T) -> Self { ArcGc::new_unsized(|| { - bevy_platform::sync::Arc::new(value) as bevy_platform::sync::Arc + bevy_platform::sync::Arc::new(value) + as bevy_platform::sync::Arc }) } } @@ -83,9 +97,10 @@ pub struct InterleavedResourceF32 { } impl InterleavedResourceF32 { - pub fn into_dyn_resource(self) -> ArcGc { + pub fn into_dyn_resource(self) -> ArcGc { ArcGc::new_unsized(|| { - bevy_platform::sync::Arc::new(self) as bevy_platform::sync::Arc + bevy_platform::sync::Arc::new(self) + as bevy_platform::sync::Arc }) } } @@ -110,7 +125,7 @@ impl SampleResource for InterleavedResourceF32 { out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, - ) { + ) -> usize { fill_buffers_interleaved( out_buffer, out_buffer_range, @@ -118,7 +133,7 @@ impl SampleResource for InterleavedResourceF32 { self.channels, &self.data, self.len_frames() as usize, - ); + ) } } @@ -149,14 +164,14 @@ impl SampleResource for Vec> { out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, - ) { + ) -> usize { fill_buffers_deinterleaved_f32( out_buffer, out_buffer_range, start_frame, self, self[0].len(), - ); + ) } } @@ -167,6 +182,10 @@ impl SampleResourceF32 for Vec> { } /// A helper method to fill buffers from a resource of interleaved samples. +/// +/// Returns the number of frames that were successfully filled. This may +/// be less than the length of `out_buffer_range` if the range is all or +/// partly out of bounds of the resource pub fn fill_buffers_interleaved( out_buffer: &mut [&mut [f32]], out_buffer_range: Range, @@ -174,7 +193,7 @@ pub fn fill_buffers_interleaved( channels: NonZeroUsize, resource: &[T], resource_len_frames: usize, -) { +) -> usize { let channels = channels.get(); let Some((frames, start_frame)) = constrain_frames( @@ -182,7 +201,7 @@ pub fn fill_buffers_interleaved( start_frame, resource_len_frames, ) else { - return; + return 0; }; // Provide an optimized loop for stereo. @@ -201,7 +220,7 @@ pub fn fill_buffers_interleaved( *buf1_s = src_chunk[1].to_scaled_float(); } - return; + return frames; } let src_slice = &resource[start_frame * channels..(start_frame + frames) * channels]; @@ -216,21 +235,27 @@ pub fn fill_buffers_interleaved( &mut out_ch[out_buffer_range.start..out_buffer_range.start + frames], ); } + + frames } /// A helper method to fill buffers from a resource of deinterleaved samples. +/// +/// Returns the number of frames that were successfully filled. This may +/// be less than the length of `out_buffer_range` if the range is all or +/// partly out of bounds of the resource pub fn fill_channel_deinterleaved( out_buffer_channel: &mut [f32], out_buffer_range: Range, start_frame: u64, resource_channel: &[T], -) { +) -> usize { let Some((frames, start_frame)) = constrain_frames( out_buffer_range.end - out_buffer_range.start, start_frame, resource_channel.len(), ) else { - return; + return 0; }; let adapter = SequentialSlice::new(resource_channel, 1, frames).unwrap(); @@ -241,36 +266,50 @@ pub fn fill_channel_deinterleaved( start_frame, &mut out_buffer_channel[out_buffer_range.start..out_buffer_range.start + frames], ); + + frames } /// A helper method to fill buffers from a resource of deinterleaved `f32` samples. +/// +/// Returns the number of frames that were successfully filled. This may +/// be less than the length of `out_buffer_range` if the range is all or +/// partly out of bounds of the resource pub fn fill_buffers_deinterleaved_f32>( out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, resource_channels: &[V], resource_len_frames: usize, -) { +) -> usize { let Some((frames, start_frame)) = constrain_frames( out_buffer_range.end - out_buffer_range.start, start_frame, resource_len_frames, ) else { - return; + return 0; }; for (out_ch, in_ch) in out_buffer.iter_mut().zip(resource_channels.iter()) { out_ch[out_buffer_range.start..out_buffer_range.start + frames] .copy_from_slice(&in_ch.as_ref()[start_frame..start_frame + frames]); } + + frames } -fn constrain_frames( - out_buffer_len: usize, +/// A helper to constrain the requested number of frames to the available frames +/// in the sample resource. +/// +/// Returns `Some((available_frames, start_frame as usize))` if the range is all +/// or partly contained in the resource, or `None` if the range is fully outside +/// the resource (`available_frames == 0`). +pub fn constrain_frames( + requested_frames: usize, start_frame: u64, resource_len_frames: usize, ) -> Option<(usize, usize)> { - let frames = (out_buffer_len as u64) + let frames = (requested_frames as u64) .min((resource_len_frames as u64).saturating_sub(start_frame)) as usize; if frames == 0 { None diff --git a/crates/firewheel-nodes/src/convolution.rs b/crates/firewheel-nodes/src/convolution.rs index 0f5f0bc7..8b93ccc0 100644 --- a/crates/firewheel-nodes/src/convolution.rs +++ b/crates/firewheel-nodes/src/convolution.rs @@ -71,7 +71,7 @@ pub struct ConvolutionNode { /// The impulse response to use. #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] #[cfg_attr(feature = "serde", serde(skip))] - pub impulse_response: Option>, + pub impulse_response: Option>, /// Pause the convolution processing. /// diff --git a/crates/firewheel-nodes/src/sampler.rs b/crates/firewheel-nodes/src/sampler.rs index 3f30b343..1cd77c55 100644 --- a/crates/firewheel-nodes/src/sampler.rs +++ b/crates/firewheel-nodes/src/sampler.rs @@ -9,11 +9,10 @@ // a custom `SampleResource`). use firewheel_core::clock::{DurationSamples, DurationSeconds}; +use firewheel_core::collector::{OwnedGc, OwnedGcUnsized}; use firewheel_core::node::{NodeError, ProcBuffers, ProcExtra, ProcStreamCtx}; -#[cfg(not(feature = "std"))] -use num_traits::Float; -use bevy_platform::sync::atomic::{AtomicU64, Ordering}; +use bevy_platform::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use bevy_platform::time::Instant; use core::sync::atomic::AtomicU32; use core::{ @@ -21,8 +20,14 @@ use core::{ ops::Range, }; use firewheel_core::diff::{EventQueue, PatchError, PathBuilder, RealtimeClone}; +use firewheel_core::sample_resource::SampleResourceInfo; use smallvec::SmallVec; +#[cfg(not(feature = "std"))] +use bevy_platform::prelude::Box; +#[cfg(not(feature = "std"))] +use num_traits::Float; + use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount, NonZeroChannelCount}, clock::InstantSeconds, @@ -98,19 +103,188 @@ pub enum PlaybackSpeedQuality { // TODO: more quality options } +/// A source of audio samples for a [`SamplerNode`]. +pub enum SamplerNodeResource { + /// A resource of audio samples where the entire contents of the sample are + /// already loaded into memory. + /// + /// Prefer this for resources which are less than 20 or so seconds long + /// (i.e. sound effects). + InMemory(ArcGc), + + /// NOT IMPLEMENTED YET! Will lead to a panic if used. + /// + /// A resource of audio samples that are streamed from disk or over a network. + /// + /// Prefer this for resources which are greather than 20 or so seconds long + /// (i.e. music tracks and ambience). + /// + /// This uses considerably less memory, but requires a more complicated setup. + /// It also has the potential to run into cache misses if the playhead is moved + /// to a region that hasn't been loaded yet, or if the stream fails to send + /// enough samples in time. + Streamed(OwnedGcUnsized), +} + +impl SamplerNodeResource { + pub fn from_sample(sample: T) -> Self { + Self::InMemory(sample.into()) + } + + pub fn from_streamed(sample: T) -> Self { + Self::Streamed(OwnedGcUnsized::new_unsized(Box::new(sample))) + } + + /// The number of channels in this resource. + pub fn num_channels(&self) -> NonZeroUsize { + match self { + Self::InMemory(s) => s.num_channels(), + Self::Streamed(s) => s.num_channels(), + } + } + + /// The length of this resource in samples (of a single channel of audio). + /// + /// Not to be confused with video frames. + pub fn len_frames(&self) -> u64 { + match self { + Self::InMemory(s) => s.len_frames(), + Self::Streamed(s) => s.len_frames(), + } + } + + /// The sample rate of this resource. + /// + /// Returns `None` if the sample rate is unknown. + pub fn sample_rate(&self) -> Option { + match self { + Self::InMemory(s) => s.sample_rate(), + Self::Streamed(s) => s.sample_rate(), + } + } + + /// Fill the given buffers with audio data starting from the given + /// starting frame in the resource. + /// + /// * `out_buffer` - The buffers to fill with data. If the length of `buffers` + /// is greater than the number of channels in this resource, then ignore + /// the extra buffers. + /// * `out_buffer_range` - The range inside each buffer slice in which to + /// fill with data. Do not fill any data outside of this range. + /// * `start_frame` - The sample (of a single channel of audio) in the + /// resource at which to start copying from. Not to be confused with video + /// frames. + /// * `speed` - The speed at which playback is occuring, where `1.0` is + /// playing at the sample rate of this resource, `0.5` is playing at half + /// the sample rate, and `2.0` is playing at twice the sample rate. + /// + /// Returns the number of frames that were successfully filled. This may + /// be less than the length of `out_buffer_range` if the range is all or + /// partly out of bounds of the resource, or if a cache miss occured. + /// Any frames that were not successfully filled will be left untouched. + pub fn fill_buffers( + &mut self, + out_buffer: &mut [&mut [f32]], + out_buffer_range: Range, + start_frame: u64, + speed: f64, + is_playing_backwards: bool, + ) -> usize { + match self { + SamplerNodeResource::InMemory(s) => { + s.fill_buffers(out_buffer, out_buffer_range.clone(), start_frame) + } + SamplerNodeResource::Streamed(s) => s.fill_buffers( + out_buffer, + out_buffer_range, + start_frame, + speed, + is_playing_backwards, + ), + } + } + + /// Returns `true` if the given range of frames is loaded + /// into memory and ready to be read. + pub fn range_is_ready(&mut self, range: Range) -> bool { + if let SamplerNodeResource::Streamed(s) = self { + s.range_is_ready(range) + } else { + true + } + } + + /// Request to cache a new region at the given starting frame. + pub fn cache_new_starting_frame(&mut self, frame: u64, speed: f64, will_play_backwards: bool) { + if let SamplerNodeResource::Streamed(s) = self { + s.cache_new_starting_frame(frame, speed, will_play_backwards); + } + } +} + +impl From> for SamplerNodeResource { + fn from(value: ArcGc) -> Self { + Self::InMemory(value) + } +} + +impl From> for SamplerNodeResource { + fn from(value: OwnedGcUnsized) -> Self { + Self::Streamed(value) + } +} + +/// A resource of audio samples that are streamed from disk or over a network. +/// +/// This uses considerably less memory, but requires a more complicated setup. It +/// also has the potential to run into cache misses if the playhead is moved to a +/// region that hasn't been loaded yet, or if the stream fails to send enough samples +/// in time. +pub trait StreamedSample: SampleResourceInfo + Send + Sync + 'static { + /// Fill the given buffers with audio data starting from the given + /// starting frame in the resource. + /// + /// * `out_buffer` - The buffers to fill with data. If the length of `buffers` + /// is greater than the number of channels in this resource, then ignore + /// the extra buffers. + /// * `out_buffer_range` - The range inside each buffer slice in which to + /// fill with data. Do not fill any data outside of this range. + /// * `start_frame` - The sample (of a single channel of audio) in the + /// resource at which to start copying from. Not to be confused with video + /// frames. + /// * `speed` - The speed at which playback is occuring, where `1.0` is + /// playing at the sample rate of this resource, `0.5` is playing at half + /// the sample rate, and `2.0` is playing at twice the sample rate. + /// + /// Returns the number of frames that were successfully filled. This may + /// be less than the length of `out_buffer_range` if the range is all or + /// partly out of bounds of the resource, or if a cache miss occured. + /// Any frames that were not successfully filled will be left untouched. + fn fill_buffers( + &mut self, + out_buffer: &mut [&mut [f32]], + out_buffer_range: Range, + start_frame: u64, + speed: f64, + is_playing_backwards: bool, + ) -> usize; + + /// Returns `true` if the given range of frames is loaded + /// into memory and ready to be read. + fn range_is_ready(&mut self, range: Range) -> bool; + + /// Request to cache a new region at the given starting frame. + fn cache_new_starting_frame(&mut self, frame: u64, speed: f64, will_play_backwards: bool); +} + /// A node that plays samples /// /// It supports pausing, resuming, looping, and changing the playback speed. -#[derive(Clone, Diff, Patch, PartialEq)] +#[derive(Debug, Clone, Copy, Diff, Patch, PartialEq)] #[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SamplerNode { - /// The sample resource to use. - #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] - #[cfg_attr(feature = "serde", serde(skip))] - pub sample: Option>, - /// The volume to play the sample at. /// /// Note, this gain parameter is *NOT* smoothed! If you need the gain to be @@ -158,7 +332,6 @@ pub struct SamplerNode { impl Default for SamplerNode { fn default() -> Self { Self { - sample: None, volume: Volume::default(), play: Default::default(), play_from: PlayFrom::default(), @@ -171,49 +344,59 @@ impl Default for SamplerNode { } } -impl core::fmt::Debug for SamplerNode { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let mut f = f.debug_struct("SamplerNode"); - f.field("has_sample", &self.sample.is_some()); - f.field("volume", &self.volume); - f.field("play", &self.play); - f.field("play_from", &self.play_from); - f.field("repeat_mode", &self.repeat_mode); - f.field("speed", &self.speed); - f.field("mono_to_stereo", &self.mono_to_stereo); - f.field("crossfade_on_seek", &self.crossfade_on_seek); - f.field("min_gain", &self.min_gain); - f.finish() +impl SamplerNode { + /// Returns an event to clear the sample resource from a sampler node. + pub fn clear_sample_event() -> NodeEventType { + NodeEventType::Custom(OwnedGc::new(Box::>::new(None))) } -} -impl SamplerNode { - /// Set the parameters to a play a single sample. - pub fn set_sample(&mut self, sample: ArcGc) { - self.sample = Some(sample); + /// Returns an event to set the sample resource for a sampler node from the + /// given sample resource. + pub fn set_sample_event(sample: T) -> NodeEventType { + Self::set_resource_event(SamplerNodeResource::from_sample(sample)) } - /// Returns an event type to sync the `sample` parameter. - pub fn sync_sample_event(&self) -> NodeEventType { - NodeEventType::Param { - data: ParamData::any(self.sample.clone()), - path: ParamPath::Single(0), - } + /// Returns an event to set the sample resource for a sampler node from the + /// given streamed sample resource. + pub fn set_streamed_sample_event(sample: T) -> NodeEventType { + Self::set_resource_event(SamplerNodeResource::from_streamed(sample)) + } + + /// Returns an event to set the sample resource for a sampler node from the + /// given type-erased sample resource. + pub fn set_dyn_sample_event( + sample: ArcGc, + ) -> NodeEventType { + Self::set_resource_event(sample.into()) + } + + /// Returns an event to set the sample resource for a sampler node from the + /// given type-erased streamed sample resource. + pub fn set_dyn_streamed_sample_event( + sample: OwnedGcUnsized, + ) -> NodeEventType { + Self::set_resource_event(sample.into()) + } + + /// Returns an event to set the sample resource for a sampler node. + pub fn set_resource_event(sample: SamplerNodeResource) -> NodeEventType { + NodeEventType::Custom(OwnedGc::new(Box::new(Some(sample)))) } /// Returns an event type to sync the `volume` parameter. pub fn sync_volume_event(&self) -> NodeEventType { NodeEventType::Param { data: ParamData::Volume(self.volume), - path: ParamPath::Single(1), + path: ParamPath::Single(0), } } /// Returns an event type to sync the `play` parameter. pub fn sync_play_event(&self) -> NodeEventType { NodeEventType::Param { + // TODO: This is not how `Patch` for `Notify` is implemented. data: ParamData::Bool(*self.play), - path: ParamPath::Single(2), + path: ParamPath::Single(1), } } @@ -221,7 +404,7 @@ impl SamplerNode { pub fn sync_play_from_event(&self) -> NodeEventType { NodeEventType::Param { data: self.play_from.as_param_data(), - path: ParamPath::Single(3), + path: ParamPath::Single(2), } } @@ -229,7 +412,7 @@ impl SamplerNode { pub fn sync_repeat_mode_event(&self) -> NodeEventType { NodeEventType::Param { data: ParamData::any(self.repeat_mode), - path: ParamPath::Single(4), + path: ParamPath::Single(3), } } @@ -237,10 +420,34 @@ impl SamplerNode { pub fn sync_speed_event(&self) -> NodeEventType { NodeEventType::Param { data: ParamData::F64(self.speed), + path: ParamPath::Single(4), + } + } + + /// Returns an event type to sync the `mono_to_stereo` parameter. + pub fn sync_mono_to_stereo_event(&self) -> NodeEventType { + NodeEventType::Param { + data: ParamData::Bool(self.mono_to_stereo), path: ParamPath::Single(5), } } + /// Returns an event type to sync the `crossfade_on_seek` parameter. + pub fn sync_crossfade_on_seek_event(&self) -> NodeEventType { + NodeEventType::Param { + data: ParamData::Bool(self.crossfade_on_seek), + path: ParamPath::Single(6), + } + } + + /// Returns an event type to sync the `min_gain` parameter. + pub fn sync_min_gain_event(&self) -> NodeEventType { + NodeEventType::Param { + data: ParamData::F32(self.min_gain), + path: ParamPath::Single(7), + } + } + /// Start/restart the sample in this node. /// /// If a sample is already playing, then it will restart from the beginning. @@ -373,6 +580,13 @@ impl SamplerState { ) } + /// Returns `true` if the processor currently has a sample resource. + pub fn has_sample_resource(&self) -> bool { + self.shared_state + .has_sample_resource + .load(Ordering::Relaxed) + } + /// Returns `true` if the sample is currently playing. pub fn playing(&self) -> bool { SharedPlaybackState::from_u32(self.shared_state.playback_state.load(Ordering::Relaxed)) @@ -429,36 +643,35 @@ impl SamplerState { /// 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 params.sample.is_some() { - let playback_state = SharedPlaybackState::from_u32( - self.shared_state.playback_state.load(Ordering::Relaxed), - ); + if !self.has_sample_resource() { + return u64::MAX; + } - if *params.play { - let playhead_frames = self.playhead_frames(); + let playback_state = + SharedPlaybackState::from_u32(self.shared_state.playback_state.load(Ordering::Relaxed)); - 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 - } + if *params.play { + let playhead_frames = self.playhead_frames(); + + if playback_state == SharedPlaybackState::Stopped { + if playhead_frames.0 > 0 { + // Sequence has likely finished playing. + u64::MAX - 4 } else { - // The older the sample is, the better it is as a candidate to steal - // work from. - playhead_frames.0 as u64 + // Sequence has likely not started playing yet. + u64::MAX - 5 } } else { - match playback_state { - SharedPlaybackState::Stopped => u64::MAX - 1, - SharedPlaybackState::Paused => u64::MAX - 2, - SharedPlaybackState::Playing => u64::MAX - 3, - } + // The older the sample is, the better it is as a candidate to steal + // work from. + playhead_frames.0 as u64 } } else { - u64::MAX + match playback_state { + SharedPlaybackState::Stopped => u64::MAX - 1, + SharedPlaybackState::Paused => u64::MAX - 2, + SharedPlaybackState::Playing => u64::MAX - 3, + } } } } @@ -515,7 +728,11 @@ impl Default for PlayFrom { impl Diff for PlayFrom { fn diff(&self, baseline: &Self, path: PathBuilder, event_queue: &mut E) { if self != baseline { - event_queue.push_param(self.as_param_data(), path); + match self { + Self::Resume => event_queue.push_param(ParamData::None, path), + Self::Seconds(seconds) => event_queue.push_param(*seconds, path), + Self::Frames(frames) => event_queue.push_param(*frames, path), + } } } } @@ -593,7 +810,7 @@ impl AudioNode for SamplerNode { Ok(SamplerProcessor { config: *config, - params: self.clone(), + params: *self, shared_state: ArcGc::clone(&cx.custom_state::().unwrap().shared_state), loaded_sample_state: None, declicker: Declicker::SettledAt1, @@ -607,9 +824,9 @@ impl AudioNode for SamplerNode { #[cfg(feature = "scheduled_events")] queued_playback_instant: None, min_gain: self.min_gain.max(0.0), - is_first_process: true, max_block_frames: cx.stream_info.max_block_frames.get() as usize, num_out_channels: config.channels.get().get() as usize, + is_first_process: true, }) } } @@ -638,9 +855,9 @@ struct SamplerProcessor { min_gain: f32, - is_first_process: bool, max_block_frames: usize, num_out_channels: usize, + is_first_process: bool, } impl SamplerProcessor { @@ -725,11 +942,18 @@ impl SamplerProcessor { }; if first_copy_frames > 0 { - state.sample.fill_buffers( - buffers, - range_in_buffer.start..range_in_buffer.start + first_copy_frames, - state.playhead_frames, - ); + match &mut state.sample { + SamplerNodeResource::InMemory(sample) => { + sample.fill_buffers( + buffers, + range_in_buffer.start..range_in_buffer.start + first_copy_frames, + state.playhead_frames, + ); + } + SamplerNodeResource::Streamed(_) => { + todo!() + } + } state.playhead_frames += first_copy_frames as u64; } @@ -743,12 +967,19 @@ impl SamplerProcessor { .min(state.sample_len_frames) as usize; - state.sample.fill_buffers( - buffers, - range_in_buffer.start + frames_copied - ..range_in_buffer.start + frames_copied + copy_frames, - 0, - ); + match &mut state.sample { + SamplerNodeResource::InMemory(sample) => { + sample.fill_buffers( + buffers, + range_in_buffer.start + frames_copied + ..range_in_buffer.start + frames_copied + copy_frames, + 0, + ); + } + SamplerNodeResource::Streamed(_) => { + todo!() + } + } state.playhead_frames = copy_frames as u64; state.num_times_looped_back += 1; @@ -769,7 +1000,7 @@ impl SamplerProcessor { } fn currently_processing_sample(&self) -> bool { - if self.params.sample.is_none() { + if self.loaded_sample_state.is_none() { false } else { self.playing || (self.paused && !self.declicker.has_settled()) @@ -837,14 +1068,16 @@ impl SamplerProcessor { } } - fn load_sample(&mut self, sample: ArcGc) { + fn load_sample(&mut self, sample: SamplerNodeResource) { let mut gain = self.params.volume.amp_clamped(self.min_gain); if gain > 0.99999 && gain < 1.00001 { gain = 1.0; } - let sample_len_frames = sample.len_frames(); - let sample_num_channels = sample.num_channels(); + let (sample_len_frames, sample_num_channels) = match &sample { + SamplerNodeResource::InMemory(s) => (s.len_frames(), s.num_channels()), + SamplerNodeResource::Streamed(s) => (s.len_frames(), s.num_channels()), + }; let sample_mono_to_stereo = self.params.mono_to_stereo && self.num_out_channels > 1 @@ -864,56 +1097,71 @@ impl SamplerProcessor { impl AudioNodeProcessor for SamplerProcessor { fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) { - let mut sample_changed = self.is_first_process; - let mut repeat_mode_changed = false; - let mut speed_changed = false; - let mut volume_changed = false; - let mut new_playing: Option = if self.is_first_process { + let is_first_process = self.is_first_process; + self.is_first_process = false; + + let mut new_playing: Option = if is_first_process { Some(self.playing) } else { None }; + let mut new_sample = None; + let mut repeat_mode_changed = false; + let mut speed_changed = false; + let mut volume_changed = false; #[cfg(feature = "scheduled_events")] let mut playback_instant: Option = None; - #[cfg(not(feature = "scheduled_events"))] - for patch in events.drain_patches::() { - match patch { - SamplerNodePatch::Sample(_) => sample_changed = true, - SamplerNodePatch::Volume(_) => volume_changed = true, - SamplerNodePatch::Play(play) => { - new_playing = Some(*play); - } - SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, - SamplerNodePatch::Speed(_) => speed_changed = true, - SamplerNodePatch::MinGain(min_gain) => { - self.min_gain = min_gain.max(0.0); - } - _ => {} + #[cfg(feature = "scheduled_events")] + for (mut event, timestamp) in events.drain_with_timestamps() { + let mut s = None; + if event.downcast_swap::>(&mut s) { + new_sample = Some(s); } - self.params.apply(patch); + if let Some(patch) = SamplerNode::patch_event(&event) { + match patch { + SamplerNodePatch::Volume(_) => volume_changed = true, + SamplerNodePatch::Play(play) => { + playback_instant = timestamp; + new_playing = Some(*play); + } + SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, + SamplerNodePatch::Speed(_) => speed_changed = true, + SamplerNodePatch::MinGain(min_gain) => { + self.min_gain = min_gain.max(0.0); + } + _ => {} + } + + self.params.apply(patch); + } } - #[cfg(feature = "scheduled_events")] - for (patch, timestamp) in events.drain_patches_with_timestamps::() { - match patch { - SamplerNodePatch::Sample(_) => sample_changed = true, - SamplerNodePatch::Volume(_) => volume_changed = true, - SamplerNodePatch::Play(play) => { - playback_instant = timestamp; - new_playing = Some(*play); - } - SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, - SamplerNodePatch::Speed(_) => speed_changed = true, - SamplerNodePatch::MinGain(min_gain) => { - self.min_gain = min_gain.max(0.0); - } - _ => {} + #[cfg(not(feature = "scheduled_events"))] + for mut event in events.drain() { + let mut s = None; + if event.downcast_swap::>(&mut s) { + new_sample = Some(s); } - self.params.apply(patch); + if let Some(patch) = SamplerNode::patch_event(&event) { + match patch { + SamplerNodePatch::Volume(_) => volume_changed = true, + SamplerNodePatch::Play(play) => { + new_playing = Some(*play); + } + SamplerNodePatch::RepeatMode(_) => repeat_mode_changed = true, + SamplerNodePatch::Speed(_) => speed_changed = true, + SamplerNodePatch::MinGain(min_gain) => { + self.min_gain = min_gain.max(0.0); + } + _ => {} + } + + self.params.apply(patch); + } } if speed_changed { @@ -939,7 +1187,11 @@ impl AudioNodeProcessor for SamplerProcessor { } } - if sample_changed { + if let Some(maybe_sample) = new_sample { + self.shared_state + .has_sample_resource + .store(maybe_sample.is_some(), Ordering::Relaxed); + self.stop(extra); #[cfg(feature = "scheduled_events")] @@ -953,8 +1205,8 @@ impl AudioNodeProcessor for SamplerProcessor { self.loaded_sample_state = None; - if let Some(sample) = &self.params.sample { - self.load_sample(ArcGc::clone(sample)); + if let Some(sample) = maybe_sample { + self.load_sample(sample); } } @@ -966,7 +1218,7 @@ impl AudioNodeProcessor for SamplerProcessor { if self.params.play_from == PlayFrom::Resume { // Resume - if self.playing && !self.is_first_process { + if self.playing && !is_first_process { // Sample is already playing, no need to do anything. #[cfg(feature = "scheduled_events")] { @@ -1099,8 +1351,6 @@ impl AudioNodeProcessor for SamplerProcessor { } as u32, Ordering::Relaxed, ); - - self.is_first_process = false; } fn bypassed(&mut self, _bypassed: bool) { @@ -1133,7 +1383,7 @@ impl AudioNodeProcessor for SamplerProcessor { let mut num_filled_channels = 0; - if currently_processing_sample && self.params.sample.is_some() { + if currently_processing_sample { let sample_state = self.loaded_sample_state.as_ref().unwrap(); let looping = self @@ -1235,7 +1485,6 @@ impl AudioNodeProcessor for SamplerProcessor { // The sample rate has changed, meaning that the sample resources now have // the incorrect sample rate and the user must reload them. - self.params.sample = None; self.loaded_sample_state = None; self.playing = false; self.paused = false; @@ -1249,6 +1498,7 @@ impl AudioNodeProcessor for SamplerProcessor { struct SharedState { sample_playhead_frames: AtomicU64, + has_sample_resource: AtomicBool, playback_state: AtomicU32, finished: AtomicU64, } @@ -1257,6 +1507,7 @@ impl Default for SharedState { fn default() -> Self { Self { sample_playhead_frames: AtomicU64::new(0), + has_sample_resource: AtomicBool::new(false), playback_state: AtomicU32::new(SharedPlaybackState::Stopped as u32), finished: AtomicU64::new(0), } @@ -1282,7 +1533,7 @@ impl SharedPlaybackState { } struct LoadedSampleState { - sample: ArcGc, + sample: SamplerNodeResource, sample_len_frames: u64, sample_num_channels: NonZeroUsize, sample_mono_to_stereo: bool, diff --git a/crates/firewheel-pool/src/lib.rs b/crates/firewheel-pool/src/lib.rs index 30da24c4..65766522 100644 --- a/crates/firewheel-pool/src/lib.rs +++ b/crates/firewheel-pool/src/lib.rs @@ -204,9 +204,9 @@ where self.workers.len() } - /// Queue a new work to play a sequence. + /// Queue new work to play a sequence. /// - /// * `params` - The parameters of the sequence to play. + /// * `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. @@ -215,6 +215,8 @@ where /// 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 add additional nodes to this worker instance. /// /// This will return an error if `params.playback == PlaybackState::Stop`. @@ -224,6 +226,7 @@ where #[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) { @@ -279,6 +282,8 @@ where 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(); @@ -288,6 +293,7 @@ where Ok(NewWorkerResult { worker_id, old_worker_id, + first_node_id: worker.first_node_id, was_playing_sequence, }) } @@ -619,6 +625,9 @@ pub struct NewWorkerResult { /// 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, diff --git a/crates/firewheel-symphonium/src/lib.rs b/crates/firewheel-symphonium/src/lib.rs index 14bf1aff..35d4ca9c 100644 --- a/crates/firewheel-symphonium/src/lib.rs +++ b/crates/firewheel-symphonium/src/lib.rs @@ -18,7 +18,7 @@ impl SymphoniumAudio { self.0.frames() as f64 / self.0.sample_rate().get() as f64 } - pub fn into_dyn_resource(self) -> ArcGc { + pub fn into_dyn_resource(self) -> ArcGc { self.into() } @@ -53,17 +53,26 @@ impl SampleResource for SymphoniumAudio { out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, - ) { - if start_frame > self.0.frames() as u64 { - return; - } - let start_frame = start_frame as usize; + ) -> usize { + let Some((frames, start_frame)) = firewheel_core::sample_resource::constrain_frames( + out_buffer_range.end - out_buffer_range.start, + start_frame, + self.0.frames(), + ) else { + return 0; + }; for (ch_i, out_ch) in out_buffer.iter_mut().enumerate().take(self.0.channels()) { self.0 - .fill_channel(ch_i, start_frame, &mut out_ch[out_buffer_range.clone()]) + .fill_channel( + ch_i, + start_frame, + &mut out_ch[out_buffer_range.start..out_buffer_range.start + frames], + ) .unwrap(); } + + frames } } @@ -83,7 +92,7 @@ impl SymphoniumAudioF32 { self.0.frames() as f64 / sample_rate.get() as f64 } - pub fn into_dyn_resource(self) -> ArcGc { + pub fn into_dyn_resource(self) -> ArcGc { self.into() } @@ -132,14 +141,14 @@ impl SampleResource for SymphoniumAudioF32 { out_buffer: &mut [&mut [f32]], out_buffer_range: Range, start_frame: u64, - ) { + ) -> usize { firewheel_core::sample_resource::fill_buffers_deinterleaved_f32( out_buffer, out_buffer_range, start_frame, &self.0.data, self.0.frames(), - ); + ) } } @@ -157,7 +166,9 @@ impl From for SymphoniumAudioF32 { /// A helper method to convert a [`symphonium::DecodedAudio`] resource into /// a type erased [`SampleResource`]. -pub fn dyn_symphonium_resource(data: symphonium::DecodedAudio) -> ArcGc { +pub fn dyn_symphonium_resource( + data: symphonium::DecodedAudio, +) -> ArcGc { SymphoniumAudio(data).into_dyn_resource() } @@ -165,6 +176,6 @@ pub fn dyn_symphonium_resource(data: symphonium::DecodedAudio) -> ArcGc ArcGc { +) -> ArcGc { SymphoniumAudioF32(data).into_dyn_resource() } diff --git a/examples/play_sample/src/main.rs b/examples/play_sample/src/main.rs index 2a439184..e96abc9a 100644 --- a/examples/play_sample/src/main.rs +++ b/examples/play_sample/src/main.rs @@ -62,17 +62,16 @@ fn main() { .unwrap(), ); - sampler_node.set_sample(sample); - cx.queue_event_for(sampler_id, sampler_node.sync_sample_event()); + cx.queue_event_for(sampler_id, SamplerNode::set_dyn_sample_event(sample)); sampler_node.start_or_restart(); cx.queue_event_for(sampler_id, sampler_node.sync_play_event()); - // Manually set the shared `stopped` flag. This is needed to account for the delay + // 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_stopped(); + .mark_playing(); // --- Simulated update loop --------------------------------------------------------- diff --git a/examples/sampler_pool/src/system.rs b/examples/sampler_pool/src/system.rs index c7e0d11c..9140cb69 100644 --- a/examples/sampler_pool/src/system.rs +++ b/examples/sampler_pool/src/system.rs @@ -1,5 +1,6 @@ use firewheel::{ channel_config::NonZeroChannelCount, + collector::ArcGc, cpal::CpalStream, diff::Memo, nodes::{ @@ -8,6 +9,7 @@ use firewheel::{ StereoToMonoNode, }, pool::{AudioNodePool, FxChain, SamplerPool, SamplerPoolVolumePan}, + sample_resource::SampleResource, FirewheelContext, }; @@ -27,6 +29,7 @@ pub struct AudioSystem { pub sampler_pool_1: SamplerPoolVolumePan, pub sampler_pool_2: AudioNodePool, pub sampler_node: SamplerNode, + pub sample: ArcGc, } impl AudioSystem { @@ -74,8 +77,7 @@ impl AudioSystem { .unwrap(), ); - let mut sampler_node = SamplerNode::default(); - sampler_node.set_sample(sample); + let sampler_node = SamplerNode::default(); // Note, you can get the playhead and other state of a worker like this: // let playhead = sampler_pool_1 @@ -89,6 +91,7 @@ impl AudioSystem { sampler_pool_1, sampler_pool_2, sampler_node, + sample, } } diff --git a/examples/sampler_pool/src/ui.rs b/examples/sampler_pool/src/ui.rs index eb2d135a..37420c6e 100644 --- a/examples/sampler_pool/src/ui.rs +++ b/examples/sampler_pool/src/ui.rs @@ -1,5 +1,9 @@ use eframe::App; -use firewheel::{nodes::volume_pan::VolumePanNode, Volume}; +use firewheel::{ + diff::EventQueue, + nodes::{sampler::SamplerNode, volume_pan::VolumePanNode}, + Volume, +}; use crate::system::AudioSystem; @@ -45,6 +49,12 @@ impl App for DemoApp { 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 @@ -77,6 +87,12 @@ impl App for DemoApp { 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 diff --git a/examples/sampler_test/src/system.rs b/examples/sampler_test/src/system.rs index e5247956..7a288beb 100644 --- a/examples/sampler_test/src/system.rs +++ b/examples/sampler_test/src/system.rs @@ -73,13 +73,14 @@ impl AudioSystem { .unwrap(), ); - let mut params = SamplerNode::default(); - params.set_sample(sample); + let params = SamplerNode::default(); let node_id = cx .add_node(params.clone(), None) .expect("Sampler node should construct without error"); + cx.queue_event_for(node_id, SamplerNode::set_dyn_sample_event(sample)); + cx.connect(node_id, peak_meter_id, &[(0, 0), (1, 1)], false) .unwrap(); diff --git a/examples/spatial_basic/src/system.rs b/examples/spatial_basic/src/system.rs index cd485a06..4e0f1971 100644 --- a/examples/spatial_basic/src/system.rs +++ b/examples/spatial_basic/src/system.rs @@ -46,7 +46,6 @@ impl AudioSystem { let graph_out_node_id = cx.graph_out_node_id(); let mut sampler_node = SamplerNode::default(); - sampler_node.set_sample(sample); sampler_node.repeat_mode = RepeatMode::RepeatEndlessly; sampler_node.start_or_restart(); @@ -54,6 +53,8 @@ impl AudioSystem { .add_node(sampler_node.clone(), None) .expect("Sampler node should construct without error"); + cx.queue_event_for(sampler_node_id, SamplerNode::set_dyn_sample_event(sample)); + let spatial_basic_node = SpatialBasicNode::default(); let spatial_basic_node_id = cx .add_node(spatial_basic_node, None) diff --git a/examples/visual_node_graph/src/system.rs b/examples/visual_node_graph/src/system.rs index 502c5034..f7e5bcf5 100644 --- a/examples/visual_node_graph/src/system.rs +++ b/examples/visual_node_graph/src/system.rs @@ -57,8 +57,11 @@ pub enum NodeType { pub struct AudioSystem { pub cx: FirewheelContext, pub stream: CpalStream, - pub(crate) samples: Vec>, - pub(crate) ir_samples: Vec<(&'static str, ArcGc)>, + pub(crate) samples: Vec>, + pub(crate) ir_samples: Vec<( + &'static str, + ArcGc, + )>, } const IR_SAMPLE_PATHS: [&'static str; 2] = [ @@ -100,6 +103,7 @@ impl AudioSystem { ) .unwrap(), ) + .into() }) .collect(); diff --git a/examples/visual_node_graph/src/ui.rs b/examples/visual_node_graph/src/ui.rs index 184a3c08..0bac947c 100644 --- a/examples/visual_node_graph/src/ui.rs +++ b/examples/visual_node_graph/src/ui.rs @@ -647,11 +647,26 @@ impl<'a> SnarlViewer for DemoViewer<'a> { }) .wrap_mode(egui::TextWrapMode::Truncate) .show_ui(ui, |ui| { + let mut tmp_selection = selection; + + if ui + .selectable_value(&mut tmp_selection, None, "None") + .clicked() + { + ui.memory_mut(|mem| { + mem.data.insert_temp::>(mem_id, None); + }); + + self.audio_system + .cx + .queue_event_for(node.id, SamplerNode::clear_sample_event()); + } + for sample_index in 0..SAMPLE_PATHS.len() { if ui .selectable_value( - &mut params.sample, - Some(self.audio_system.samples[sample_index].clone()), + &mut tmp_selection, + Some(sample_index), SAMPLE_PATHS[sample_index].rsplit("/").next().unwrap(), ) .clicked() @@ -662,8 +677,12 @@ impl<'a> SnarlViewer for DemoViewer<'a> { Some(sample_index), ); }); - params.set_sample( - self.audio_system.samples[sample_index].clone(), + + self.audio_system.cx.queue_event_for( + node.id, + SamplerNode::set_dyn_sample_event( + self.audio_system.samples[sample_index].clone(), + ), ); } } diff --git a/examples/visualizer/src/system.rs b/examples/visualizer/src/system.rs index 49f51e7b..dbdf5526 100644 --- a/examples/visualizer/src/system.rs +++ b/examples/visualizer/src/system.rs @@ -49,11 +49,11 @@ impl AudioSystem { .unwrap(), ); - let mut sampler_params = SamplerNode::default(); - sampler_params.set_sample(sample); + let sampler_params = SamplerNode::default(); let sampler_node_id = cx .add_node(sampler_params.clone(), None) .expect("Sampler node should construct without error"); + cx.queue_event_for(sampler_node_id, SamplerNode::set_dyn_sample_event(sample)); let triple_buffer_params = TripleBufferNode { window_size: WindowSize::Samples(window_size),