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
88 changes: 87 additions & 1 deletion crates/firewheel-core/src/dsp/declick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,92 @@ impl Declicker {
}
}

pub fn process_into_crossfade_gain_buffers(
&mut self,
wet_buffer: &mut [f32],
dry_buffer: &mut [f32],
buffer_already_cleared: bool,
declick_values: &DeclickValues,
fade_curve: DeclickFadeCurve,
) {
let buffer_frames = wet_buffer.len().min(dry_buffer.len());

let fade_buffer = |buffer: &mut [f32],
proc_frames: usize,
declick_frames_left: usize,
values: &[f32],
fill_rest_with: f32| {
let start_frame = values.len() - declick_frames_left;

buffer[..proc_frames].copy_from_slice(&values[start_frame..start_frame + proc_frames]);

if proc_frames < buffer.len() {
buffer[proc_frames..].fill(fill_rest_with);
}
};

match self {
Self::SettledAt0 => {
if !buffer_already_cleared {
wet_buffer.fill(0.0);
}
dry_buffer.fill(1.0);
}
Self::SettledAt1 => {
wet_buffer.fill(1.0);
if !buffer_already_cleared {
dry_buffer.fill(0.0);
}
}
Self::FadingTo0 { frames_left } => {
let (wet_values, dry_values) = match fade_curve {
DeclickFadeCurve::Linear => (
&declick_values.linear_1_to_0_values,
&declick_values.linear_0_to_1_values,
),
DeclickFadeCurve::EqualPower3dB => (
&declick_values.circular_1_to_0_values,
&declick_values.circular_0_to_1_values,
),
};

let proc_frames = buffer_frames.min(*frames_left);

fade_buffer(wet_buffer, proc_frames, *frames_left, wet_values, 0.0);
fade_buffer(dry_buffer, proc_frames, *frames_left, dry_values, 1.0);

*frames_left -= proc_frames;

if *frames_left == 0 {
*self = Self::SettledAt0;
}
}
Self::FadingTo1 { frames_left } => {
let (wet_values, dry_values) = match fade_curve {
DeclickFadeCurve::Linear => (
&declick_values.linear_0_to_1_values,
&declick_values.linear_1_to_0_values,
),
DeclickFadeCurve::EqualPower3dB => (
&declick_values.circular_0_to_1_values,
&declick_values.circular_1_to_0_values,
),
};

let proc_frames = buffer_frames.min(*frames_left);

fade_buffer(wet_buffer, proc_frames, *frames_left, wet_values, 1.0);
fade_buffer(dry_buffer, proc_frames, *frames_left, dry_values, 0.0);

*frames_left -= proc_frames;

if *frames_left == 0 {
*self = Self::SettledAt1;
}
}
}
}

