Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 34 additions & 92 deletions crates/firewheel-core/src/clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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")]
Expand All @@ -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<InstantSamples> {
match self {
EventInstant::AtClockSamples(samples) => Some(*samples),
Expand All @@ -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),
}
Expand Down Expand Up @@ -123,91 +150,6 @@ impl From<InstantMusical> for EventInstant {
}
}

#[cfg(feature = "scheduled_events")]
impl Diff for EventInstant {
fn diff<E: crate::diff::EventQueue>(
&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<Self::Patch, crate::diff::PatchError> {
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<EventInstant> {
fn diff<E: crate::diff::EventQueue>(
&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<EventInstant> {
type Patch = Self;

fn patch(data: &ParamData, _path: &[u32]) -> Result<Self::Patch, crate::diff::PatchError> {
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)]
Expand Down
4 changes: 4 additions & 0 deletions crates/firewheel-core/src/clock/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
14 changes: 7 additions & 7 deletions crates/firewheel-core/src/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ impl<T: Send + Sync + ?Sized> StrongCount for Arc<T> {
/// # Example
///
/// ```rust
/// # use rtgc::*;
/// # use firewheel_core::collector::*;
/// # use std::time::Duration;
/// let value: ArcGc<String> = ArcGc::new(String::from("foo"));
///
Expand Down Expand Up @@ -230,7 +230,7 @@ impl<T: Send + Sync + 'static> ArcGc<T, GlobalRtGc> {
/// Construct a new [`ArcGc`].
///
/// ```
/// # use rtgc::*;
/// # use firewheel_core::collector::*;
/// let value: ArcGc<String> = ArcGc::new(String::from("foo"));
/// ```
pub fn new(value: T) -> Self {
Expand All @@ -249,7 +249,7 @@ impl<T: ?Sized + Send + Sync + 'static> ArcGc<T, GlobalRtGc> {
/// 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]),
Expand Down Expand Up @@ -279,7 +279,7 @@ impl ArcGc<dyn Any + Send + Sync + 'static, GlobalRtGc> {
/// 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<dyn Any + Send + Sync + 'static> =
Expand Down Expand Up @@ -348,7 +348,7 @@ impl<T: Debug + ?Sized + Send + Sync + 'static, C: Collector> Debug for ArcGc<T,
/// # Example
///
/// ```rust
/// # use rtgc::*;
/// # use firewheel_core::collector::*;
/// # use std::time::Duration;
/// let value: OwnedGc<String> = OwnedGc::new(String::from("foo"));
///
Expand Down Expand Up @@ -473,7 +473,7 @@ unsafe impl<T: ?Sized + Send + 'static> Sync for OwnedGcWrapper<T> {}
/// # 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(),
Expand Down Expand Up @@ -516,7 +516,7 @@ impl<T: ?Sized + Send + 'static> OwnedGcUnsized<T, GlobalRtGc> {
/// 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(),
Expand Down
6 changes: 6 additions & 0 deletions crates/firewheel-core/src/diff/leaf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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")]
Expand Down
18 changes: 4 additions & 14 deletions crates/firewheel-core/src/diff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<MyParams>() {
/// self.params.apply(patch);
/// }
///
/// // ...
///
/// ProcessStatus::OutputsModified
/// }
/// }
/// ```
Expand All @@ -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::<MyParams>() {
/// // When you derive `Patch`, it creates an enum with variants
/// // for each field.
Expand All @@ -476,10 +470,6 @@ impl core::ops::Deref for ParamPath {
/// // And / or apply it directly.
/// self.params.apply(patch);
/// }
///
/// // ...
///
/// ProcessStatus::OutputsModified
/// }
/// }
/// ```
Expand Down
6 changes: 3 additions & 3 deletions crates/firewheel-core/src/dsp/algo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<CHUNK>();
for chunk in chunked_data.iter() {
for i in 0..CHUNK {
let abs = chunk[i].abs();
if abs > tmp[i] {
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions crates/firewheel-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ pub enum NodeEventType {
Custom(OwnedGc<Box<dyn Any + Send + 'static>>),
/// 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>),
}
Expand Down Expand Up @@ -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(),
}
Expand Down
8 changes: 8 additions & 0 deletions crates/firewheel-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion crates/firewheel-core/src/sample_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ pub fn fill_buffers_interleaved<T: RawSample + Clone>(
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();
Expand Down
3 changes: 1 addition & 2 deletions crates/firewheel-graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions crates/firewheel-graph/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B: AudioBackend, D: Diff>(
/// # use firewheel_graph::{FirewheelContext, ContextQueue};
/// # fn context_queue<D: Diff>(
/// # context: &mut FirewheelContext,
/// # node_id: NodeID,
/// # params: &D,
Expand Down
Loading
Loading