From b2f6175d54249be6b03a45afed7d1264de4f91d6 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 11 Nov 2022 21:23:39 +0100 Subject: [PATCH] Add bypass smoothing for Diopser Using a simple equal-power crossfade. --- Cargo.lock | 1 + plugins/crisp/src/lib.rs | 8 +-- plugins/diopser/Cargo.toml | 2 + plugins/diopser/src/lib.rs | 117 +++++++++++++++++++++++++++++-------- 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 089edfa7..35ec9728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1086,6 +1086,7 @@ dependencies = [ name = "diopser" version = "0.3.0" dependencies = [ + "atomic_float", "nih_plug", "nih_plug_vizia", "open", diff --git a/plugins/crisp/src/lib.rs b/plugins/crisp/src/lib.rs index fa4ad03e..5cc5de1d 100644 --- a/plugins/crisp/src/lib.rs +++ b/plugins/crisp/src/lib.rs @@ -25,8 +25,8 @@ mod pcg; /// The number of channels we support. Hardcoded to allow for easier SIMD-ifying in the future. const NUM_CHANNELS: u32 = 2; -/// The number of channels to iterate over at a time. -const BLOCK_SIZE: usize = 64; +/// The number of samples to iterate over at a time. +const MAX_BLOCK_SIZE: usize = 64; /// These seeds being fixed makes bouncing deterministic. const INITIAL_PRNG_SEED: Pcg32iState = Pcg32iState::new(69, 420); @@ -362,8 +362,8 @@ impl Plugin for Crisp { _aux: &mut AuxiliaryBuffers, _context: &mut impl ProcessContext, ) -> ProcessStatus { - for (_, mut block) in buffer.iter_blocks(BLOCK_SIZE) { - let mut rm_outputs = [[0.0; NUM_CHANNELS as usize]; BLOCK_SIZE]; + for (_, mut block) in buffer.iter_blocks(MAX_BLOCK_SIZE) { + let mut rm_outputs = [[0.0; NUM_CHANNELS as usize]; MAX_BLOCK_SIZE]; // Reduce per-sample branching a bit by iterating over smaller blocks and only then // deciding what to do with the output. This version branches only once per sample (in diff --git a/plugins/diopser/Cargo.toml b/plugins/diopser/Cargo.toml index cbd2c1f5..4b41f05b 100644 --- a/plugins/diopser/Cargo.toml +++ b/plugins/diopser/Cargo.toml @@ -19,6 +19,8 @@ simd = ["nih_plug/simd"] nih_plug = { path = "../../", features = ["assert_process_allocs"] } nih_plug_vizia = { path = "../../nih_plug_vizia" } +atomic_float = "0.1" + # For the GUI realfft = "3.0" open = "3.0" diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 494e027c..77870e30 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -19,6 +19,7 @@ #[cfg(not(feature = "simd"))] compile_error!("Compiling without SIMD support is currently not supported"); +use atomic_float::AtomicF32; use nih_plug::prelude::*; use nih_plug_vizia::ViziaState; use std::simd::f32x2; @@ -40,6 +41,9 @@ const MIN_AUTOMATION_STEP_SIZE: u32 = 1; /// expensive, so updating them in larger steps can be useful. const MAX_AUTOMATION_STEP_SIZE: u32 = 512; +/// The maximum number of samples to iterate over at a time. +const MAX_BLOCK_SIZE: usize = 64; + // All features from the original Diopser have been implemented (and the spread control has been // improved). Other features I want to implement are: // - Briefly muting the output when changing the number of filters to get rid of the clicks @@ -47,13 +51,18 @@ const MAX_AUTOMATION_STEP_SIZE: u32 = 512; pub struct Diopser { params: Arc, - /// Needed for computing the filter coefficients. - sample_rate: f32, + /// Needed for computing the filter coefficients. Also used to update `bypass_smoother`, hence + /// why this needs to be an `Arc`. + sample_rate: Arc, /// All of the all-pass filters, with vectorized coefficients so they can be calculated for /// multiple channels at once. [`DiopserParams::num_stages`] controls how many filters are /// actually active. filters: [filter::Biquad; MAX_NUM_FILTERS], + /// When the bypass parameter is toggled, this smoother fades between 0.0 and 1.0. This lets us + /// crossfade the dry and the wet signal to avoid clicks. The smoothing target is set in a + /// callback handler on the bypass parameter. + bypass_smoother: Arc>, /// If this is set at the start of the processing cycle, then the filter coefficients should be /// updated. For the regular filter parameters we can look at the smoothers, but this is needed @@ -125,18 +134,25 @@ struct DiopserParams { impl Default for Diopser { fn default() -> Self { + let sample_rate = Arc::new(AtomicF32::new(1.0)); let should_update_filters = Arc::new(AtomicBool::new(false)); + let bypass_smoother = Arc::new(Smoother::new(SmoothingStyle::Linear(10.0))); // We only do stereo right now so this is simple let (spectrum_input, spectrum_output) = SpectrumInput::new(Self::DEFAULT_OUTPUT_CHANNELS as usize); Self { - params: Arc::new(DiopserParams::new(should_update_filters.clone())), + params: Arc::new(DiopserParams::new( + sample_rate.clone(), + should_update_filters.clone(), + bypass_smoother.clone(), + )), - sample_rate: 1.0, + sample_rate, filters: [filter::Biquad::default(); MAX_NUM_FILTERS], + bypass_smoother, should_update_filters, next_filter_smoothing_in: 1, @@ -148,12 +164,23 @@ impl Default for Diopser { } impl DiopserParams { - fn new(should_update_filters: Arc) -> Self { + fn new( + sample_rate: Arc, + should_update_filters: Arc, + bypass_smoother: Arc>, + ) -> Self { Self { editor_state: editor::default_state(), safe_mode: Arc::new(AtomicBool::new(true)), - bypass: BoolParam::new("Bypass", false).make_bypass(), + bypass: BoolParam::new("Bypass", false) + .with_callback(Arc::new(move |value| { + bypass_smoother.set_target( + sample_rate.load(Ordering::Relaxed), + if value { 1.0 } else { 0.0 }, + ); + })) + .make_bypass(), filter_stages: IntParam::new( "Filter Stages", @@ -284,7 +311,8 @@ impl Plugin for Diopser { buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { - self.sample_rate = buffer_config.sample_rate; + self.sample_rate + .store(buffer_config.sample_rate, Ordering::Relaxed); true } @@ -292,6 +320,8 @@ impl Plugin for Diopser { fn reset(&mut self) { // Initialize and/or reset the filters on the next process call self.should_update_filters.store(true, Ordering::Release); + self.bypass_smoother + .reset(if self.params.bypass.value() { 1.0 } else { 0.0 }); } fn process( @@ -306,25 +336,61 @@ impl Plugin for Diopser { let smoothing_interval = unnormalize_automation_precision(self.params.automation_precision.value()); - // TODO: At some point when I have too much time, a crossfade for the bypass. And maybe also - // for the filter stages. - if !self.params.bypass.value() { - for mut channel_samples in buffer.iter_samples() { - self.maybe_update_filters(smoothing_interval); - - // We can compute the filters for both channels at once. The SIMD version thus now - // only supports steroo audio. - let mut samples = unsafe { channel_samples.to_simd_unchecked() }; - - for filter in self - .filters - .iter_mut() - .take(self.params.filter_stages.value() as usize) + // The bypass parameter controls a smoother so we can crossfade between the dry and the wet + // signals as needed + if !self.params.bypass.value() || self.bypass_smoother.is_smoothing() { + // We'll iterate in blocks to make the blending relatively cheap without having to + // duplicate code or add a bunch of per-sample conditionals + for (_, mut block) in buffer.iter_blocks(MAX_BLOCK_SIZE) { + // We'll blend this with the dry signal as needed + let mut dry = [f32x2::default(); MAX_BLOCK_SIZE]; + let mut wet = [f32x2::default(); MAX_BLOCK_SIZE]; + for (input_samples, (dry_samples, wet_samples)) in block + .iter_samples() + .zip(std::iter::zip(dry.iter_mut(), wet.iter_mut())) { - samples = filter.process(samples); + self.maybe_update_filters(smoothing_interval); + + // We can compute the filters for both channels at once. The SIMD version thus now + // only supports steroo audio. + *dry_samples = unsafe { input_samples.to_simd_unchecked() }; + *wet_samples = *dry_samples; + + for filter in self + .filters + .iter_mut() + .take(self.params.filter_stages.value() as usize) + { + *wet_samples = filter.process(*wet_samples); + } } - unsafe { channel_samples.from_simd_unchecked(samples) }; + // If the bypass smoother is activated then the bypass switch has just been flipped to + // either the on or the off position + if self.bypass_smoother.is_smoothing() { + for (mut channel_samples, (dry_samples, wet_samples)) in block + .iter_samples() + .zip(std::iter::zip(dry.iter_mut(), wet.iter_mut())) + { + // We'll do an equal-power fade + let dry_t_squared = self.bypass_smoother.next(); + let dry_t = dry_t_squared.sqrt(); + let wet_t = (1.0 - dry_t_squared).sqrt(); + + let dry_weighted = *dry_samples * f32x2::splat(dry_t); + let wet_weighted = *wet_samples * f32x2::splat(wet_t); + + unsafe { channel_samples.from_simd_unchecked(dry_weighted + wet_weighted) }; + } + } else if self.params.bypass.value() { + // If the bypass is enabled and we're no longer smoothing then the output should + // just be the origianl dry signal + } else { + // Otherwise the signal is 100% wet + for (mut channel_samples, wet_samples) in block.iter_samples().zip(wet.iter()) { + unsafe { channel_samples.from_simd_unchecked(*wet_samples) }; + } + } } } @@ -368,6 +434,7 @@ impl Diopser { return; } + let sample_rate = self.sample_rate.load(Ordering::Relaxed); let frequency = self .params .filter_frequency @@ -395,7 +462,7 @@ impl Diopser { // TODO: This wrecks the DSP load at high smoothing accuracy, perhaps also use SIMD here const MIN_FREQUENCY: f32 = 5.0; - let max_frequency = self.sample_rate / 2.05; + let max_frequency = sample_rate / 2.05; for filter_idx in 0..self.params.filter_stages.value() as usize { // The index of the filter normalized to range [-1, 1] let filter_proportion = @@ -410,7 +477,7 @@ impl Diopser { .clamp(MIN_FREQUENCY, max_frequency); self.filters[filter_idx].coefficients = - filter::BiquadCoefficients::allpass(self.sample_rate, filter_frequency, resonance); + filter::BiquadCoefficients::allpass(sample_rate, filter_frequency, resonance); if reset_filters { self.filters[filter_idx].reset(); }