pub fn process<V: AsMut<[f32]>>(
&mut self,
buffers: &mut [V],
Expand Down Expand Up @@ -397,7 +483,7 @@ pub struct DeclickValues {
}

impl DeclickValues {
pub const DEFAULT_FADE_SECONDS: f32 = 10.0 / 1_000.0;
pub const DEFAULT_FADE_SECONDS: f32 = 3.0 / 1_000.0;

pub fn new(frames: NonZeroU32) -> Self {
let frames = frames.get() as usize;
Expand Down
7 changes: 6 additions & 1 deletion crates/firewheel-core/src/dsp/filter/smoothing_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ use num_traits::Float;

use core::num::NonZeroU32;

pub const DEFAULT_SMOOTH_SECONDS: f32 = 15.0 / 1_000.0;
/// The default number of seconds for a [`Smoothing Filter`].
///
/// This value is chosen to be roughly equal to a typical block size
/// of 1024 samples (23 ms) to eliminate stair-stepping for most
/// games.
pub const DEFAULT_SMOOTH_SECONDS: f32 = 23.0 / 1_000.0;
pub const DEFAULT_SETTLE_EPSILON: f32 = 0.001f32;

/// The coefficients for a simple smoothing/declicking filter where:
Expand Down
7 changes: 7 additions & 0 deletions crates/firewheel-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub enum NodeEventType {
/// The path to the parameter.
path: ParamPath,
},
/// Set the bypass state of the node.
SetBypassed(bool),
/// Custom event type stored on the heap.
Custom(OwnedGc<Box<dyn Any + Send + 'static>>),
/// Custom event type stored on the stack as raw bytes.
Expand Down Expand Up @@ -163,6 +165,7 @@ impl core::fmt::Debug for NodeEventType {
.finish(),
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 = "midi_events")]
NodeEventType::MIDI(f0) => f.debug_tuple("MIDI").field(&f0).finish(),
}
Expand Down Expand Up @@ -392,6 +395,10 @@ impl<'a> ProcEvents<'a> {
self.indices.len()
}

pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}

/// Iterate over all events, draining the events from the list.
pub fn drain<'b>(&'b mut self) -> impl IntoIterator<Item = NodeEventType> + use<'b> {
self.indices.drain(..).map(|index_type| match index_type {
Expand Down
69 changes: 59 additions & 10 deletions crates/firewheel-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,34 +449,75 @@ impl<T: AudioNode> DynAudioNode for Constructor<T, T::Configuration> {
/// The trait describing the realtime processor counterpart to an
/// audio node.
pub trait AudioNodeProcessor: 'static + Send {
/// Process the given block of audio. Only process data in the
/// buffers up to `samples`.
/// Called when there are new events for this node to process.
///
/// This is called once before the first call to `process`, and after that
/// it will be called whenever there are new events (including when the
/// node is bypassed).
///
/// Unless this node is bypassed, then [`AudioNodeProcessor::process`] will be
/// called immediately after.
///
/// * `info` - Information about this processing block.
/// * `buffers` - The buffers of data to process.
/// * `events` - A list of events for this node to process.
/// * `extra` - Additional buffers and utilities.
///
/// This is always called in a realtime thread, so do not perform any
/// realtime-unsafe operations.
fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) {
let _ = info;
let _ = events;
let _ = extra;
}

/// Called when the node has been fully bypassed/unbypassed.
///
/// The Firewheel processor automatically handles bypass declicking, so
/// there is no need to handle that manually.
///
/// This is always called in a realtime thread, so do not perform any
/// realtime-unsafe operations.
fn bypassed(&mut self, bypassed: bool) {
let _ = bypassed;
}

/// Process the given block of audio.
///
/// * `info` - Information about this processing block.
/// * `buffers` - The buffers of data to process.
/// * `extra` - Additional buffers and utilities.
///
/// WARNING: Audio nodes *MUST* either completely fill all output buffers
/// with data, or return [`ProcessStatus::ClearAllOutputs`]/[`ProcessStatus::Bypass`].
/// Failing to do this will result in audio glitches.
///
/// This is always called in a realtime thread, so do not perform any
/// realtime-unsafe operations.
fn process(
&mut self,
info: &ProcInfo,
buffers: ProcBuffers,
events: &mut ProcEvents,
extra: &mut ProcExtra,
) -> ProcessStatus;
) -> ProcessStatus {
let _ = info;
let _ = buffers;
let _ = extra;

ProcessStatus::Bypass
}

/// Called when the audio stream has been stopped.
///
/// This may or may not be called in a realtime thread, so prefer not
/// perform any realtime-unsafe operations.
fn stream_stopped(&mut self, context: &mut ProcStreamCtx) {
let _ = context;
}

/// Called when a new audio stream has been started after a previous
/// call to [`AudioNodeProcessor::stream_stopped`].
///
/// Note, this method gets called on the main thread, not the audio
/// This method gets called on the main thread, not the realtime audio
/// thread. So it is safe to allocate/deallocate here.
fn new_stream(&mut self, stream_info: &StreamInfo, context: &mut ProcStreamCtx) {
let _ = stream_info;
Expand All @@ -485,21 +526,26 @@ pub trait AudioNodeProcessor: 'static + Send {
}

impl AudioNodeProcessor for Box<dyn AudioNodeProcessor> {
fn new_stream(&mut self, stream_info: &StreamInfo, context: &mut ProcStreamCtx) {
self.as_mut().new_stream(stream_info, context)
fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) {
self.as_mut().events(info, events, extra);
}
fn bypassed(&mut self, bypassed: bool) {
self.as_mut().bypassed(bypassed);
}
fn process(
&mut self,
info: &ProcInfo,
buffers: ProcBuffers,
events: &mut ProcEvents,
extra: &mut ProcExtra,
) -> ProcessStatus {
self.as_mut().process(info, buffers, events, extra)
self.as_mut().process(info, buffers, extra)
}
fn stream_stopped(&mut self, context: &mut ProcStreamCtx) {
self.as_mut().stream_stopped(context)
}
fn new_stream(&mut self, stream_info: &StreamInfo, context: &mut ProcStreamCtx) {
self.as_mut().new_stream(stream_info, context)
}
}

pub struct ProcStreamCtx<'a> {
Expand Down Expand Up @@ -678,6 +724,9 @@ pub struct ProcInfo {
/// will be `None`.
pub process_to_playback_delay: Option<Duration>,

/// If the node has just been un-bypassed, then this will be `true`.
pub did_just_unbypass: bool,

/// Information about the musical transport.
///
/// This will be `None` if no musical transport is currently active,
Expand Down
5 changes: 4 additions & 1 deletion crates/firewheel-graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,7 @@ bevy_platform.workspace = true
num-traits.workspace = true
audioadapter.workspace = true
serde = { workspace = true, optional = true }
bevy_reflect = { workspace = true, optional = true }
bevy_reflect = { workspace = true, optional = true }

[dev-dependencies]
audioadapter-buffers.workspace = true
Loading
Loading