Add bypass smoothing for Diopser
Using a simple equal-power crossfade.
This commit is contained in:
parent
ec329143ae
commit
b2f6175d54
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1086,6 +1086,7 @@ dependencies = [
|
|||
name = "diopser"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"atomic_float",
|
||||
"nih_plug",
|
||||
"nih_plug_vizia",
|
||||
"open",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue