From 4c7f6961baece55fdca8c93d98747da123f6395c Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Tue, 7 Jul 2026 23:42:39 -0500 Subject: [PATCH 1/4] improve smoothing filter algorithm --- .../src/dsp/distance_attenuation.rs | 5 +- .../src/dsp/filter/smoothing_filter.rs | 160 +++++++++++++++++- crates/firewheel-core/src/dsp/mix.rs | 6 +- crates/firewheel-core/src/param/smoother.rs | 104 +++++++++--- crates/firewheel-nodes/src/convolution.rs | 8 +- .../src/fast_filters/bandpass.rs | 1 + .../src/fast_filters/highpass.rs | 1 + .../src/fast_filters/lowpass.rs | 1 + crates/firewheel-nodes/src/freeverb/mod.rs | 3 + crates/firewheel-nodes/src/mix.rs | 3 + .../src/noise_generator/pink.rs | 2 + .../src/noise_generator/white.rs | 2 + crates/firewheel-nodes/src/spatial_basic.rs | 3 + crates/firewheel-nodes/src/svf.rs | 4 +- crates/firewheel-nodes/src/volume.rs | 2 + crates/firewheel-nodes/src/volume_pan.rs | 3 + examples/custom_nodes/src/nodes/filter.rs | 25 ++- 17 files changed, 294 insertions(+), 39 deletions(-) diff --git a/crates/firewheel-core/src/dsp/distance_attenuation.rs b/crates/firewheel-core/src/dsp/distance_attenuation.rs index 78e9a6fb..4e7cf638 100644 --- a/crates/firewheel-core/src/dsp/distance_attenuation.rs +++ b/crates/firewheel-core/src/dsp/distance_attenuation.rs @@ -10,7 +10,7 @@ use crate::{ coeff_update::{CoeffUpdateFactor, CoeffUpdateMask}, filter::single_pole_iir::{OnePoleIirLPFCoeff, OnePoleIirLPFCoeffSimd, OnePoleIirLPFSimd}, }, - param::smoother::{SmoothedParam, SmootherConfig}, + param::smoother::{DEFAULT_GAIN_SPAN, SmoothedParam, SmootherConfig}, }; pub const MUFFLE_CUTOFF_HZ_MIN: f32 = 20.0; @@ -203,9 +203,10 @@ impl DistanceAttenuatorStereoDsp { coeff_update_factor: CoeffUpdateFactor, ) -> Self { Self { - gain: SmoothedParam::new(1.0, smoother_config, sample_rate), + gain: SmoothedParam::new(1.0, DEFAULT_GAIN_SPAN, smoother_config, sample_rate), muffle_cutoff_hz: SmoothedParam::new( MUFFLE_CUTOFF_HZ_MAX, + MUFFLE_CUTOFF_HZ_MAX - MUFFLE_CUTOFF_HZ_MIN, smoother_config, sample_rate, ), diff --git a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs index ae3da952..95313bde 100644 --- a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs +++ b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs @@ -9,7 +9,20 @@ use core::num::NonZeroU32; /// 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 default epsilon value for a [`SmoothingFilter`]. +pub const DEFAULT_SETTLE_EPSILON: f32 = 0.01; + +/// The minimum supported epsilon value for a [`SmoothingFilter`]. +/// +/// Values smaller than this can tend to never settle correctly due to floating point +/// accumulation errors. +pub const MIN_SETTLE_EPSILON: f32 = 0.00075; +/// The minimum supported epsilon value for a [`SmoothingFilter`]. +/// +/// Values larger than this can tend to never settle correctly due to floating point +/// accumulation errors. +pub const MAX_SETTLE_EPSILON: f32 = 0.9; /// The coefficients for a simple smoothing/declicking filter where: /// @@ -21,10 +34,59 @@ pub struct SmoothingFilterCoeff { } impl SmoothingFilterCoeff { - pub fn new(sample_rate: NonZeroU32, smooth_secs: f32) -> Self { - let smooth_secs = smooth_secs.max(0.00001); + /// Calculate the coefficients for a [`SmoothingFilter`]. + /// + /// * `sample_rate` - The sample rate of the signal. + /// * `smooth_secs` - The amount of time it takes for the filter to smooth from one + /// end of the parameter's range to the other end. + /// * If less than 0.0, then it will be clamped to 0.0. + /// * `settle_epsilon` - The threshold at which the filter is considered "settled". + /// For example `0.01` means that the filter is considered settled if the value is + /// within 1% of the total span of the parameter's range. Must be >= + /// [`MIN_SETTLE_EPSILON`] (0.00075) and <= [`MAX_SETTLE_EPSILON`] (0.9). + /// * Will be clamped to the range `[0.00075..0.9]`. + /// Returns `true` if this filter is settled, `false` if not. + pub fn new(sample_rate: NonZeroU32, smooth_secs: f32, epsilon: f32) -> Self { + let smooth_secs = smooth_secs.max(0.0); + let epsilon = epsilon.clamp(MIN_SETTLE_EPSILON, MAX_SETTLE_EPSILON); + + // The b1 coefficient of a one pole lp filter is given by: + // + // b1 = e ^ (-1 / t_to_1_over_e) + // + // where t_to_1_over_e is the amount of time for the filter to decay to 1/e + // (about 36.8%). + // + // So, to get a filter which decays to a given "epsilon" value in + // "t_to_epsilon" frames, we need to adjust the t_to_1_over_e value. Because + // doubling the time is equivalent to decaying by another 36.8%, we can + // relate epsilon and t_to_epsilon with: + // + // epsilon = (1/e) ^ c + // t_to_epsilon = t_to_1_over_e * c + // + // where c is some unknown constant. + // + // Solve for c: + // + // c = log_(1/e)(epsilon) + // = -ln(epsilon) + // + // Solve for t_to_1_over_e: + // + // t_to_1_over_e = t_to_epsilon / c + // = t_to_epsilon * (1 / -ln(epsilon)) + // = -ln(epsilon) / t_to_epsilon + // + // Which finally gives us: + // + // b1 = e ^ (-1 / (-ln(epsilon) / t_to_epsilon)) + // = e ^ (ln(epsilon) / t_to_epsilon) + // = epsilon ^ (1 / t_to_epsilon) - let b1 = (-1.0f32 / (smooth_secs * sample_rate.get() as f32)).exp(); + let t_to_epsilon = (smooth_secs * sample_rate.get() as f32).max(1.0); + + let b1 = epsilon.powf(t_to_epsilon.recip()); let a0 = 1.0f32 - b1; Self { a0, b1 } @@ -71,11 +133,17 @@ impl SmoothingFilter { /// Settle the filter if its state is close enough to the target value. /// + /// * `target` - The target value that is being smoothed to. + /// * `span` - The size of the parameter's range (equal to `(max_value - min_value).abs()`). + /// * `settle_epsilon` - The threshold at which the filter is considered "settled". + /// For example `0.01` means that the filter is considered settled if the value is + /// within 1% of the total `span`. + /// /// Returns `true` if this filter is settled, `false` if not. - pub fn settle(&mut self, target: f32, settle_epsilon: f32) -> bool { + pub fn settle(&mut self, target: f32, span: f32, settle_epsilon: f32) -> bool { if self.z1 == target { true - } else if (self.z1 - target).abs() < (target.abs() * settle_epsilon) + settle_epsilon { + } else if span == 0.0 || (self.z1 - target).abs() < (span * settle_epsilon).abs() { self.z1 = target; true } else { @@ -87,3 +155,83 @@ impl SmoothingFilter { self.z1 == target } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn smoothing_filter_decay() { + const TEST_SAMPLE_RATES: [u32; 2] = [44100, 48000]; + const TEST_SECONDS: [f32; 6] = [ + 0.0, + 1.0 / 1_000.0, + 5.0 / 1_000.0, + 23.0 / 1_000.0, + 100.0 / 1_000.0, + 500.0 / 1_000.0, + ]; + const TEST_EPSILONS: [f32; 6] = [ + MIN_SETTLE_EPSILON, + 0.001, + 0.01, + 0.1, + 0.5, + MAX_SETTLE_EPSILON, + ]; + const TEST_VALUES: [(f32, f32); 5] = [ + (1.0, 0.0), + (0.0, 1.0), + (1.0, -1.0), + (20.0, 20480.0), + (20480.0, 20.0), + ]; + + let mut max_error = 0.0; + + for sr in TEST_SAMPLE_RATES { + for secs in TEST_SECONDS { + for eps in TEST_EPSILONS { + for (start_value, end_value) in TEST_VALUES { + let coeff = + SmoothingFilterCoeff::new(NonZeroU32::new(sr).unwrap(), secs, eps); + let mut filter = SmoothingFilter::new(start_value); + + let mut frames: u32 = 0; + while !filter.settle(end_value, (start_value - end_value).abs(), eps) + && frames < sr * 4 + { + let _ = filter.process(end_value, coeff); + frames += 1; + } + + let expected_frames = ((secs * sr as f32).round() as u32).max(1); + + let diff = (frames as f32 - expected_frames as f32).abs(); + + // Don't consider off by one frame to be an error. + let diff = if diff < 2.0 { 0.0 } else { diff }; + + let error = diff / expected_frames as f32; + + max_error = error.max(max_error); + + // Give some leeway for floating point accumulation errors. + const ERROR_TOLERANCE: f32 = 0.02; + + assert!( + error < ERROR_TOLERANCE, + "error {} is >= the maximum accepted error {} | expected_frames {}, got frames {}", + error, + ERROR_TOLERANCE, + expected_frames, + frames + ); + } + } + } + } + + dbg!(max_error); + } +} diff --git a/crates/firewheel-core/src/dsp/mix.rs b/crates/firewheel-core/src/dsp/mix.rs index c6bc3c08..71997a40 100644 --- a/crates/firewheel-core/src/dsp/mix.rs +++ b/crates/firewheel-core/src/dsp/mix.rs @@ -5,7 +5,7 @@ use crate::{ diff::{Diff, EventQueue, Patch, PatchError, PathBuilder}, dsp::fade::FadeCurve, event::ParamData, - param::smoother::{SmoothedParam, SmootherConfig}, + param::smoother::{DEFAULT_GAIN_SPAN, SmoothedParam, SmootherConfig}, }; /// A value representing the mix between two audio signals (e.g. second/first mix) @@ -131,8 +131,8 @@ impl MixDSP { let (gain_0, gain_1) = mix.compute_gains(fade_curve); Self { - gain_0: SmoothedParam::new(gain_0, config, sample_rate), - gain_1: SmoothedParam::new(gain_1, config, sample_rate), + gain_0: SmoothedParam::new(gain_0, DEFAULT_GAIN_SPAN, config, sample_rate), + gain_1: SmoothedParam::new(gain_1, DEFAULT_GAIN_SPAN, config, sample_rate), } } diff --git a/crates/firewheel-core/src/param/smoother.rs b/crates/firewheel-core/src/param/smoother.rs index 456fb3d4..57899f5e 100644 --- a/crates/firewheel-core/src/param/smoother.rs +++ b/crates/firewheel-core/src/param/smoother.rs @@ -5,10 +5,17 @@ use bevy_platform::prelude::Vec; use crate::{ StreamInfo, - dsp::filter::smoothing_filter::{self, SmoothingFilter, SmoothingFilterCoeff}, + dsp::filter::smoothing_filter::{ + self, MAX_SETTLE_EPSILON, MIN_SETTLE_EPSILON, SmoothingFilter, SmoothingFilterCoeff, + }, }; -const MIN_SMOOTH_SECONDS: f32 = 0.00001; +/// A good default value to use for the `span` argument in [`SmoothedParam::new()`] +/// when constructing gain/volume parameters. +/// +/// This causes maximum smoothing to occur when the volume immediately jumps +/// from 0% to 200% or vice versa. +pub const DEFAULT_GAIN_SPAN: f32 = 2.0; /// The configuration for a [`SmoothedParam`] #[derive(Debug, Clone, Copy, PartialEq)] @@ -17,11 +24,18 @@ const MIN_SMOOTH_SECONDS: f32 = 0.00001; pub struct SmootherConfig { /// The amount of smoothing in seconds /// - /// By default this is set to 5 milliseconds. + /// If less than 0.0, then it will be clamped to 0.0. + /// + /// By default this is set to 23 milliseconds. pub smooth_seconds: f32, /// The threshold at which the smoothing will complete /// - /// By default this is set to `0.001`. + /// For example `0.01` means that the filter is considered settled if the + /// value is within 1% of the total span of the parameter's range. + /// + /// Will be clamped to the range `[0.00075..0.9]`. + /// + /// By default this is set to `0.01`. pub settle_epsilon: f32, } @@ -43,20 +57,43 @@ pub struct SmoothedParam { coeff: SmoothingFilterCoeff, smooth_secs: f32, settle_epsilon: f32, + span: f32, } impl SmoothedParam { /// Construct a new smoothed f32 parameter with the given configuration. - pub fn new(value: f32, config: SmootherConfig, sample_rate: NonZeroU32) -> Self { - let smooth_secs = config.smooth_seconds.max(MIN_SMOOTH_SECONDS); - let settle_epsilon = config.settle_epsilon.max(f32::EPSILON); - - let coeff = SmoothingFilterCoeff::new(sample_rate, smooth_secs); + /// + /// * `initial_value` - The initial target value. + /// * `value_span` - The difference between two values where the maximum amount + /// of smoothing will take effect. + /// * If the minimum and maximum values of the parameter are known, then + /// typically `max_value - min_value` should be used. + /// * If the min and/or max values are not known, then use a span value that is + /// typical to be the worst-case-scenario (For example, if creating a "gain" + /// parameter, a good value to use is [`DEFAULT_AMP_SPAN`] (`2.0`) since + /// immediately jumping from 0% volume to 200% volume or vice versa is typically + /// the worst-case-scenario). + /// * This value does not need to be positive. + /// * `config` - Extra configuration options for the smoothing filter. + /// * `sample_rate` - The sample rate of the stream. + pub fn new( + initial_value: f32, + value_span: f32, + config: SmootherConfig, + sample_rate: NonZeroU32, + ) -> Self { + let smooth_secs = config.smooth_seconds.max(0.0); + let settle_epsilon = config + .settle_epsilon + .clamp(MIN_SETTLE_EPSILON, MAX_SETTLE_EPSILON); + + let coeff = SmoothingFilterCoeff::new(sample_rate, smooth_secs, settle_epsilon); Self { - target_value: value, - target_times_a: value * coeff.a0, - filter: SmoothingFilter::new(value), + target_value: initial_value, + target_times_a: initial_value * coeff.a0, + filter: SmoothingFilter::new(initial_value), + span: value_span.abs(), coeff, smooth_secs, settle_epsilon, @@ -78,7 +115,8 @@ impl SmoothedParam { /// /// Returns `true` if this filter is settled, `false` if not. pub fn settle(&mut self) -> bool { - self.filter.settle(self.target_value, self.settle_epsilon) + self.filter + .settle(self.target_value, self.span, self.settle_epsilon) } /// Returns `true` if this parameter is currently smoothing this process cycle, @@ -123,20 +161,21 @@ impl SmoothedParam { self.filter .process_into_buffer(buffer, self.target_value, self.coeff); - self.filter.settle(self.target_value, self.settle_epsilon); + self.filter + .settle(self.target_value, self.span, self.settle_epsilon); } else { buffer.fill(self.target_value); } } pub fn set_smooth_seconds(&mut self, seconds: f32, sample_rate: NonZeroU32) { - self.coeff = SmoothingFilterCoeff::new(sample_rate, seconds); + self.coeff = SmoothingFilterCoeff::new(sample_rate, seconds, self.settle_epsilon); self.smooth_secs = seconds; } /// Update the sample rate. pub fn update_sample_rate(&mut self, sample_rate: NonZeroU32) { - self.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs); + self.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs, self.settle_epsilon); } } @@ -149,14 +188,39 @@ pub struct SmoothedParamBuffer { } impl SmoothedParamBuffer { - /// Construct a new smoothed f32 parameter with the given configuration. - pub fn new(value: f32, config: SmootherConfig, stream_info: &StreamInfo) -> Self { + /// Construct a new smoothed f32 parameter with an internal buffer with the given + /// configuration. + /// + /// * `initial_value` - The initial target value. + /// * `value_span` - The difference between two values where the maximum amount + /// of smoothing will take effect. + /// * If the minimum and maximum values of the parameter are known, then + /// typically this would be `max_value - min_value`. + /// * If the min and/or max values are not known, then use a span value that is + /// typical to be the worst-case-scenario (For example, if creating a "gain" + /// parameter, a good value to use is [`DEFAULT_AMP_SPAN`] (`2.0`) since + /// immediately jumping from 0% volume to 200% volume or vice versa is typically + /// the worst-case-scenario). + /// * This value does not need to be positive. + /// * `config` - Extra configuration options for the smoothing filter. + /// * `stream_info` - The information about the current stream. + pub fn new( + initial_value: f32, + value_span: f32, + config: SmootherConfig, + stream_info: &StreamInfo, + ) -> Self { let mut buffer = Vec::new(); buffer.reserve_exact(stream_info.max_block_frames.get() as usize); - buffer.resize(stream_info.max_block_frames.get() as usize, value); + buffer.resize(stream_info.max_block_frames.get() as usize, initial_value); Self { - smoother: SmoothedParam::new(value, config, stream_info.sample_rate), + smoother: SmoothedParam::new( + initial_value, + value_span, + config, + stream_info.sample_rate, + ), buffer, buffer_is_constant: true, } diff --git a/crates/firewheel-nodes/src/convolution.rs b/crates/firewheel-nodes/src/convolution.rs index 512fdf03..e6d8b38a 100644 --- a/crates/firewheel-nodes/src/convolution.rs +++ b/crates/firewheel-nodes/src/convolution.rs @@ -6,6 +6,7 @@ use firewheel_core::channel_config::NonZeroChannelCount; use firewheel_core::collector::ArcGc; use firewheel_core::event::ProcEvents; use firewheel_core::node::{NodeError, ProcBuffers, ProcExtra, ProcInfo}; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::ChannelConfig, diff::{Diff, Patch}, @@ -189,7 +190,12 @@ impl AudioNode for ConvolutionNode { Ok(ConvolutionProcessor { params: self.clone(), - gain: SmoothedParam::new(self.wet_gain.amp(), smooth_config, sample_rate), + gain: SmoothedParam::new( + self.wet_gain.amp(), + DEFAULT_GAIN_SPAN, + smooth_config, + sample_rate, + ), declick: Declicker::SettledAt0, convolver, max_frames, diff --git a/crates/firewheel-nodes/src/fast_filters/bandpass.rs b/crates/firewheel-nodes/src/fast_filters/bandpass.rs index f543e87b..70606685 100644 --- a/crates/firewheel-nodes/src/fast_filters/bandpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/bandpass.rs @@ -115,6 +115,7 @@ impl AudioNode for FastBandpassNode { )), cutoff_hz: SmoothedParam::new( cutoff_hz, + MAX_HZ - MIN_HZ, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/fast_filters/highpass.rs b/crates/firewheel-nodes/src/fast_filters/highpass.rs index 43a3ed4e..1280d6c3 100644 --- a/crates/firewheel-nodes/src/fast_filters/highpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/highpass.rs @@ -107,6 +107,7 @@ impl AudioNode for FastHighpassNode { )), cutoff_hz: SmoothedParam::new( cutoff_hz, + MAX_HZ - MIN_HZ, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/fast_filters/lowpass.rs b/crates/firewheel-nodes/src/fast_filters/lowpass.rs index 665f21b7..b8796e71 100644 --- a/crates/firewheel-nodes/src/fast_filters/lowpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/lowpass.rs @@ -106,6 +106,7 @@ impl AudioNode for FastLowpassNode { )), cutoff_hz: SmoothedParam::new( cutoff_hz, + MAX_HZ - MIN_HZ, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/freeverb/mod.rs b/crates/firewheel-nodes/src/freeverb/mod.rs index 9f3c55d6..9fbd9a87 100644 --- a/crates/firewheel-nodes/src/freeverb/mod.rs +++ b/crates/firewheel-nodes/src/freeverb/mod.rs @@ -124,16 +124,19 @@ impl AudioNode for FreeverbNode { freeverb, damping: SmoothedParam::new( self.damping.clamp(0.0, 1.0), + 1.0, smoother_config, cx.stream_info.sample_rate, ), width: SmoothedParam::new( self.width.clamp(0.0, 1.0), + 1.0, smoother_config, cx.stream_info.sample_rate, ), room_size: SmoothedParam::new( self.room_size.clamp(0.0, 1.0), + 1.0, smoother_config, cx.stream_info.sample_rate, ), diff --git a/crates/firewheel-nodes/src/mix.rs b/crates/firewheel-nodes/src/mix.rs index 6ed73750..2f643cea 100644 --- a/crates/firewheel-nodes/src/mix.rs +++ b/crates/firewheel-nodes/src/mix.rs @@ -1,4 +1,5 @@ use firewheel_core::node::NodeError; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount, NonZeroChannelCount}, diff::{Diff, Patch}, @@ -189,6 +190,7 @@ impl AudioNode for MixNode { Ok(Processor { gain_0: SmoothedParam::new( gain_0, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() @@ -197,6 +199,7 @@ impl AudioNode for MixNode { ), gain_1: SmoothedParam::new( gain_1, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/noise_generator/pink.rs b/crates/firewheel-nodes/src/noise_generator/pink.rs index a0316453..b0645fc0 100644 --- a/crates/firewheel-nodes/src/noise_generator/pink.rs +++ b/crates/firewheel-nodes/src/noise_generator/pink.rs @@ -3,6 +3,7 @@ //! Base on the algorithm from use firewheel_core::node::NodeError; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount}, diff::{Diff, Patch}, @@ -88,6 +89,7 @@ impl AudioNode for PinkNoiseGenNode { Ok(Processor { gain: SmoothedParam::new( self.volume.amp_clamped(DEFAULT_MIN_AMP), + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/noise_generator/white.rs b/crates/firewheel-nodes/src/noise_generator/white.rs index 1310b429..0dfe1b38 100644 --- a/crates/firewheel-nodes/src/noise_generator/white.rs +++ b/crates/firewheel-nodes/src/noise_generator/white.rs @@ -1,6 +1,7 @@ //! A simple node that generates white noise. use firewheel_core::node::NodeError; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount}, diff::{Diff, Patch}, @@ -84,6 +85,7 @@ impl AudioNode for WhiteNoiseGenNode { fpd: seed, gain: SmoothedParam::new( self.volume.amp_clamped(DEFAULT_MIN_AMP), + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/spatial_basic.rs b/crates/firewheel-nodes/src/spatial_basic.rs index 0989cdf3..60d57cae 100644 --- a/crates/firewheel-nodes/src/spatial_basic.rs +++ b/crates/firewheel-nodes/src/spatial_basic.rs @@ -2,6 +2,7 @@ //! be used for 2D audio.) It does not make use of any fancy binaural algorithms, //! rather it just applies basic panning and filtering. +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; #[cfg(not(feature = "std"))] use num_traits::Float; @@ -226,6 +227,7 @@ impl AudioNode for SpatialBasicNode { Ok(Processor { gain_l: SmoothedParam::new( computed_values.gain_l, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() @@ -234,6 +236,7 @@ impl AudioNode for SpatialBasicNode { ), gain_r: SmoothedParam::new( computed_values.gain_r, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/svf.rs b/crates/firewheel-nodes/src/svf.rs index 52623dbf..a3b2d3e9 100644 --- a/crates/firewheel-nodes/src/svf.rs +++ b/crates/firewheel-nodes/src/svf.rs @@ -23,7 +23,6 @@ use firewheel_core::{ }; pub const DEFAULT_Q: f32 = Q_BUTTERWORTH_ORD2; - pub const DEFAULT_MIN_HZ: f32 = 20.0; pub const DEFAULT_MAX_HZ: f32 = 20_480.0; pub const DEFAULT_MIN_Q: f32 = 0.02; @@ -505,6 +504,7 @@ impl AudioNode for SvfNode { filter_type: self.filter_type, cutoff_hz: SmoothedParam::new( cutoff_hz, + config.freq_range.end - config.freq_range.start, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() @@ -513,6 +513,7 @@ impl AudioNode for SvfNode { ), q_factor: SmoothedParam::new( q_factor, + config.q_range.end - config.q_range.start, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() @@ -521,6 +522,7 @@ impl AudioNode for SvfNode { ), gain: SmoothedParam::new( gain, + max_gain - min_gain, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/volume.rs b/crates/firewheel-nodes/src/volume.rs index 18951471..539b6ba2 100644 --- a/crates/firewheel-nodes/src/volume.rs +++ b/crates/firewheel-nodes/src/volume.rs @@ -1,4 +1,5 @@ use firewheel_core::node::NodeError; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::{ChannelConfig, NonZeroChannelCount}, diff::{Diff, Patch}, @@ -151,6 +152,7 @@ impl AudioNode for VolumeNode { Ok(VolumeProcessor { gain: SmoothedParam::new( gain, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/crates/firewheel-nodes/src/volume_pan.rs b/crates/firewheel-nodes/src/volume_pan.rs index 90f9360f..60068312 100644 --- a/crates/firewheel-nodes/src/volume_pan.rs +++ b/crates/firewheel-nodes/src/volume_pan.rs @@ -1,5 +1,6 @@ pub use super::volume::VolumeNodeConfig; use firewheel_core::node::NodeError; +use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN; use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount}, diff::{Diff, Patch}, @@ -172,6 +173,7 @@ impl AudioNode for VolumePanNode { Ok(Processor { gain_l: SmoothedParam::new( gain_l, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() @@ -180,6 +182,7 @@ impl AudioNode for VolumePanNode { ), gain_r: SmoothedParam::new( gain_r, + DEFAULT_GAIN_SPAN, SmootherConfig { smooth_seconds: self.smooth_seconds, ..Default::default() diff --git a/examples/custom_nodes/src/nodes/filter.rs b/examples/custom_nodes/src/nodes/filter.rs index 454f7ee1..853900a0 100644 --- a/examples/custom_nodes/src/nodes/filter.rs +++ b/examples/custom_nodes/src/nodes/filter.rs @@ -8,6 +8,7 @@ use std::f32::consts::PI; use firewheel::dsp::coeff_update::{CoeffUpdateFactor, CoeffUpdateMask}; use firewheel::node::NodeError; +use firewheel::param::smoother::DEFAULT_GAIN_SPAN; use firewheel::{ channel_config::{ChannelConfig, ChannelCount}, diff::{Diff, Patch}, @@ -21,7 +22,7 @@ use firewheel::{ StreamInfo, }; -// The node struct holds all of the parameters of the node as plain values. +/// The node struct holds all of the parameters of the node as plain values. /// /// # Notes about ECS /// @@ -109,12 +110,20 @@ impl AudioNode for FilterNode { Ok(Processor { filter_l: OnePoleLPBiquad::new(cutoff_hz, sample_rate_recip), filter_r: OnePoleLPBiquad::new(cutoff_hz, sample_rate_recip), + // See the documentation of `SmoothedParam` for more information on what + // these arguments mean. cutoff_hz: SmoothedParam::new( - cutoff_hz, - Default::default(), - cx.stream_info.sample_rate, + cutoff_hz, // initial_value + 20_000.0 - 20.0, // value_span + Default::default(), // config + cx.stream_info.sample_rate, // sample_rate + ), + gain: SmoothedParamBuffer::new( + gain, // initial_value + DEFAULT_GAIN_SPAN, // value_span + Default::default(), // config + cx.stream_info, // stream_info ), - gain: SmoothedParamBuffer::new(gain, Default::default(), cx.stream_info), coeff_update_mask: self.coeff_update_factor.mask(), }) } @@ -127,7 +136,11 @@ struct Processor { // A helper struct to smooth a parameter. cutoff_hz: SmoothedParam, // This is similar to `SmoothedParam`, but it also contains an allocated buffer - // for the smoothed values. + // for the smoothed values. This uses a bit more memory and may be slighly less + // efficient, but it is easier to use since it doesn't need separate code paths + // to optimize different smoothing states. While the regular `SmoothedParam` + // would suffice here for this simple plugin, this demonstrates how the buffered + // variant is used if desired. gain: SmoothedParamBuffer, coeff_update_mask: CoeffUpdateMask, } From 4a8800d55ecae5826b1f7f976cb4fd3719935550 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Tue, 7 Jul 2026 23:42:49 -0500 Subject: [PATCH 2/4] update reamde --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a9f6792a..f358801a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ [![Crates.io](https://img.shields.io/crates/v/firewheel.svg)](https://crates.io/crates/firewheel) [![License](https://img.shields.io/crates/l/firewheel.svg)](https://github.com/BillyDM/firewheel/blob/main/LICENSE-APACHE) -A mid-level open source audio graph engine for games and other applications, written in Rust. +An open source audio graph engine for games and other applications, written in Rust. It can be used as-is, or it can be used as a base for other higher-level audio engines. -This crate can be used as-is or as a base for other higher-level audio engines. (Think of it like [wgpu](https://wgpu.rs/) but for audio). +## The Future of this Project + +Firewheel is currently planned to be upstreamed into the [Bevy](https://bevy.org/) game engine where it will become the default audio engine. (The current implementation can be found in the [bevy_seedling](https://github.com/CorvusPrudens/bevy_seedling) repository.) The goal is to both make Bevy integration easier by avoiding circular dependencies, and to ease the maintanance burden of a project that has experienced more feature creep than what the author originally anticipated. + +While development has shifted to focus more on the needs of Bevy, the core audio graph engine will still be available to use outside of Bevy without any other Bevy dependencies (except for the very lightweight [bevy_platform](https://crates.io/crates/bevy_platform) dependency). ## Key Features @@ -21,18 +25,14 @@ This crate can be used as-is or as a base for other higher-level audio engines. * Fault tolerance for audio streams (The game shouldn't stop or crash just because the player accidentally unplugged their headphones.) * Properly respects realtime constraints (no mutexes!) * `no_std` compatibility (some features require the standard library) -* (TODO) Basic [CLAP] plugin hosting (non-WASM only), allowing for more open source and proprietary 3rd party effects and synths -* (TODO) Bindings for C, and (possibly) C++ and C# ## Non-features -While Firewheel is meant to cover nearly every use case for games and other applications, it is not meant to be a complete DAW (digital audio workstation) engine. Not only would this greatly increase complexity, but the needs of game audio engines and DAW audio engines are in conflict. (See the design document for more details on why). +While Firewheel aims to cover most use cases for games and other generic applications, it does *NOT* aim to be a complete DAW (digital audio workstation) engine. Not only would this greatly increase complexity, but the needs of game audio engines and DAW audio engines are in conflict. (See the design document for more details on why). ## Get Involved -Join the discussion in the [Firewheel Discord Server](https://discord.gg/rKzZpjGCGs) or in the [Bevy Discord Server](https://discord.gg/bevy) under the `working-groups -> Better Audio` channel! - -If you are interested in contributing code, first read the [Design Document] and then visit the [Project Board](https://github.com/users/BillyDM/projects/1). +Join the discussion in the [Bevy Discord Server](https://discord.gg/bevy) under the `working-groups -> Better Audio` channel! ## License From 041fcb25b03017a967337154d42eb977be29f1b4 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 8 Jul 2026 17:26:05 -0500 Subject: [PATCH 3/4] tweak smoothing filter, improve silence detection logic in nodes --- crates/firewheel-core/src/collector.rs | 2 +- .../src/dsp/filter/smoothing_filter.rs | 109 +++++++++--------- crates/firewheel-core/src/node.rs | 24 +++- crates/firewheel-core/src/param/smoother.rs | 61 +++++----- crates/firewheel-graph/src/processor.rs | 1 - .../src/processor/handle_messages.rs | 1 - .../firewheel-graph/src/processor/process.rs | 16 --- crates/firewheel-nodes/src/convolution.rs | 6 +- .../src/fast_filters/bandpass.rs | 6 +- .../src/fast_filters/highpass.rs | 6 +- .../src/fast_filters/lowpass.rs | 6 +- crates/firewheel-nodes/src/freeverb/mod.rs | 20 ++-- crates/firewheel-nodes/src/mix.rs | 15 ++- crates/firewheel-nodes/src/spatial_basic.rs | 15 ++- crates/firewheel-nodes/src/svf.rs | 6 +- crates/firewheel-nodes/src/volume.rs | 15 ++- crates/firewheel-nodes/src/volume_pan.rs | 15 ++- 17 files changed, 175 insertions(+), 149 deletions(-) diff --git a/crates/firewheel-core/src/collector.rs b/crates/firewheel-core/src/collector.rs index dc59d58f..1d4448be 100644 --- a/crates/firewheel-core/src/collector.rs +++ b/crates/firewheel-core/src/collector.rs @@ -279,7 +279,7 @@ impl ArcGc { /// Construct a type-erased [`ArcGc`]. /// /// ``` - /// # use rtgc::*; + /// # use firewheel-core::collector::ArcGC; /// # use std::sync::Arc; /// # use std::any::Any; /// let value: ArcGc = diff --git a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs index 95313bde..8c10a874 100644 --- a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs +++ b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs @@ -5,24 +5,24 @@ use core::num::NonZeroU32; /// 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; +/// This value is chosen to where the halfway decay point is roughly equal to a +/// typical block size of 1024 samples (23 ms), which should eliminate the stair-stepping +/// for most games. +pub const DEFAULT_SMOOTH_SECONDS: f32 = 46.0 / 1_000.0; -/// The default epsilon value for a [`SmoothingFilter`]. -pub const DEFAULT_SETTLE_EPSILON: f32 = 0.01; +/// The default settle ratio value for a [`SmoothingFilter`]. +pub const DEFAULT_SETTLE_RATIO: f32 = 0.01; -/// The minimum supported epsilon value for a [`SmoothingFilter`]. +/// The minimum supported settle ratio value for a [`SmoothingFilter`]. /// /// Values smaller than this can tend to never settle correctly due to floating point /// accumulation errors. -pub const MIN_SETTLE_EPSILON: f32 = 0.00075; -/// The minimum supported epsilon value for a [`SmoothingFilter`]. +pub const MIN_SETTLE_RATIO: f32 = 0.00075; +/// The maximum supported settle ratio value for a [`SmoothingFilter`]. /// /// Values larger than this can tend to never settle correctly due to floating point /// accumulation errors. -pub const MAX_SETTLE_EPSILON: f32 = 0.9; +pub const MAX_SETTLE_RATIO: f32 = 0.9; /// The coefficients for a simple smoothing/declicking filter where: /// @@ -37,56 +37,57 @@ impl SmoothingFilterCoeff { /// Calculate the coefficients for a [`SmoothingFilter`]. /// /// * `sample_rate` - The sample rate of the signal. - /// * `smooth_secs` - The amount of time it takes for the filter to smooth from one - /// end of the parameter's range to the other end. - /// * If less than 0.0, then it will be clamped to 0.0. - /// * `settle_epsilon` - The threshold at which the filter is considered "settled". + /// * `smooth_secs` - The amount of time in seconds it takes for the filter to smooth + /// from one value to another. + /// * If less than 0.0, then 0.0 will be used. + /// * `settle_ratio` - The threshold at which the filter is considered "settled". /// For example `0.01` means that the filter is considered settled if the value is /// within 1% of the total span of the parameter's range. Must be >= - /// [`MIN_SETTLE_EPSILON`] (0.00075) and <= [`MAX_SETTLE_EPSILON`] (0.9). + /// [`MIN_SETTLE_RATIO`] (0.00075) and <= [`MAX_SETTLE_RATIO`] (0.9). /// * Will be clamped to the range `[0.00075..0.9]`. + /// /// Returns `true` if this filter is settled, `false` if not. - pub fn new(sample_rate: NonZeroU32, smooth_secs: f32, epsilon: f32) -> Self { + pub fn new(sample_rate: NonZeroU32, smooth_secs: f32, settle_ratio: f32) -> Self { let smooth_secs = smooth_secs.max(0.0); - let epsilon = epsilon.clamp(MIN_SETTLE_EPSILON, MAX_SETTLE_EPSILON); + let ratio = settle_ratio.clamp(MIN_SETTLE_RATIO, MAX_SETTLE_RATIO); // The b1 coefficient of a one pole lp filter is given by: // // b1 = e ^ (-1 / t_to_1_over_e) // - // where t_to_1_over_e is the amount of time for the filter to decay to 1/e - // (about 36.8%). + // where t_to_1_over_e is the amount of time in frames for an impulse signal + // to decay to 1/e (about 36.8%). // - // So, to get a filter which decays to a given "epsilon" value in - // "t_to_epsilon" frames, we need to adjust the t_to_1_over_e value. Because - // doubling the time is equivalent to decaying by another 36.8%, we can - // relate epsilon and t_to_epsilon with: + // So, to find the coefficients where an impulse signal decays to a "ratio" + // in "t_to_ratio" frames, we need to adjust the t_to_1_over_e value. + // Because doubling the time is equivalent to decaying by another 36.8%, we + // can relate ratio and t_to_ratio with: // - // epsilon = (1/e) ^ c - // t_to_epsilon = t_to_1_over_e * c + // ratio = (1/e) ^ c + // t_to_ratio = t_to_1_over_e * c // // where c is some unknown constant. // // Solve for c: // - // c = log_(1/e)(epsilon) - // = -ln(epsilon) + // c = log_(1/e)(ratio) + // = -ln(ratio) // // Solve for t_to_1_over_e: // - // t_to_1_over_e = t_to_epsilon / c - // = t_to_epsilon * (1 / -ln(epsilon)) - // = -ln(epsilon) / t_to_epsilon + // t_to_1_over_e = t_to_ratio / c + // = t_to_ratio * (1 / -ln(ratio)) + // = -ln(ratio) / t_to_ratio // // Which finally gives us: // - // b1 = e ^ (-1 / (-ln(epsilon) / t_to_epsilon)) - // = e ^ (ln(epsilon) / t_to_epsilon) - // = epsilon ^ (1 / t_to_epsilon) + // b1 = e ^ (-1 / (-ln(ratio) / t_to_ratio)) + // = e ^ (ln(ratio) / t_to_ratio) + // = ratio ^ (1 / t_to_ratio) - let t_to_epsilon = (smooth_secs * sample_rate.get() as f32).max(1.0); + let t_to_ratio = (smooth_secs * sample_rate.get() as f32).max(1.0); - let b1 = epsilon.powf(t_to_epsilon.recip()); + let b1 = ratio.powf(t_to_ratio.recip()); let a0 = 1.0f32 - b1; Self { a0, b1 } @@ -134,16 +135,27 @@ impl SmoothingFilter { /// Settle the filter if its state is close enough to the target value. /// /// * `target` - The target value that is being smoothed to. - /// * `span` - The size of the parameter's range (equal to `(max_value - min_value).abs()`). - /// * `settle_epsilon` - The threshold at which the filter is considered "settled". + /// * `value_span` - The size of this parameter's range. + /// * If the minimum and maximum values of the parameter are known, then + /// typically `max_value - min_value` should be used. + /// * If the min and/or max values are not known, then use a span value that is + /// typical to be the worst-case-scenario (For example, if creating a "gain" + /// parameter, a good value to use is [`DEFAULT_GAIN_SPAN`] (`2.0`) since + /// immediately jumping from 0% volume to 200% volume or vice versa is typically + /// the worst-case-scenario). + /// * This value does not need to be positive. + /// * `settle_ratio` - The threshold at which the filter is considered "settled". /// For example `0.01` means that the filter is considered settled if the value is - /// within 1% of the total `span`. + /// within 1% of the total `value_span`. /// /// Returns `true` if this filter is settled, `false` if not. - pub fn settle(&mut self, target: f32, span: f32, settle_epsilon: f32) -> bool { + /// + /// [`DEFAULT_GAIN_SPAN`]: crate::param::smoother::DEFAULT_GAIN_SPAN + pub fn try_settle(&mut self, target: f32, value_span: f32, settle_ratio: f32) -> bool { if self.z1 == target { true - } else if span == 0.0 || (self.z1 - target).abs() < (span * settle_epsilon).abs() { + } else if value_span == 0.0 || (self.z1 - target).abs() < (value_span * settle_ratio).abs() + { self.z1 = target; true } else { @@ -167,18 +179,11 @@ mod test { 0.0, 1.0 / 1_000.0, 5.0 / 1_000.0, - 23.0 / 1_000.0, + DEFAULT_SMOOTH_SECONDS, 100.0 / 1_000.0, 500.0 / 1_000.0, ]; - const TEST_EPSILONS: [f32; 6] = [ - MIN_SETTLE_EPSILON, - 0.001, - 0.01, - 0.1, - 0.5, - MAX_SETTLE_EPSILON, - ]; + const TEST_RATIOS: [f32; 6] = [MIN_SETTLE_RATIO, 0.001, 0.01, 0.1, 0.5, MAX_SETTLE_RATIO]; const TEST_VALUES: [(f32, f32); 5] = [ (1.0, 0.0), (0.0, 1.0), @@ -191,14 +196,14 @@ mod test { for sr in TEST_SAMPLE_RATES { for secs in TEST_SECONDS { - for eps in TEST_EPSILONS { + for ratio in TEST_RATIOS { for (start_value, end_value) in TEST_VALUES { let coeff = - SmoothingFilterCoeff::new(NonZeroU32::new(sr).unwrap(), secs, eps); + SmoothingFilterCoeff::new(NonZeroU32::new(sr).unwrap(), secs, ratio); let mut filter = SmoothingFilter::new(start_value); let mut frames: u32 = 0; - while !filter.settle(end_value, (start_value - end_value).abs(), eps) + while !filter.try_settle(end_value, (start_value - end_value).abs(), ratio) && frames < sr * 4 { let _ = filter.process(end_value, coeff); diff --git a/crates/firewheel-core/src/node.rs b/crates/firewheel-core/src/node.rs index a8f92cc6..214f1315 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -648,6 +648,25 @@ impl<'a, 'b> ProcBuffers<'a, 'b> { ProcessStatus::OutputsModified } } + + /// Returns `true` if the input signal has settled to silence by the end + /// of the block. + pub fn inputs_settled_at_zero(&self) -> bool { + let mut settled_at_zero = true; + + for ch in self.inputs.iter() { + // Check the last two samples instead of just one since it is + // incredibly unlikely that an active signal has two exact + // zeros in a row. + settled_at_zero = ch.iter().rev().take(2).all(|s| *s == 0.0); + + if !settled_at_zero { + break; + } + } + + settled_at_zero + } } /// Extra buffers and utilities for [`AudioNodeProcessor::process`] @@ -714,11 +733,6 @@ pub struct ProcInfo { /// nodes in the graph. pub out_connected_mask: ConnectedMask, - /// If the previous processing block had all output buffers silent - /// (or if this is the first processing block), then this will be - /// `true`. Otherwise, this will be `false`. - pub prev_output_was_silent: bool, - /// The sample rate of the audio stream in samples per second. pub sample_rate: NonZeroU32, diff --git a/crates/firewheel-core/src/param/smoother.rs b/crates/firewheel-core/src/param/smoother.rs index 57899f5e..70515ac9 100644 --- a/crates/firewheel-core/src/param/smoother.rs +++ b/crates/firewheel-core/src/param/smoother.rs @@ -6,7 +6,7 @@ use bevy_platform::prelude::Vec; use crate::{ StreamInfo, dsp::filter::smoothing_filter::{ - self, MAX_SETTLE_EPSILON, MIN_SETTLE_EPSILON, SmoothingFilter, SmoothingFilterCoeff, + self, MAX_SETTLE_RATIO, MIN_SETTLE_RATIO, SmoothingFilter, SmoothingFilterCoeff, }, }; @@ -22,28 +22,29 @@ pub const DEFAULT_GAIN_SPAN: f32 = 2.0; #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SmootherConfig { - /// The amount of smoothing in seconds + /// The amount of time in seconds it takes for the filter to smooth from one + /// value to another. /// - /// If less than 0.0, then it will be clamped to 0.0. + /// If less than 0.0, then 0.0 will be used. /// /// By default this is set to 23 milliseconds. pub smooth_seconds: f32, - /// The threshold at which the smoothing will complete + /// The threshold at which the filter is considered "settled". /// - /// For example `0.01` means that the filter is considered settled if the - /// value is within 1% of the total span of the parameter's range. + /// For example `0.01` means that the filter is considered settled if the value + /// is within 1% of the parameter's range. /// /// Will be clamped to the range `[0.00075..0.9]`. /// /// By default this is set to `0.01`. - pub settle_epsilon: f32, + pub settle_ratio: f32, } impl Default for SmootherConfig { fn default() -> Self { Self { smooth_seconds: smoothing_filter::DEFAULT_SMOOTH_SECONDS, - settle_epsilon: smoothing_filter::DEFAULT_SETTLE_EPSILON, + settle_ratio: smoothing_filter::DEFAULT_SETTLE_RATIO, } } } @@ -56,7 +57,7 @@ pub struct SmoothedParam { filter: SmoothingFilter, coeff: SmoothingFilterCoeff, smooth_secs: f32, - settle_epsilon: f32, + settle_ratio: f32, span: f32, } @@ -64,15 +65,14 @@ impl SmoothedParam { /// Construct a new smoothed f32 parameter with the given configuration. /// /// * `initial_value` - The initial target value. - /// * `value_span` - The difference between two values where the maximum amount - /// of smoothing will take effect. + /// * `value_span` - The size of this parameter's range. /// * If the minimum and maximum values of the parameter are known, then /// typically `max_value - min_value` should be used. /// * If the min and/or max values are not known, then use a span value that is - /// typical to be the worst-case-scenario (For example, if creating a "gain" - /// parameter, a good value to use is [`DEFAULT_AMP_SPAN`] (`2.0`) since - /// immediately jumping from 0% volume to 200% volume or vice versa is typically - /// the worst-case-scenario). + /// typical to be the worst-case-scenario (For example, if creating a "gain" + /// parameter, a good value to use is [`DEFAULT_GAIN_SPAN`] (`2.0`) since + /// immediately jumping from 0% volume to 200% volume or vice versa is typically + /// the worst-case-scenario). /// * This value does not need to be positive. /// * `config` - Extra configuration options for the smoothing filter. /// * `sample_rate` - The sample rate of the stream. @@ -83,11 +83,11 @@ impl SmoothedParam { sample_rate: NonZeroU32, ) -> Self { let smooth_secs = config.smooth_seconds.max(0.0); - let settle_epsilon = config - .settle_epsilon - .clamp(MIN_SETTLE_EPSILON, MAX_SETTLE_EPSILON); + let settle_ratio = config + .settle_ratio + .clamp(MIN_SETTLE_RATIO, MAX_SETTLE_RATIO); - let coeff = SmoothingFilterCoeff::new(sample_rate, smooth_secs, settle_epsilon); + let coeff = SmoothingFilterCoeff::new(sample_rate, smooth_secs, settle_ratio); Self { target_value: initial_value, @@ -96,7 +96,7 @@ impl SmoothedParam { span: value_span.abs(), coeff, smooth_secs, - settle_epsilon, + settle_ratio, } } @@ -116,7 +116,7 @@ impl SmoothedParam { /// Returns `true` if this filter is settled, `false` if not. pub fn settle(&mut self) -> bool { self.filter - .settle(self.target_value, self.span, self.settle_epsilon) + .try_settle(self.target_value, self.span, self.settle_ratio) } /// Returns `true` if this parameter is currently smoothing this process cycle, @@ -162,20 +162,20 @@ impl SmoothedParam { .process_into_buffer(buffer, self.target_value, self.coeff); self.filter - .settle(self.target_value, self.span, self.settle_epsilon); + .try_settle(self.target_value, self.span, self.settle_ratio); } else { buffer.fill(self.target_value); } } pub fn set_smooth_seconds(&mut self, seconds: f32, sample_rate: NonZeroU32) { - self.coeff = SmoothingFilterCoeff::new(sample_rate, seconds, self.settle_epsilon); + self.coeff = SmoothingFilterCoeff::new(sample_rate, seconds, self.settle_ratio); self.smooth_secs = seconds; } /// Update the sample rate. pub fn update_sample_rate(&mut self, sample_rate: NonZeroU32) { - self.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs, self.settle_epsilon); + self.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs, self.settle_ratio); } } @@ -192,15 +192,14 @@ impl SmoothedParamBuffer { /// configuration. /// /// * `initial_value` - The initial target value. - /// * `value_span` - The difference between two values where the maximum amount - /// of smoothing will take effect. + /// * `value_span` - The size of this parameter's range. /// * If the minimum and maximum values of the parameter are known, then - /// typically this would be `max_value - min_value`. + /// typically `max_value - min_value` should be used. /// * If the min and/or max values are not known, then use a span value that is - /// typical to be the worst-case-scenario (For example, if creating a "gain" - /// parameter, a good value to use is [`DEFAULT_AMP_SPAN`] (`2.0`) since - /// immediately jumping from 0% volume to 200% volume or vice versa is typically - /// the worst-case-scenario). + /// typical to be the worst-case-scenario (For example, if creating a "gain" + /// parameter, a good value to use is [`DEFAULT_GAIN_SPAN`] (`2.0`) since + /// immediately jumping from 0% volume to 200% volume or vice versa is typically + /// the worst-case-scenario). /// * This value does not need to be positive. /// * `config` - Extra configuration options for the smoothing filter. /// * `stream_info` - The information about the current stream. diff --git a/crates/firewheel-graph/src/processor.rs b/crates/firewheel-graph/src/processor.rs index 4a4a7964..f544e7bf 100644 --- a/crates/firewheel-graph/src/processor.rs +++ b/crates/firewheel-graph/src/processor.rs @@ -234,7 +234,6 @@ impl FirewheelProcessorInner { pub(crate) struct NodeEntry { pub processor: Box, - pub prev_output_was_silent: bool, pub bypass_declick: Declicker, pub is_bypassed: bool, pub is_first_process: bool, diff --git a/crates/firewheel-graph/src/processor/handle_messages.rs b/crates/firewheel-graph/src/processor/handle_messages.rs index 573fa85e..c1b4c14b 100644 --- a/crates/firewheel-graph/src/processor/handle_messages.rs +++ b/crates/firewheel-graph/src/processor/handle_messages.rs @@ -125,7 +125,6 @@ impl FirewheelProcessorInner { n.id.0, NodeEntry { processor: n.processor, - prev_output_was_silent: true, event_data: NodeEventSchedulerData::new(n.is_pre_process), bypass_declick: Declicker::SettledAt1, is_bypassed: false, diff --git a/crates/firewheel-graph/src/processor/process.rs b/crates/firewheel-graph/src/processor/process.rs index 8d581f16..fe6f1dae 100644 --- a/crates/firewheel-graph/src/processor/process.rs +++ b/crates/firewheel-graph/src/processor/process.rs @@ -291,7 +291,6 @@ impl FirewheelProcessorInner { out_constant_mask: ConstantMask::default(), in_connected_mask: ConnectedMask::default(), out_connected_mask: ConnectedMask::default(), - prev_output_was_silent: false, sample_rate, sample_rate_recip, clock_samples, @@ -410,7 +409,6 @@ impl FirewheelProcessorInner { // Set the timing information for the process info for this sub-chunk. info.frames = sub_chunk_frames; info.clock_samples = sub_clock_samples; - info.prev_output_was_silent = node_entry.prev_output_was_silent; info.did_just_unbypass = false; // Call the node's process method. @@ -522,20 +520,6 @@ impl FirewheelProcessorInner { ); } - node_entry.prev_output_was_silent = match process_status { - ProcessStatus::ClearAllOutputs => true, - ProcessStatus::Bypass => info - .in_silence_mask - .all_channels_silent(proc_buffers.inputs.len()), - ProcessStatus::OutputsModified => false, - ProcessStatus::OutputsModifiedWithMask(out_mask) => match out_mask { - MaskType::Silence(mask) => { - mask.all_channels_silent(proc_buffers.outputs.len()) - } - MaskType::Constant(_) => false, - }, - }; - // If there are multiple sub-chunks, and the node returned a different process // status this sub-chunk than the previous sub-chunk, then we must manually // handle the process statuses. diff --git a/crates/firewheel-nodes/src/convolution.rs b/crates/firewheel-nodes/src/convolution.rs index e6d8b38a..d4ebed70 100644 --- a/crates/firewheel-nodes/src/convolution.rs +++ b/crates/firewheel-nodes/src/convolution.rs @@ -89,9 +89,9 @@ pub struct ConvolutionNode { /// Adjusts the time in seconds over which parameters are smoothed for `mix` /// and `wet_gain`. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, } diff --git a/crates/firewheel-nodes/src/fast_filters/bandpass.rs b/crates/firewheel-nodes/src/fast_filters/bandpass.rs index 70606685..078aab6c 100644 --- a/crates/firewheel-nodes/src/fast_filters/bandpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/bandpass.rs @@ -37,9 +37,9 @@ pub struct FastBandpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/fast_filters/highpass.rs b/crates/firewheel-nodes/src/fast_filters/highpass.rs index 1280d6c3..3feab90a 100644 --- a/crates/firewheel-nodes/src/fast_filters/highpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/highpass.rs @@ -34,9 +34,9 @@ pub struct FastHighpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/fast_filters/lowpass.rs b/crates/firewheel-nodes/src/fast_filters/lowpass.rs index b8796e71..0a374679 100644 --- a/crates/firewheel-nodes/src/fast_filters/lowpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/lowpass.rs @@ -33,9 +33,9 @@ pub struct FastLowpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/freeverb/mod.rs b/crates/firewheel-nodes/src/freeverb/mod.rs index 9fbd9a87..f3c48eca 100644 --- a/crates/firewheel-nodes/src/freeverb/mod.rs +++ b/crates/firewheel-nodes/src/freeverb/mod.rs @@ -5,6 +5,7 @@ #![allow(clippy::module_inception)] use firewheel_core::dsp::coeff_update::{CoeffUpdateFactor, CoeffUpdateMask}; +use firewheel_core::dsp::filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS; use firewheel_core::node::NodeError; use firewheel_core::{ channel_config::{ChannelConfig, ChannelCount}, @@ -64,9 +65,9 @@ pub struct FreeverbNode { /// Adjusts the time in seconds over which parameters are smoothed. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are @@ -91,7 +92,7 @@ impl Default for FreeverbNode { width: 0.5, pause: false, reset: Notify::new(()), - smooth_seconds: 0.015, + smooth_seconds: DEFAULT_SMOOTH_SECONDS, coeff_update_factor: CoeffUpdateFactor::default(), } } @@ -148,6 +149,7 @@ impl AudioNode for FreeverbNode { }, values: DeclickValues::new(cx.stream_info.declick_frames), coeff_update_mask: self.coeff_update_factor.mask(), + prev_output_was_silent: true, }; processor.apply_parameters(); @@ -165,6 +167,7 @@ struct FreeverbProcessor { pause_declicker: Declicker, values: DeclickValues, coeff_update_mask: CoeffUpdateMask, + prev_output_was_silent: bool, } impl FreeverbProcessor { @@ -233,14 +236,15 @@ impl AudioNodeProcessor for FreeverbProcessor { let all_silent = info.in_silence_mask.all_channels_silent(2); if (self.paused && self.pause_declicker.has_settled()) - || (all_silent && info.prev_output_was_silent) + || (all_silent && self.prev_output_was_silent) { self.reset(false); + self.prev_output_was_silent = true; return ProcessStatus::ClearAllOutputs; } - if !all_silent && info.prev_output_was_silent { + if !all_silent && self.prev_output_was_silent { // re-apply the parameters self.apply_parameters(); } @@ -291,7 +295,7 @@ impl AudioNodeProcessor for FreeverbProcessor { // We do this before the declicking just to make sure we // finish declicking if we're paused simultaneously with the // input going silent. - if all_silent && !info.prev_output_was_silent { + if all_silent && !self.prev_output_was_silent { // check the output buffers to see if they pass // the threshold for "completely silent" @@ -299,6 +303,7 @@ impl AudioNodeProcessor for FreeverbProcessor { buffers.check_for_silence_on_outputs(DEFAULT_MIN_AMP), ProcessStatus::ClearAllOutputs ) { + self.prev_output_was_silent = true; return ProcessStatus::ClearAllOutputs; } } @@ -322,6 +327,7 @@ impl AudioNodeProcessor for FreeverbProcessor { self.width.update_sample_rate(stream_info.sample_rate); self.room_size.update_sample_rate(stream_info.sample_rate); self.reset(true); + self.prev_output_was_silent = true; } } diff --git a/crates/firewheel-nodes/src/mix.rs b/crates/firewheel-nodes/src/mix.rs index 2f643cea..9a57bc54 100644 --- a/crates/firewheel-nodes/src/mix.rs +++ b/crates/firewheel-nodes/src/mix.rs @@ -72,9 +72,9 @@ pub struct MixNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to @@ -208,6 +208,7 @@ impl AudioNode for MixNode { ), params: *self, min_gain, + prev_input_settled: true, }) } } @@ -219,6 +220,7 @@ struct Processor { params: MixNode, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for Processor { @@ -252,8 +254,8 @@ impl AudioNodeProcessor for Processor { self.gain_0.set_value(gain_0); self.gain_1.set_value(gain_1); - if info.prev_output_was_silent { - // Previous block was silent, so no need to smooth. + if self.prev_input_settled { + // The previous block's input settled at zero, so no need to smooth. self.gain_0.reset_to_target(); self.gain_1.reset_to_target(); } @@ -284,10 +286,13 @@ impl AudioNodeProcessor for Processor { { self.gain_0.reset_to_target(); self.gain_1.reset_to_target(); + self.prev_input_settled = true; return ProcessStatus::ClearAllOutputs; } + self.prev_input_settled = buffers.inputs_settled_at_zero(); + let mut out_silence_mask = SilenceMask::NONE_SILENT; if has_settled { diff --git a/crates/firewheel-nodes/src/spatial_basic.rs b/crates/firewheel-nodes/src/spatial_basic.rs index 60d57cae..56b6f253 100644 --- a/crates/firewheel-nodes/src/spatial_basic.rs +++ b/crates/firewheel-nodes/src/spatial_basic.rs @@ -91,9 +91,9 @@ pub struct SpatialBasicNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less than or equal /// to this value, the the gain will be clamped to `0` (silence). @@ -252,6 +252,7 @@ impl AudioNode for SpatialBasicNode { self.coeff_update_factor, ), params: *self, + prev_input_settled: true, }) } } @@ -263,6 +264,7 @@ struct Processor { distance_attenuator: DistanceAttenuatorStereoDsp, params: SpatialBasicNode, + prev_input_settled: bool, } impl Processor { @@ -318,8 +320,8 @@ impl AudioNodeProcessor for Processor { self.params.min_gain, ); - if info.prev_output_was_silent { - // Previous block was silent, so no need to smooth. + if self.prev_input_settled { + // The previous block's input settled at zero, so no need to smooth. self.reset(); } } @@ -337,9 +339,12 @@ impl AudioNodeProcessor for Processor { ) -> ProcessStatus { if info.in_silence_mask.all_channels_silent(2) { self.reset(); + self.prev_input_settled = true; return ProcessStatus::ClearAllOutputs; } + self.prev_input_settled = buffers.inputs_settled_at_zero(); + let scratch_buffer = extra.scratch_buffers.first_mut(); let (in1, in2) = if info.in_connected_mask == ConnectedMask::STEREO_CONNECTED { diff --git a/crates/firewheel-nodes/src/svf.rs b/crates/firewheel-nodes/src/svf.rs index a3b2d3e9..a38c0397 100644 --- a/crates/firewheel-nodes/src/svf.rs +++ b/crates/firewheel-nodes/src/svf.rs @@ -132,9 +132,9 @@ pub struct SvfNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/volume.rs b/crates/firewheel-nodes/src/volume.rs index 539b6ba2..60c76060 100644 --- a/crates/firewheel-nodes/src/volume.rs +++ b/crates/firewheel-nodes/src/volume.rs @@ -45,9 +45,9 @@ pub struct VolumeNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to @@ -161,6 +161,7 @@ impl AudioNode for VolumeNode { ), min_gain, num_channels: config.channels.get().get() as usize, + prev_input_settled: true, }) } } @@ -170,6 +171,7 @@ struct VolumeProcessor { num_channels: usize, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for VolumeProcessor { @@ -183,8 +185,8 @@ impl AudioNodeProcessor for VolumeProcessor { } self.gain.set_value(gain); - if info.prev_output_was_silent { - // Previous block was silent, so no need to smooth. + if self.prev_input_settled { + // The previous block's input settled at zero, so no need to smooth. self.gain.reset_to_target(); } } @@ -212,10 +214,13 @@ impl AudioNodeProcessor for VolumeProcessor { // All channels are silent, so there is no need to process. Also reset // the filter since it doesn't need to smooth anything. self.gain.reset_to_target(); + self.prev_input_settled = true; return ProcessStatus::ClearAllOutputs; } + self.prev_input_settled = buffers.inputs_settled_at_zero(); + if self.gain.has_settled() { if self.gain.target_value() <= self.min_gain { // Muted, so there is no need to process. diff --git a/crates/firewheel-nodes/src/volume_pan.rs b/crates/firewheel-nodes/src/volume_pan.rs index 60068312..ec26c95e 100644 --- a/crates/firewheel-nodes/src/volume_pan.rs +++ b/crates/firewheel-nodes/src/volume_pan.rs @@ -36,9 +36,9 @@ pub struct VolumePanNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.023` (23ms). 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. + /// By default this is set to `0.046` (46ms). This value is chosen to where + /// the halfway decay point is roughly equal to a typical block size of 1024 + /// samples (23 ms), which should eliminate the stair-stepping for most games. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to @@ -191,6 +191,7 @@ impl AudioNode for VolumePanNode { ), params: *self, min_gain, + prev_input_settled: true, }) } } @@ -202,6 +203,7 @@ struct Processor { params: VolumePanNode, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for Processor { @@ -231,8 +233,8 @@ impl AudioNodeProcessor for Processor { self.gain_l.set_value(gain_l); self.gain_r.set_value(gain_r); - if info.prev_output_was_silent { - // Previous block was silent, so no need to smooth. + if self.prev_input_settled { + // The previous block's input settled at zero, so no need to smooth. self.gain_l.reset_to_target(); self.gain_r.reset_to_target(); } @@ -253,10 +255,13 @@ impl AudioNodeProcessor for Processor { if info.in_silence_mask.all_channels_silent(2) { self.gain_l.reset_to_target(); self.gain_r.reset_to_target(); + self.prev_input_settled = true; return ProcessStatus::ClearAllOutputs; } + self.prev_input_settled = buffers.inputs_settled_at_zero(); + let in1 = &buffers.inputs[0][..info.frames]; let in2 = &buffers.inputs[1][..info.frames]; let (out1, out2) = buffers.outputs.split_first_mut().unwrap(); From d9ee9de644f6c4fd2427ab5b519b0d54e1393245 Mon Sep 17 00:00:00 2001 From: Billy Messenger Date: Wed, 8 Jul 2026 18:04:38 -0500 Subject: [PATCH 4/4] tweak DEFAULT_SMOOTH_SECONDS value --- crates/firewheel-core/src/dsp/filter/smoothing_filter.rs | 7 +++---- crates/firewheel-core/src/param/smoother.rs | 4 +++- crates/firewheel-nodes/src/convolution.rs | 6 +++--- crates/firewheel-nodes/src/fast_filters/bandpass.rs | 6 +++--- crates/firewheel-nodes/src/fast_filters/highpass.rs | 6 +++--- crates/firewheel-nodes/src/fast_filters/lowpass.rs | 6 +++--- crates/firewheel-nodes/src/freeverb/mod.rs | 6 +++--- crates/firewheel-nodes/src/mix.rs | 6 +++--- crates/firewheel-nodes/src/spatial_basic.rs | 6 +++--- crates/firewheel-nodes/src/svf.rs | 6 +++--- crates/firewheel-nodes/src/volume.rs | 6 +++--- crates/firewheel-nodes/src/volume_pan.rs | 6 +++--- 12 files changed, 36 insertions(+), 35 deletions(-) diff --git a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs index 8c10a874..70d71723 100644 --- a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs +++ b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs @@ -5,10 +5,9 @@ use core::num::NonZeroU32; /// The default number of seconds for a [`Smoothing Filter`]. /// -/// This value is chosen to where the halfway decay point is roughly equal to a -/// typical block size of 1024 samples (23 ms), which should eliminate the stair-stepping -/// for most games. -pub const DEFAULT_SMOOTH_SECONDS: f32 = 46.0 / 1_000.0; +/// This value is chosen to where the stair-stepping effect isn't noticeable for a +/// typical block size of 1024 samples. +pub const DEFAULT_SMOOTH_SECONDS: f32 = 62.0 / 1_000.0; /// The default settle ratio value for a [`SmoothingFilter`]. pub const DEFAULT_SETTLE_RATIO: f32 = 0.01; diff --git a/crates/firewheel-core/src/param/smoother.rs b/crates/firewheel-core/src/param/smoother.rs index 70515ac9..acc6b051 100644 --- a/crates/firewheel-core/src/param/smoother.rs +++ b/crates/firewheel-core/src/param/smoother.rs @@ -27,7 +27,9 @@ pub struct SmootherConfig { /// /// If less than 0.0, then 0.0 will be used. /// - /// By default this is set to 23 milliseconds. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// The threshold at which the filter is considered "settled". /// diff --git a/crates/firewheel-nodes/src/convolution.rs b/crates/firewheel-nodes/src/convolution.rs index d4ebed70..206e95d8 100644 --- a/crates/firewheel-nodes/src/convolution.rs +++ b/crates/firewheel-nodes/src/convolution.rs @@ -89,9 +89,9 @@ pub struct ConvolutionNode { /// Adjusts the time in seconds over which parameters are smoothed for `mix` /// and `wet_gain`. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, } diff --git a/crates/firewheel-nodes/src/fast_filters/bandpass.rs b/crates/firewheel-nodes/src/fast_filters/bandpass.rs index 078aab6c..c5e3898a 100644 --- a/crates/firewheel-nodes/src/fast_filters/bandpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/bandpass.rs @@ -37,9 +37,9 @@ pub struct FastBandpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/fast_filters/highpass.rs b/crates/firewheel-nodes/src/fast_filters/highpass.rs index 3feab90a..75d2daca 100644 --- a/crates/firewheel-nodes/src/fast_filters/highpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/highpass.rs @@ -34,9 +34,9 @@ pub struct FastHighpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/fast_filters/lowpass.rs b/crates/firewheel-nodes/src/fast_filters/lowpass.rs index 0a374679..82961d81 100644 --- a/crates/firewheel-nodes/src/fast_filters/lowpass.rs +++ b/crates/firewheel-nodes/src/fast_filters/lowpass.rs @@ -33,9 +33,9 @@ pub struct FastLowpassNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/freeverb/mod.rs b/crates/firewheel-nodes/src/freeverb/mod.rs index f3c48eca..bd1727ac 100644 --- a/crates/firewheel-nodes/src/freeverb/mod.rs +++ b/crates/firewheel-nodes/src/freeverb/mod.rs @@ -65,9 +65,9 @@ pub struct FreeverbNode { /// Adjusts the time in seconds over which parameters are smoothed. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/mix.rs b/crates/firewheel-nodes/src/mix.rs index 9a57bc54..fa812889 100644 --- a/crates/firewheel-nodes/src/mix.rs +++ b/crates/firewheel-nodes/src/mix.rs @@ -72,9 +72,9 @@ pub struct MixNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to diff --git a/crates/firewheel-nodes/src/spatial_basic.rs b/crates/firewheel-nodes/src/spatial_basic.rs index 56b6f253..dc644e30 100644 --- a/crates/firewheel-nodes/src/spatial_basic.rs +++ b/crates/firewheel-nodes/src/spatial_basic.rs @@ -91,9 +91,9 @@ pub struct SpatialBasicNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less than or equal /// to this value, the the gain will be clamped to `0` (silence). diff --git a/crates/firewheel-nodes/src/svf.rs b/crates/firewheel-nodes/src/svf.rs index a38c0397..ed551620 100644 --- a/crates/firewheel-nodes/src/svf.rs +++ b/crates/firewheel-nodes/src/svf.rs @@ -132,9 +132,9 @@ pub struct SvfNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// An exponent representing the rate at which DSP coefficients are diff --git a/crates/firewheel-nodes/src/volume.rs b/crates/firewheel-nodes/src/volume.rs index 60c76060..7dd6397a 100644 --- a/crates/firewheel-nodes/src/volume.rs +++ b/crates/firewheel-nodes/src/volume.rs @@ -45,9 +45,9 @@ pub struct VolumeNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to diff --git a/crates/firewheel-nodes/src/volume_pan.rs b/crates/firewheel-nodes/src/volume_pan.rs index ec26c95e..1836269e 100644 --- a/crates/firewheel-nodes/src/volume_pan.rs +++ b/crates/firewheel-nodes/src/volume_pan.rs @@ -36,9 +36,9 @@ pub struct VolumePanNode { /// The time in seconds of the internal smoothing filter. /// - /// By default this is set to `0.046` (46ms). This value is chosen to where - /// the halfway decay point is roughly equal to a typical block size of 1024 - /// samples (23 ms), which should eliminate the stair-stepping for most games. + /// By default this is set to `0.062` (62ms). This value is chosen such that + /// the stair-stepping effect isn't noticeable for a typical block size of 1024 + /// samples. pub smooth_seconds: f32, /// If the resulting gain (in raw amplitude, not decibels) is less /// than or equal to this value, then the gain will be clamped to