1
0
Fork 0

Add bypass smoothing for Diopser

Using a simple equal-power crossfade.
This commit is contained in:
Robbert van der Helm 2022-11-11 21:23:39 +01:00
parent ec329143ae
commit b2f6175d54
4 changed files with 99 additions and 29 deletions

1
Cargo.lock generated
View file

@ -1086,6 +1086,7 @@ dependencies = [
name = "diopser"
version = "0.3.0"
dependencies = [
"atomic_float",
"nih_plug",
"nih_plug_vizia",
"open",

View file

@ -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<Self>,
) -> 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

View file

@ -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"

View file

@ -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<DiopserParams>,
/// 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<AtomicF32>`.
sample_rate: Arc<AtomicF32>,
/// 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<f32x2>; 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<Smoother<f32>>,
/// 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<AtomicBool>) -> Self {
fn new(
sample_rate: Arc<AtomicF32>,
should_update_filters: Arc<AtomicBool>,
bypass_smoother: Arc<Smoother<f32>>,
) -> 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<Self>,
) -> 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() {
// 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()))
{
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() };
*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)
{
samples = filter.process(samples);
*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();
}