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 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/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..70d71723 100644 --- a/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs +++ b/crates/firewheel-core/src/dsp/filter/smoothing_filter.rs @@ -5,11 +5,23 @@ 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; -pub const DEFAULT_SETTLE_EPSILON: f32 = 0.001f32; +/// 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; + +/// 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_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_RATIO: f32 = 0.9; /// The coefficients for a simple smoothing/declicking filter where: /// @@ -21,10 +33,60 @@ 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 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_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, settle_ratio: f32) -> Self { + let smooth_secs = smooth_secs.max(0.0); + let ratio = settle_ratio.clamp(MIN_SETTLE_RATIO, MAX_SETTLE_RATIO); - let b1 = (-1.0f32 / (smooth_secs * sample_rate.get() as f32)).exp(); + // 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 in frames for an impulse signal + // to decay to 1/e (about 36.8%). + // + // 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: + // + // 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)(ratio) + // = -ln(ratio) + // + // Solve for t_to_1_over_e: + // + // 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(ratio) / t_to_ratio)) + // = e ^ (ln(ratio) / t_to_ratio) + // = ratio ^ (1 / t_to_ratio) + + let t_to_ratio = (smooth_secs * sample_rate.get() as f32).max(1.0); + + let b1 = ratio.powf(t_to_ratio.recip()); let a0 = 1.0f32 - b1; Self { a0, b1 } @@ -71,11 +133,28 @@ impl SmoothingFilter { /// Settle the filter if its state is close enough to the target value. /// + /// * `target` - The target value that is being smoothed to. + /// * `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 `value_span`. + /// /// Returns `true` if this filter is settled, `false` if not. - pub fn settle(&mut self, target: 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 (self.z1 - target).abs() < (target.abs() * settle_epsilon) + settle_epsilon { + } else if value_span == 0.0 || (self.z1 - target).abs() < (value_span * settle_ratio).abs() + { self.z1 = target; true } else { @@ -87,3 +166,76 @@ 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, + DEFAULT_SMOOTH_SECONDS, + 100.0 / 1_000.0, + 500.0 / 1_000.0, + ]; + 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), + (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 ratio in TEST_RATIOS { + for (start_value, end_value) in TEST_VALUES { + let coeff = + SmoothingFilterCoeff::new(NonZeroU32::new(sr).unwrap(), secs, ratio); + let mut filter = SmoothingFilter::new(start_value); + + let mut frames: u32 = 0; + while !filter.try_settle(end_value, (start_value - end_value).abs(), ratio) + && 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/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 456fb3d4..acc6b051 100644 --- a/crates/firewheel-core/src/param/smoother.rs +++ b/crates/firewheel-core/src/param/smoother.rs @@ -5,31 +5,48 @@ use bevy_platform::prelude::Vec; use crate::{ StreamInfo, - dsp::filter::smoothing_filter::{self, SmoothingFilter, SmoothingFilterCoeff}, + dsp::filter::smoothing_filter::{ + self, MAX_SETTLE_RATIO, MIN_SETTLE_RATIO, 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)] #[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. /// - /// By default this is set to 5 milliseconds. + /// If less than 0.0, then 0.0 will be used. + /// + /// 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 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 parameter's range. + /// + /// Will be clamped to the range `[0.00075..0.9]`. /// - /// By default this is set to `0.001`. - pub settle_epsilon: f32, + /// By default this is set to `0.01`. + 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, } } } @@ -42,24 +59,46 @@ pub struct SmoothedParam { filter: SmoothingFilter, coeff: SmoothingFilterCoeff, smooth_secs: f32, - settle_epsilon: f32, + settle_ratio: 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 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. + /// * `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_ratio = config + .settle_ratio + .clamp(MIN_SETTLE_RATIO, MAX_SETTLE_RATIO); + + let coeff = SmoothingFilterCoeff::new(sample_rate, smooth_secs, settle_ratio); 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, + settle_ratio, } } @@ -78,7 +117,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 + .try_settle(self.target_value, self.span, self.settle_ratio) } /// Returns `true` if this parameter is currently smoothing this process cycle, @@ -123,20 +163,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 + .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.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.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs, self.settle_ratio); } } @@ -149,14 +190,38 @@ 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 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. + /// * `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-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 512fdf03..206e95d8 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}, @@ -88,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.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, } @@ -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..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.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.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 @@ -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..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.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.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 @@ -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..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.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.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 @@ -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..bd1727ac 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.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 @@ -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(), } } @@ -124,16 +125,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, ), @@ -145,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(); @@ -162,6 +167,7 @@ struct FreeverbProcessor { pause_declicker: Declicker, values: DeclickValues, coeff_update_mask: CoeffUpdateMask, + prev_output_was_silent: bool, } impl FreeverbProcessor { @@ -230,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(); } @@ -288,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" @@ -296,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; } } @@ -319,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 6ed73750..fa812889 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}, @@ -71,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.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 @@ -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() @@ -205,6 +208,7 @@ impl AudioNode for MixNode { ), params: *self, min_gain, + prev_input_settled: true, }) } } @@ -216,6 +220,7 @@ struct Processor { params: MixNode, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for Processor { @@ -249,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(); } @@ -281,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/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..dc644e30 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; @@ -90,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.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). @@ -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() @@ -249,6 +252,7 @@ impl AudioNode for SpatialBasicNode { self.coeff_update_factor, ), params: *self, + prev_input_settled: true, }) } } @@ -260,6 +264,7 @@ struct Processor { distance_attenuator: DistanceAttenuatorStereoDsp, params: SpatialBasicNode, + prev_input_settled: bool, } impl Processor { @@ -315,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(); } } @@ -334,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 52623dbf..ed551620 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; @@ -133,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.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 @@ -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..7dd6397a 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}, @@ -44,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.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 @@ -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() @@ -159,6 +161,7 @@ impl AudioNode for VolumeNode { ), min_gain, num_channels: config.channels.get().get() as usize, + prev_input_settled: true, }) } } @@ -168,6 +171,7 @@ struct VolumeProcessor { num_channels: usize, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for VolumeProcessor { @@ -181,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(); } } @@ -210,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 90f9360f..1836269e 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}, @@ -35,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.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 @@ -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() @@ -188,6 +191,7 @@ impl AudioNode for VolumePanNode { ), params: *self, min_gain, + prev_input_settled: true, }) } } @@ -199,6 +203,7 @@ struct Processor { params: VolumePanNode, min_gain: f32, + prev_input_settled: bool, } impl AudioNodeProcessor for Processor { @@ -228,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(); } @@ -250,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(); 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, }