diff --git a/crates/firewheel-core/src/clock.rs b/crates/firewheel-core/src/clock.rs index 819d1afa..77680cd8 100644 --- a/crates/firewheel-core/src/clock.rs +++ b/crates/firewheel-core/src/clock.rs @@ -5,10 +5,6 @@ use bevy_platform::time::Instant; use core::num::NonZeroU32; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; -#[cfg(feature = "scheduled_events")] -use crate::diff::{Diff, Patch}; -#[cfg(feature = "scheduled_events")] -use crate::event::ParamData; #[cfg(feature = "scheduled_events")] use crate::node::ProcInfo; @@ -51,6 +47,31 @@ pub enum EventInstant { /// triggered at the lowest latency possible. DelaySamples(DurationSamples), + /// The event should happen the given number of seconds after the + /// last [`NodeEventType::Marker`](crate::event::NodeEventType::Marker) + /// event that was sent to this node. + /// + /// This can be useful for creating a sequence of rapid-fire events that are + /// triggered with the lowest latency possible. + /// + /// If a [`NodeEventType::Marker`](crate::event::NodeEventType::Marker) + /// event was never sent to this node, then the start of the stream will be used + /// as the marker. + DelaySecondsFromMarker(DurationSeconds), + + /// The event should happen the given number of samples (of a single channel + /// of audio) after the + /// [`NodeEventType::Marker`](crate::event::NodeEventType::Marker) + /// event that was sent to this node. + /// + /// This can be useful for creating a sequence of rapid-fire events that are + /// triggered with the lowest latency possible. + /// + /// If a [`NodeEventType::Marker`](crate::event::NodeEventType::Marker) + /// event was never sent to this node, then the start of the stream will be used + /// as the marker. + DelaySamplesFromMarker(DurationSamples), + /// The event should happen when the musical clock reaches the given /// musical time. #[cfg(feature = "musical_transport")] @@ -69,9 +90,9 @@ impl EventInstant { /// Convert the instant to the given time in samples. /// - /// If this instant is of type [`EventInstant::AtClockMusical`] and either - /// there is no musical transport or the musical transport is not - /// currently playing, then this will return `None`. + /// This may return `None` if this instant is of type [`EventInstant::AtClockMusical`] + /// and either there is no musical transport or the musical transport is not currently + /// playing. pub fn to_samples(&self, proc_info: &ProcInfo) -> Option { match self { EventInstant::AtClockSamples(samples) => Some(*samples), @@ -82,6 +103,12 @@ impl EventInstant { EventInstant::DelaySeconds(seconds) => { Some(proc_info.clock_samples + seconds.to_samples(proc_info.sample_rate)) } + EventInstant::DelaySamplesFromMarker(samples) => { + Some(proc_info.last_marker_instant + *samples) + } + EventInstant::DelaySecondsFromMarker(seconds) => { + Some(proc_info.last_marker_instant + seconds.to_samples(proc_info.sample_rate)) + } #[cfg(feature = "musical_transport")] EventInstant::AtClockMusical(musical) => proc_info.musical_to_samples(*musical), } @@ -123,91 +150,6 @@ impl From for EventInstant { } } -#[cfg(feature = "scheduled_events")] -impl Diff for EventInstant { - fn diff( - &self, - baseline: &Self, - path: crate::diff::PathBuilder, - event_queue: &mut E, - ) { - if self != baseline { - match self { - EventInstant::AtClockSeconds(s) => event_queue.push_param(*s, path), - EventInstant::AtClockSamples(s) => event_queue.push_param(*s, path), - EventInstant::DelaySeconds(s) => event_queue.push_param(*s, path), - EventInstant::DelaySamples(s) => event_queue.push_param(*s, path), - #[cfg(feature = "musical_transport")] - EventInstant::AtClockMusical(m) => event_queue.push_param(*m, path), - } - } - } -} - -#[cfg(feature = "scheduled_events")] -impl Patch for EventInstant { - type Patch = Self; - - fn patch(data: &ParamData, _path: &[u32]) -> Result { - match data { - ParamData::InstantSeconds(s) => Ok(EventInstant::AtClockSeconds(*s)), - ParamData::InstantSamples(s) => Ok(EventInstant::AtClockSamples(*s)), - ParamData::DurationSeconds(s) => Ok(EventInstant::DelaySeconds(*s)), - ParamData::DurationSamples(s) => Ok(EventInstant::DelaySamples(*s)), - #[cfg(feature = "musical_transport")] - ParamData::InstantMusical(s) => Ok(EventInstant::AtClockMusical(*s)), - _ => Err(crate::diff::PatchError::InvalidData), - } - } - - fn apply(&mut self, patch: Self::Patch) { - *self = patch; - } -} - -#[cfg(feature = "scheduled_events")] -impl Diff for Option { - fn diff( - &self, - baseline: &Self, - path: crate::diff::PathBuilder, - event_queue: &mut E, - ) { - if self != baseline { - match self { - Some(EventInstant::AtClockSeconds(s)) => event_queue.push_param(*s, path), - Some(EventInstant::AtClockSamples(s)) => event_queue.push_param(*s, path), - Some(EventInstant::DelaySeconds(s)) => event_queue.push_param(*s, path), - Some(EventInstant::DelaySamples(s)) => event_queue.push_param(*s, path), - #[cfg(feature = "musical_transport")] - Some(EventInstant::AtClockMusical(m)) => event_queue.push_param(*m, path), - None => event_queue.push_param(ParamData::None, path), - } - } - } -} - -#[cfg(feature = "scheduled_events")] -impl Patch for Option { - type Patch = Self; - - fn patch(data: &ParamData, _path: &[u32]) -> Result { - match data { - ParamData::InstantSeconds(s) => Ok(Some(EventInstant::AtClockSeconds(*s))), - ParamData::InstantSamples(s) => Ok(Some(EventInstant::AtClockSamples(*s))), - ParamData::DurationSeconds(s) => Ok(Some(EventInstant::DelaySeconds(*s))), - ParamData::DurationSamples(s) => Ok(Some(EventInstant::DelaySamples(*s))), - #[cfg(feature = "musical_transport")] - ParamData::InstantMusical(s) => Ok(Some(EventInstant::AtClockMusical(*s))), - _ => Err(crate::diff::PatchError::InvalidData), - } - } - - fn apply(&mut self, patch: Self::Patch) { - *self = patch; - } -} - /// An absolute audio clock instant in units of seconds. #[repr(transparent)] #[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)] diff --git a/crates/firewheel-core/src/clock/transport.rs b/crates/firewheel-core/src/clock/transport.rs index 22c279cb..a20dd42b 100644 --- a/crates/firewheel-core/src/clock/transport.rs +++ b/crates/firewheel-core/src/clock/transport.rs @@ -253,6 +253,10 @@ pub struct SpeedMultiplierKeyframe { pub multiplier: f64, /// The instant that this keyframe happens. + /// + /// Note, [`EventInstant::DelaySecondsFromMarker`] and + /// [`EventInstant::DelaySamplesFromMarker`] cannot be used here, and + /// will result in a panic. pub instant: EventInstant, } diff --git a/crates/firewheel-core/src/collector.rs b/crates/firewheel-core/src/collector.rs index 1d4448be..0ec5e20c 100644 --- a/crates/firewheel-core/src/collector.rs +++ b/crates/firewheel-core/src/collector.rs @@ -198,7 +198,7 @@ impl StrongCount for Arc { /// # Example /// /// ```rust -/// # use rtgc::*; +/// # use firewheel_core::collector::*; /// # use std::time::Duration; /// let value: ArcGc = ArcGc::new(String::from("foo")); /// @@ -230,7 +230,7 @@ impl ArcGc { /// Construct a new [`ArcGc`]. /// /// ``` - /// # use rtgc::*; + /// # use firewheel_core::collector::*; /// let value: ArcGc = ArcGc::new(String::from("foo")); /// ``` pub fn new(value: T) -> Self { @@ -249,7 +249,7 @@ impl ArcGc { /// Construct a new [`ArcGc`] with _unsized_ data, such as `[T]` or `dyn Trait`. /// /// ``` - /// # use rtgc::*; + /// # use firewheel_core::collector::*; /// # use std::sync::Arc; /// let value_1: ArcGc<[f32]> = ArcGc::new_unsized( /// || Arc::<[f32]>::from([1.0, 2.0, 3.0]), @@ -279,7 +279,7 @@ impl ArcGc { /// Construct a type-erased [`ArcGc`]. /// /// ``` - /// # use firewheel-core::collector::ArcGC; + /// # use firewheel_core::collector::*; /// # use std::sync::Arc; /// # use std::any::Any; /// let value: ArcGc = @@ -348,7 +348,7 @@ impl Debug for ArcGc = OwnedGc::new(String::from("foo")); /// @@ -473,7 +473,7 @@ unsafe impl Sync for OwnedGcWrapper {} /// # Example /// /// ```rust -/// # use rtgc::*; +/// # use firewheel_core::collector::*; /// # use std::time::Duration; /// let value_1: OwnedGcUnsized<[f32]> = OwnedGcUnsized::new_unsized( /// vec![0.0, 1.0, 2.0].into_boxed_slice(), @@ -516,7 +516,7 @@ impl OwnedGcUnsized { /// Construct a new [`OwnedGcUnsized`] with _unsized_ data, such as `[T]` or `dyn Trait`. /// /// ``` - /// # use rtgc::*; + /// # use firewheel_core::collector::*; /// # use std::sync::Arc; /// let value_1: OwnedGcUnsized<[f32]> = OwnedGcUnsized::new_unsized( /// vec![0.0, 1.0, 2.0].into_boxed_slice(), diff --git a/crates/firewheel-core/src/diff/leaf.rs b/crates/firewheel-core/src/diff/leaf.rs index 55b6c161..c9e0c3f6 100644 --- a/crates/firewheel-core/src/diff/leaf.rs +++ b/crates/firewheel-core/src/diff/leaf.rs @@ -10,6 +10,9 @@ use crate::{ vector::{Vec2, Vec3}, }; +#[cfg(feature = "scheduled_events")] +use crate::clock::EventInstant; + #[cfg(feature = "musical_transport")] use crate::clock::{DurationMusical, InstantMusical}; @@ -149,6 +152,9 @@ primitive_diff!(DurationSamples, DurationSamples); primitive_diff!(InstantSeconds, InstantSeconds); primitive_diff!(DurationSeconds, DurationSeconds); +#[cfg(feature = "scheduled_events")] +primitive_diff!(EventInstant, EventInstant); + #[cfg(feature = "musical_transport")] primitive_diff!(InstantMusical, InstantMusical); #[cfg(feature = "musical_transport")] diff --git a/crates/firewheel-core/src/diff/mod.rs b/crates/firewheel-core/src/diff/mod.rs index b5f63de1..501c2bce 100644 --- a/crates/firewheel-core/src/diff/mod.rs +++ b/crates/firewheel-core/src/diff/mod.rs @@ -421,21 +421,16 @@ impl core::ops::Deref for ParamPath { /// } /// /// impl AudioNodeProcessor for MyProcessor { -/// fn process( +/// fn events( /// &mut self, /// info: &ProcInfo, -/// buffers: ProcBuffers, /// events: &mut ProcEvents, /// extra: &mut ProcExtra, -/// ) -> ProcessStatus { +/// ) { /// // Synchronize `params` from the event list. /// for patch in events.drain_patches::() { /// self.params.apply(patch); /// } -/// -/// // ... -/// -/// ProcessStatus::OutputsModified /// } /// } /// ``` @@ -454,13 +449,12 @@ impl core::ops::Deref for ParamPath { /// # params: MyParams, /// # } /// impl AudioNodeProcessor for MyProcessor { -/// fn process( +/// fn events( /// &mut self, /// info: &ProcInfo, -/// buffers: ProcBuffers, /// events: &mut ProcEvents, /// extra: &mut ProcExtra, -/// ) -> ProcessStatus { +/// ) { /// for mut patch in events.drain_patches::() { /// // When you derive `Patch`, it creates an enum with variants /// // for each field. @@ -476,10 +470,6 @@ impl core::ops::Deref for ParamPath { /// // And / or apply it directly. /// self.params.apply(patch); /// } -/// -/// // ... -/// -/// ProcessStatus::OutputsModified /// } /// } /// ``` diff --git a/crates/firewheel-core/src/dsp/algo.rs b/crates/firewheel-core/src/dsp/algo.rs index bfcf254e..6a32130f 100644 --- a/crates/firewheel-core/src/dsp/algo.rs +++ b/crates/firewheel-core/src/dsp/algo.rs @@ -7,8 +7,8 @@ pub fn max_peak(data: &[f32]) -> f32 { // Processing in chunks like this breaks the dependency chain which allows // the compiler to properly auto-vectorize this loop. let mut tmp = [0.0; CHUNK]; - let mut iter = data.chunks_exact(CHUNK); - for chunk in iter.by_ref() { + let (chunked_data, remainder) = data.as_chunks::(); + for chunk in chunked_data.iter() { for i in 0..CHUNK { let abs = chunk[i].abs(); if abs > tmp[i] { @@ -24,7 +24,7 @@ pub fn max_peak(data: &[f32]) -> f32 { } } - for &s in iter.remainder() { + for &s in remainder.iter() { let abs = s.abs(); if abs > res { res = abs; diff --git a/crates/firewheel-core/src/event.rs b/crates/firewheel-core/src/event.rs index 5dd6bbcc..803656f9 100644 --- a/crates/firewheel-core/src/event.rs +++ b/crates/firewheel-core/src/event.rs @@ -81,6 +81,14 @@ pub enum NodeEventType { Custom(OwnedGc>), /// Custom event type stored on the stack as raw bytes. CustomBytes([u8; 36]), + /// The instant the Firewheel processor receives this event is used as the marker for + /// future events scheduled with [`EventInstant::DelaySecondsFromMarker`] and + /// [`EventInstant::DelaySamplesFromMarker`]. + /// + /// Note, this only applies if [`NodeEvent::time`] is `None`. If [`NodeEvent::time`] + /// is not `None`, then this event will be discarded. + #[cfg(feature = "scheduled_events")] + Marker, #[cfg(feature = "midi_events")] MIDI(MidiMessage<'static>), } @@ -166,6 +174,8 @@ impl core::fmt::Debug for NodeEventType { NodeEventType::Custom(_) => f.debug_tuple("Custom").finish_non_exhaustive(), NodeEventType::CustomBytes(f0) => f.debug_tuple("CustomBytes").field(&f0).finish(), NodeEventType::SetBypassed(b) => f.debug_tuple("SetBypassed").field(&b).finish(), + #[cfg(feature = "scheduled_events")] + NodeEventType::Marker => f.write_str("Marker"), #[cfg(feature = "midi_events")] NodeEventType::MIDI(f0) => f.debug_tuple("MIDI").field(&f0).finish(), } diff --git a/crates/firewheel-core/src/node.rs b/crates/firewheel-core/src/node.rs index 214f1315..0b005d23 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -791,6 +791,14 @@ pub struct ProcInfo { /// If the node has just been un-bypassed, then this will be `true`. pub did_just_unbypass: bool, + /// The instant that the last [`NodeEventType::Marker`] event that + /// was sent to this node occured at. + /// + /// If a [`NodeEventType::Marker`] was never sent to this node, then + /// this will be `InstantSamples(0)`. + #[cfg(feature = "scheduled_events")] + pub last_marker_instant: InstantSamples, + /// Information about the musical transport. /// /// This will be `None` if no musical transport is currently active, diff --git a/crates/firewheel-core/src/sample_resource.rs b/crates/firewheel-core/src/sample_resource.rs index 72aecb9d..d9573628 100644 --- a/crates/firewheel-core/src/sample_resource.rs +++ b/crates/firewheel-core/src/sample_resource.rs @@ -213,7 +213,9 @@ pub fn fill_buffers_interleaved( let src_slice = &resource[start_frame * 2..(start_frame + frames) * 2]; for (src_chunk, (buf0_s, buf1_s)) in src_slice - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .zip(buf0.iter_mut().zip(buf1.iter_mut())) { *buf0_s = src_chunk[0].to_scaled_float(); diff --git a/crates/firewheel-graph/Cargo.toml b/crates/firewheel-graph/Cargo.toml index 09901091..6c1528f7 100644 --- a/crates/firewheel-graph/Cargo.toml +++ b/crates/firewheel-graph/Cargo.toml @@ -16,8 +16,7 @@ exclude.workspace = true all-features = true [features] -# TODO: Remove "node_profiling" from default features. -default = ["std", "tracing", "node_profiling"] +default = ["std", "tracing"] std = [ "arrayvec/std", "bevy_platform/std", diff --git a/crates/firewheel-graph/src/context.rs b/crates/firewheel-graph/src/context.rs index df1cc3eb..0a6bff86 100644 --- a/crates/firewheel-graph/src/context.rs +++ b/crates/firewheel-graph/src/context.rs @@ -1413,8 +1413,8 @@ impl Drop for FirewheelContext { /// /// ``` /// # use firewheel_core::{diff::{Diff, PathBuilder}, node::NodeID}; -/// # use firewheel_graph::{backend::AudioBackend, FirewheelContext, ContextQueue}; -/// # fn context_queue( +/// # use firewheel_graph::{FirewheelContext, ContextQueue}; +/// # fn context_queue( /// # context: &mut FirewheelContext, /// # node_id: NodeID, /// # params: &D, diff --git a/crates/firewheel-graph/src/processor/event_scheduler.rs b/crates/firewheel-graph/src/processor/event_scheduler.rs index 845a876d..31af51ae 100644 --- a/crates/firewheel-graph/src/processor/event_scheduler.rs +++ b/crates/firewheel-graph/src/processor/event_scheduler.rs @@ -133,6 +133,11 @@ impl EventScheduler { ) { #[cfg(feature = "scheduled_events")] if let Some(event_instant) = event.time { + // Sending a `Marker` event with an `EventInstant` is invalid. + if let NodeEventType::Marker = &event.event { + return; + } + let slot = if let Some(slot) = self.scheduled_event_arena_free_slots.pop() { slot } else { @@ -169,6 +174,18 @@ impl EventScheduler { clock_samples + seconds.to_samples(sample_rate) } + EventInstant::DelaySamplesFromMarker(samples) => { + self.num_scheduled_non_musical_events += 1; + node_data.num_scheduled_non_musical_events += 1; + + node_data.last_marker_instant + samples + } + EventInstant::DelaySecondsFromMarker(seconds) => { + self.num_scheduled_non_musical_events += 1; + node_data.num_scheduled_non_musical_events += 1; + + node_data.last_marker_instant + seconds.to_samples(sample_rate) + } #[cfg(feature = "musical_transport")] EventInstant::AtClockMusical(musical) => { self.num_scheduled_musical_events += 1; @@ -196,6 +213,8 @@ impl EventScheduler { self.sorted_event_buffer_indices.push((slot, time_samples)); return; + } else if let NodeEventType::Marker = &event.event { + node_data.last_marker_instant = clock_samples; } if self.immediate_event_buffer.len() == self.immediate_event_buffer_capacity { @@ -576,12 +595,11 @@ impl EventScheduler { mut proc_buffers: ProcBuffers, mut on_sub_chunk: impl FnMut(ProcessSubChunkInfo), ) { + // Rust is getting confused when the cfg is put on the argument for some reason, + // so just duplicate the closure. + #[cfg(not(feature = "scheduled_events"))] let push_event = |node_event_queue: &mut Vec, immediate_event_buffer: &[Option], - #[cfg(feature = "scheduled_events")] - scheduled_event_arena: &[Option< - ScheduledEventEntry, - >], event: ProcEventsIndex, logger: &mut RealtimeLogger, set_bypassed: &mut Option| { @@ -596,7 +614,45 @@ impl EventScheduler { return; } } - #[cfg(feature = "scheduled_events")] + } + + if node_event_queue.len() == node_event_queue.capacity() { + match self.buffer_out_of_space_mode { + BufferOutOfSpaceMode::AllocateOnAudioThread => { + let _ = logger.try_error("Firewheel event queue is full! Please increase FirewheelConfig::event_queue_capacity to avoid audio glitches."); + } + BufferOutOfSpaceMode::Panic => { + panic!( + "Firewheel event queue is full! Please increase FirewheelConfig::event_queue_capacity." + ); + } + BufferOutOfSpaceMode::DropEvents => { + let _ = logger.try_error("Firewheel event queue is full and event was dropped! Please increase FirewheelConfig::event_queue_capacity."); + } + } + } + + node_event_queue.push(event); + }; + + #[cfg(feature = "scheduled_events")] + let push_event = |node_event_queue: &mut Vec, + immediate_event_buffer: &[Option], + scheduled_event_arena: &[Option], + event: ProcEventsIndex, + logger: &mut RealtimeLogger, + set_bypassed: &mut Option| { + match event { + ProcEventsIndex::Immediate(i) => { + if let Some(event) = immediate_event_buffer + .get(i as usize) + .and_then(|e| e.as_ref()) + && let NodeEventType::SetBypassed(bypassed) = &event.event + { + *set_bypassed = Some(*bypassed); + return; + } + } ProcEventsIndex::Scheduled(i) => { if let Some(event) = scheduled_event_arena .get(i as usize) @@ -910,6 +966,8 @@ pub(super) struct NodeEventSchedulerData { num_scheduled_events_this_block: usize, #[cfg(feature = "scheduled_events")] first_sorted_event_index: usize, + #[cfg(feature = "scheduled_events")] + pub last_marker_instant: InstantSamples, #[allow(unused)] is_pre_process: bool, @@ -928,6 +986,8 @@ impl NodeEventSchedulerData { num_scheduled_events_this_block: 0, #[cfg(feature = "scheduled_events")] first_sorted_event_index: 0, + #[cfg(feature = "scheduled_events")] + last_marker_instant: InstantSamples(0), is_pre_process, } } diff --git a/crates/firewheel-graph/src/processor/process.rs b/crates/firewheel-graph/src/processor/process.rs index 19b369a5..e65862bf 100644 --- a/crates/firewheel-graph/src/processor/process.rs +++ b/crates/firewheel-graph/src/processor/process.rs @@ -300,6 +300,8 @@ impl FirewheelProcessorInner { dropped_frames, process_to_playback_delay, did_just_unbypass: false, + #[cfg(feature = "scheduled_events")] + last_marker_instant: InstantSamples(0), #[cfg(feature = "musical_transport")] transport_info, }; @@ -345,6 +347,11 @@ impl FirewheelProcessorInner { info.in_connected_mask = in_connected_mask; info.out_connected_mask = out_connected_mask; + #[cfg(feature = "scheduled_events")] + { + info.last_marker_instant = node_entry.event_data.last_marker_instant; + } + // Used to keep track of what status this closure should return. let mut prev_process_status = None; let mut final_mask = None; diff --git a/crates/firewheel-graph/src/processor/transport.rs b/crates/firewheel-graph/src/processor/transport.rs index 249cf6b6..353d30b4 100644 --- a/crates/firewheel-graph/src/processor/transport.rs +++ b/crates/firewheel-graph/src/processor/transport.rs @@ -232,6 +232,12 @@ impl ProcTransportState { clock_samples + seconds.to_samples(sample_rate) } EventInstant::DelaySamples(samples) => clock_samples + samples, + EventInstant::DelaySamplesFromMarker(_) + | EventInstant::DelaySecondsFromMarker(_) => { + panic!( + "DelaySecondsFromMarker/DelaySamplesFromMarker cannot be used for SpeedMultiplierKeyframe" + ); + } EventInstant::AtClockMusical(musical) => transport.musical_to_samples( musical, self.transport_start_samples, diff --git a/crates/firewheel-macros/src/firewheel_manifest.rs b/crates/firewheel-macros/src/firewheel_manifest.rs index 605ae600..e83925b3 100644 --- a/crates/firewheel-macros/src/firewheel_manifest.rs +++ b/crates/firewheel-macros/src/firewheel_manifest.rs @@ -48,10 +48,9 @@ impl FirewheelManifest { let find_in_deps = |deps: &Item| -> Option { let package = if let Some(dep) = deps.get(name) { return Some(Self::parse_str(dep_package(dep).unwrap_or(name))); - } else if let Some(dep) = deps.get(FIREWHEEL) { - dep_package(dep).unwrap_or(FIREWHEEL) } else { - return None; + let dep = deps.get(FIREWHEEL)?; + dep_package(dep).unwrap_or(FIREWHEEL) }; let mut path = Self::parse_str::(package);