From 97683bd1353536a4305fa38aa98377d53f2c3a9f Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 2 Sep 2026 15:08:43 -0500 Subject: [PATCH 1/7] add "DelayFromLastOrigin" EventInstant variants --- crates/firewheel-core/src/clock.rs | 124 +++++------------- crates/firewheel-core/src/clock/transport.rs | 4 + crates/firewheel-core/src/diff/leaf.rs | 6 + crates/firewheel-core/src/event.rs | 10 ++ crates/firewheel-core/src/node.rs | 8 ++ .../src/processor/event_scheduler.rs | 70 +++++++++- .../firewheel-graph/src/processor/process.rs | 7 + .../src/processor/transport.rs | 6 + 8 files changed, 138 insertions(+), 97 deletions(-) diff --git a/crates/firewheel-core/src/clock.rs b/crates/firewheel-core/src/clock.rs index 819d1afa..c24d718d 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,29 @@ pub enum EventInstant { /// triggered at the lowest latency possible. DelaySamples(DurationSamples), + /// The event should happen the given number of seconds after the + /// last [`NodeEventType::DelayOrigin`] 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::DelayOrigin`] event was never sent to this node, + /// then the start of the stream will be used as the origin (effectively + /// making this behave like [`EventInstant::AtClockSeconds`]). + DelaySecondsFromLastOrigin(DurationSeconds), + + /// The event should happen the given number of samples (of a single channel + /// of audio) after the [`NodeEventType::DelayOrigin`] 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::DelayOrigin`] event was never sent to this node, + /// then the start of the stream will be used as the origin (effectively + /// making this behave like [`EventInstant::AtClockSamples`]). + DelaySamplesFromLastOrigin(DurationSamples), + /// The event should happen when the musical clock reaches the given /// musical time. #[cfg(feature = "musical_transport")] @@ -69,9 +88,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 +101,12 @@ impl EventInstant { EventInstant::DelaySeconds(seconds) => { Some(proc_info.clock_samples + seconds.to_samples(proc_info.sample_rate)) } + EventInstant::DelaySamplesFromLastOrigin(samples) => { + Some(proc_info.last_delay_origin + *samples) + } + EventInstant::DelaySecondsFromLastOrigin(seconds) => { + Some(proc_info.last_delay_origin + seconds.to_samples(proc_info.sample_rate)) + } #[cfg(feature = "musical_transport")] EventInstant::AtClockMusical(musical) => proc_info.musical_to_samples(*musical), } @@ -123,91 +148,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..020c8e91 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::DelaySecondsFromLastOrigin`] and + /// [`EventInstant::DelaySamplesFromLastOrigin`] cannot be used here, and + /// will result in a panic. pub instant: EventInstant, } 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/event.rs b/crates/firewheel-core/src/event.rs index 5dd6bbcc..e7e8d3b0 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 origin for + /// future events scheduled with [`EventInstant::DelaySecondsFrom`] and + /// [`EventInstant::DelaySamplesFrom`]. + /// + /// 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")] + DelayOrigin, #[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::DelayOrigin => f.write_str("DelayOrigin"), #[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..4b467271 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::DelayOrigin`] event that + /// was sent to this node occured at. + /// + /// If a [`NodeEventType::DelayOrigin`] was never sent to this node, then + /// this will be `InstantSamples(0)`. + #[cfg(feature = "scheduled_events")] + pub last_delay_origin: InstantSamples, + /// Information about the musical transport. /// /// This will be `None` if no musical transport is currently active, diff --git a/crates/firewheel-graph/src/processor/event_scheduler.rs b/crates/firewheel-graph/src/processor/event_scheduler.rs index 845a876d..d74de1af 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 `DelayOrigin` event with an `EventInstant` is invalid. + if let NodeEventType::DelayOrigin = &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::DelaySamplesFromLastOrigin(samples) => { + self.num_scheduled_non_musical_events += 1; + node_data.num_scheduled_non_musical_events += 1; + + node_data.last_delay_origin + samples + } + EventInstant::DelaySecondsFromLastOrigin(seconds) => { + self.num_scheduled_non_musical_events += 1; + node_data.num_scheduled_non_musical_events += 1; + + node_data.last_delay_origin + 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::DelayOrigin = &event.event { + node_data.last_delay_origin = 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_delay_origin: 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_delay_origin: 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..e3e135b2 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_delay_origin: 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_delay_origin = node_entry.event_data.last_delay_origin; + } + // 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..813fdeb1 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::DelaySamplesFromLastOrigin(_) + | EventInstant::DelaySecondsFromLastOrigin(_) => { + panic!( + "DelaySecondsFromLastOrigin/DelaySamplesFromLastOrigin cannot be used for SpeedMultiplierKeyframe" + ); + } EventInstant::AtClockMusical(musical) => transport.musical_to_samples( musical, self.transport_start_samples, From b0444b40ca6a41e72201716cdbe998807ca22373 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 2 Sep 2026 15:09:08 -0500 Subject: [PATCH 2/7] Remove "node_profiling" from default features --- crates/firewheel-graph/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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", From 767dedc162b76a4542029b217281ac0efd00b1b3 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 2 Sep 2026 15:18:11 -0500 Subject: [PATCH 3/7] fix doc tests --- crates/firewheel-core/src/collector.rs | 14 +++++++------- crates/firewheel-core/src/diff/mod.rs | 18 ++++-------------- crates/firewheel-graph/src/context.rs | 4 ++-- 3 files changed, 13 insertions(+), 23 deletions(-) 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/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-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, From cb3c4e51a8baafa16c017c824f645ed88ad90a4c Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 2 Sep 2026 15:25:46 -0500 Subject: [PATCH 4/7] fix doc errors --- crates/firewheel-core/src/clock.rs | 20 +++++++++++--------- crates/firewheel-core/src/event.rs | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/firewheel-core/src/clock.rs b/crates/firewheel-core/src/clock.rs index c24d718d..bf6666d5 100644 --- a/crates/firewheel-core/src/clock.rs +++ b/crates/firewheel-core/src/clock.rs @@ -48,26 +48,28 @@ pub enum EventInstant { DelaySamples(DurationSamples), /// The event should happen the given number of seconds after the - /// last [`NodeEventType::DelayOrigin`] event that was sent to this node. + /// last [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// 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::DelayOrigin`] event was never sent to this node, - /// then the start of the stream will be used as the origin (effectively - /// making this behave like [`EventInstant::AtClockSeconds`]). + /// If a [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// event was never sent to this node, then the start of the stream will be used + /// as the origin. DelaySecondsFromLastOrigin(DurationSeconds), /// The event should happen the given number of samples (of a single channel - /// of audio) after the [`NodeEventType::DelayOrigin`] event that was sent to - /// this node. + /// of audio) after the + /// [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// 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::DelayOrigin`] event was never sent to this node, - /// then the start of the stream will be used as the origin (effectively - /// making this behave like [`EventInstant::AtClockSamples`]). + /// If a [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// event was never sent to this node, then the start of the stream will be used + /// as the origin. DelaySamplesFromLastOrigin(DurationSamples), /// The event should happen when the musical clock reaches the given diff --git a/crates/firewheel-core/src/event.rs b/crates/firewheel-core/src/event.rs index e7e8d3b0..ad10583f 100644 --- a/crates/firewheel-core/src/event.rs +++ b/crates/firewheel-core/src/event.rs @@ -82,8 +82,8 @@ pub enum NodeEventType { /// 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 origin for - /// future events scheduled with [`EventInstant::DelaySecondsFrom`] and - /// [`EventInstant::DelaySamplesFrom`]. + /// future events scheduled with [`EventInstant::DelaySecondsFromLastOrigin`] and + /// [`EventInstant::DelaySamplesFromLastOrigin`]. /// /// Note, this only applies if [`NodeEvent::time`] is `None`. If [`NodeEvent::time`] /// is not `None`, then this event will be discarded. From 319a943ccd660c3c675ea66353487e0f2dce85a3 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 2 Sep 2026 15:34:07 -0500 Subject: [PATCH 5/7] fix clippy warnings --- crates/firewheel-core/src/dsp/algo.rs | 6 +++--- crates/firewheel-core/src/sample_resource.rs | 4 +++- crates/firewheel-macros/src/firewheel_manifest.rs | 5 ++--- 3 files changed, 8 insertions(+), 7 deletions(-) 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/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-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); From 57e21a9b8fdb7e906adef5381cc31f49eaf13544 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Thu, 3 Sep 2026 13:39:59 -0500 Subject: [PATCH 6/7] rename DelayOrigin to Marker --- crates/firewheel-core/src/clock.rs | 24 +++++++++---------- crates/firewheel-core/src/clock/transport.rs | 4 ++-- crates/firewheel-core/src/event.rs | 10 ++++---- crates/firewheel-core/src/node.rs | 6 ++--- crates/firewheel-graph/Cargo.toml | 2 +- .../src/processor/event_scheduler.rs | 20 ++++++++-------- .../firewheel-graph/src/processor/process.rs | 4 ++-- .../src/processor/transport.rs | 6 ++--- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/crates/firewheel-core/src/clock.rs b/crates/firewheel-core/src/clock.rs index bf6666d5..77680cd8 100644 --- a/crates/firewheel-core/src/clock.rs +++ b/crates/firewheel-core/src/clock.rs @@ -48,29 +48,29 @@ pub enum EventInstant { DelaySamples(DurationSamples), /// The event should happen the given number of seconds after the - /// last [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// 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::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// 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 origin. - DelaySecondsFromLastOrigin(DurationSeconds), + /// as the marker. + DelaySecondsFromMarker(DurationSeconds), /// The event should happen the given number of samples (of a single channel /// of audio) after the - /// [`NodeEventType::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// [`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::DelayOrigin`](crate::event::NodeEventType::DelayOrigin) + /// 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 origin. - DelaySamplesFromLastOrigin(DurationSamples), + /// as the marker. + DelaySamplesFromMarker(DurationSamples), /// The event should happen when the musical clock reaches the given /// musical time. @@ -103,11 +103,11 @@ impl EventInstant { EventInstant::DelaySeconds(seconds) => { Some(proc_info.clock_samples + seconds.to_samples(proc_info.sample_rate)) } - EventInstant::DelaySamplesFromLastOrigin(samples) => { - Some(proc_info.last_delay_origin + *samples) + EventInstant::DelaySamplesFromMarker(samples) => { + Some(proc_info.last_marker_instant + *samples) } - EventInstant::DelaySecondsFromLastOrigin(seconds) => { - Some(proc_info.last_delay_origin + seconds.to_samples(proc_info.sample_rate)) + 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), diff --git a/crates/firewheel-core/src/clock/transport.rs b/crates/firewheel-core/src/clock/transport.rs index 020c8e91..a20dd42b 100644 --- a/crates/firewheel-core/src/clock/transport.rs +++ b/crates/firewheel-core/src/clock/transport.rs @@ -254,8 +254,8 @@ pub struct SpeedMultiplierKeyframe { /// The instant that this keyframe happens. /// - /// Note, [`EventInstant::DelaySecondsFromLastOrigin`] and - /// [`EventInstant::DelaySamplesFromLastOrigin`] cannot be used here, and + /// 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/event.rs b/crates/firewheel-core/src/event.rs index ad10583f..803656f9 100644 --- a/crates/firewheel-core/src/event.rs +++ b/crates/firewheel-core/src/event.rs @@ -81,14 +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 origin for - /// future events scheduled with [`EventInstant::DelaySecondsFromLastOrigin`] and - /// [`EventInstant::DelaySamplesFromLastOrigin`]. + /// 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")] - DelayOrigin, + Marker, #[cfg(feature = "midi_events")] MIDI(MidiMessage<'static>), } @@ -175,7 +175,7 @@ impl core::fmt::Debug for NodeEventType { 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::DelayOrigin => f.write_str("DelayOrigin"), + 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 4b467271..0b005d23 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -791,13 +791,13 @@ 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::DelayOrigin`] event that + /// The instant that the last [`NodeEventType::Marker`] event that /// was sent to this node occured at. /// - /// If a [`NodeEventType::DelayOrigin`] was never sent to this node, then + /// If a [`NodeEventType::Marker`] was never sent to this node, then /// this will be `InstantSamples(0)`. #[cfg(feature = "scheduled_events")] - pub last_delay_origin: InstantSamples, + pub last_marker_instant: InstantSamples, /// Information about the musical transport. /// diff --git a/crates/firewheel-graph/Cargo.toml b/crates/firewheel-graph/Cargo.toml index 6c1528f7..cf78c37a 100644 --- a/crates/firewheel-graph/Cargo.toml +++ b/crates/firewheel-graph/Cargo.toml @@ -16,7 +16,7 @@ exclude.workspace = true all-features = true [features] -default = ["std", "tracing"] +default = ["std", "tracing", "scheduled_events"] std = [ "arrayvec/std", "bevy_platform/std", diff --git a/crates/firewheel-graph/src/processor/event_scheduler.rs b/crates/firewheel-graph/src/processor/event_scheduler.rs index d74de1af..31af51ae 100644 --- a/crates/firewheel-graph/src/processor/event_scheduler.rs +++ b/crates/firewheel-graph/src/processor/event_scheduler.rs @@ -133,8 +133,8 @@ impl EventScheduler { ) { #[cfg(feature = "scheduled_events")] if let Some(event_instant) = event.time { - // Sending a `DelayOrigin` event with an `EventInstant` is invalid. - if let NodeEventType::DelayOrigin = &event.event { + // Sending a `Marker` event with an `EventInstant` is invalid. + if let NodeEventType::Marker = &event.event { return; } @@ -174,17 +174,17 @@ impl EventScheduler { clock_samples + seconds.to_samples(sample_rate) } - EventInstant::DelaySamplesFromLastOrigin(samples) => { + EventInstant::DelaySamplesFromMarker(samples) => { self.num_scheduled_non_musical_events += 1; node_data.num_scheduled_non_musical_events += 1; - node_data.last_delay_origin + samples + node_data.last_marker_instant + samples } - EventInstant::DelaySecondsFromLastOrigin(seconds) => { + EventInstant::DelaySecondsFromMarker(seconds) => { self.num_scheduled_non_musical_events += 1; node_data.num_scheduled_non_musical_events += 1; - node_data.last_delay_origin + seconds.to_samples(sample_rate) + node_data.last_marker_instant + seconds.to_samples(sample_rate) } #[cfg(feature = "musical_transport")] EventInstant::AtClockMusical(musical) => { @@ -213,8 +213,8 @@ impl EventScheduler { self.sorted_event_buffer_indices.push((slot, time_samples)); return; - } else if let NodeEventType::DelayOrigin = &event.event { - node_data.last_delay_origin = clock_samples; + } 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 { @@ -967,7 +967,7 @@ pub(super) struct NodeEventSchedulerData { #[cfg(feature = "scheduled_events")] first_sorted_event_index: usize, #[cfg(feature = "scheduled_events")] - pub last_delay_origin: InstantSamples, + pub last_marker_instant: InstantSamples, #[allow(unused)] is_pre_process: bool, @@ -987,7 +987,7 @@ impl NodeEventSchedulerData { #[cfg(feature = "scheduled_events")] first_sorted_event_index: 0, #[cfg(feature = "scheduled_events")] - last_delay_origin: InstantSamples(0), + 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 e3e135b2..e65862bf 100644 --- a/crates/firewheel-graph/src/processor/process.rs +++ b/crates/firewheel-graph/src/processor/process.rs @@ -301,7 +301,7 @@ impl FirewheelProcessorInner { process_to_playback_delay, did_just_unbypass: false, #[cfg(feature = "scheduled_events")] - last_delay_origin: InstantSamples(0), + last_marker_instant: InstantSamples(0), #[cfg(feature = "musical_transport")] transport_info, }; @@ -349,7 +349,7 @@ impl FirewheelProcessorInner { #[cfg(feature = "scheduled_events")] { - info.last_delay_origin = node_entry.event_data.last_delay_origin; + info.last_marker_instant = node_entry.event_data.last_marker_instant; } // Used to keep track of what status this closure should return. diff --git a/crates/firewheel-graph/src/processor/transport.rs b/crates/firewheel-graph/src/processor/transport.rs index 813fdeb1..353d30b4 100644 --- a/crates/firewheel-graph/src/processor/transport.rs +++ b/crates/firewheel-graph/src/processor/transport.rs @@ -232,10 +232,10 @@ impl ProcTransportState { clock_samples + seconds.to_samples(sample_rate) } EventInstant::DelaySamples(samples) => clock_samples + samples, - EventInstant::DelaySamplesFromLastOrigin(_) - | EventInstant::DelaySecondsFromLastOrigin(_) => { + EventInstant::DelaySamplesFromMarker(_) + | EventInstant::DelaySecondsFromMarker(_) => { panic!( - "DelaySecondsFromLastOrigin/DelaySamplesFromLastOrigin cannot be used for SpeedMultiplierKeyframe" + "DelaySecondsFromMarker/DelaySamplesFromMarker cannot be used for SpeedMultiplierKeyframe" ); } EventInstant::AtClockMusical(musical) => transport.musical_to_samples( From c8e7b079d0e89eb4ac900f088b9d118bc3ef2725 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Thu, 3 Sep 2026 13:42:25 -0500 Subject: [PATCH 7/7] forgot to remove "scheduled_events" from default features --- crates/firewheel-graph/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/firewheel-graph/Cargo.toml b/crates/firewheel-graph/Cargo.toml index cf78c37a..6c1528f7 100644 --- a/crates/firewheel-graph/Cargo.toml +++ b/crates/firewheel-graph/Cargo.toml @@ -16,7 +16,7 @@ exclude.workspace = true all-features = true [features] -default = ["std", "tracing", "scheduled_events"] +default = ["std", "tracing"] std = [ "arrayvec/std", "bevy_platform/std",