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"
|
name = "diopser"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_vizia",
|
"nih_plug_vizia",
|
||||||
"open",
|
"open",
|
||||||
|
|
|
@ -25,8 +25,8 @@ mod pcg;
|
||||||
|
|
||||||
/// The number of channels we support. Hardcoded to allow for easier SIMD-ifying in the future.
|
/// The number of channels we support. Hardcoded to allow for easier SIMD-ifying in the future.
|
||||||
const NUM_CHANNELS: u32 = 2;
|
const NUM_CHANNELS: u32 = 2;
|
||||||
/// The number of channels to iterate over at a time.
|
/// The number of samples to iterate over at a time.
|
||||||
const BLOCK_SIZE: usize = 64;
|
const MAX_BLOCK_SIZE: usize = 64;
|
||||||
|
|
||||||
/// These seeds being fixed makes bouncing deterministic.
|
/// These seeds being fixed makes bouncing deterministic.
|
||||||
const INITIAL_PRNG_SEED: Pcg32iState = Pcg32iState::new(69, 420);
|
const INITIAL_PRNG_SEED: Pcg32iState = Pcg32iState::new(69, 420);
|
||||||
|
@ -362,8 +362,8 @@ impl Plugin for Crisp {
|
||||||
_aux: &mut AuxiliaryBuffers,
|
_aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext<Self>,
|
_context: &mut impl ProcessContext<Self>,
|
||||||
) -> ProcessStatus {
|
) -> ProcessStatus {
|
||||||
for (_, mut block) in buffer.iter_blocks(BLOCK_SIZE) {
|
for (_, mut block) in buffer.iter_blocks(MAX_BLOCK_SIZE) {
|
||||||
let mut rm_outputs = [[0.0; NUM_CHANNELS as usize]; 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
|
// 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
|
// 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 = { path = "../../", features = ["assert_process_allocs"] }
|
||||||
nih_plug_vizia = { path = "../../nih_plug_vizia" }
|
nih_plug_vizia = { path = "../../nih_plug_vizia" }
|
||||||
|
|
||||||
|
atomic_float = "0.1"
|
||||||
|
|
||||||
# For the GUI
|
# For the GUI
|
||||||
realfft = "3.0"
|
realfft = "3.0"
|
||||||
open = "3.0"
|
open = "3.0"
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#[cfg(not(feature = "simd"))]
|
#[cfg(not(feature = "simd"))]
|
||||||
compile_error!("Compiling without SIMD support is currently not supported");
|
compile_error!("Compiling without SIMD support is currently not supported");
|
||||||
|
|
||||||
|
use atomic_float::AtomicF32;
|
||||||
use nih_plug::prelude::*;
|
use nih_plug::prelude::*;
|
||||||
use nih_plug_vizia::ViziaState;
|
use nih_plug_vizia::ViziaState;
|
||||||
use std::simd::f32x2;
|
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.
|
/// expensive, so updating them in larger steps can be useful.
|
||||||
const MAX_AUTOMATION_STEP_SIZE: u32 = 512;
|
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
|
// All features from the original Diopser have been implemented (and the spread control has been
|
||||||
// improved). Other features I want to implement are:
|
// improved). Other features I want to implement are:
|
||||||
// - Briefly muting the output when changing the number of filters to get rid of the clicks
|
// - 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 {
|
pub struct Diopser {
|
||||||
params: Arc<DiopserParams>,
|
params: Arc<DiopserParams>,
|
||||||
|
|
||||||
/// Needed for computing the filter coefficients.
|
/// Needed for computing the filter coefficients. Also used to update `bypass_smoother`, hence
|
||||||
sample_rate: f32,
|
/// 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
|
/// 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
|
/// multiple channels at once. [`DiopserParams::num_stages`] controls how many filters are
|
||||||
/// actually active.
|
/// actually active.
|
||||||
filters: [filter::Biquad<f32x2>; MAX_NUM_FILTERS],
|
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
|
/// 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
|
/// 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 {
|
impl Default for Diopser {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let sample_rate = Arc::new(AtomicF32::new(1.0));
|
||||||
let should_update_filters = Arc::new(AtomicBool::new(false));
|
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
|
// We only do stereo right now so this is simple
|
||||||
let (spectrum_input, spectrum_output) =
|
let (spectrum_input, spectrum_output) =
|
||||||
SpectrumInput::new(Self::DEFAULT_OUTPUT_CHANNELS as usize);
|
SpectrumInput::new(Self::DEFAULT_OUTPUT_CHANNELS as usize);
|
||||||
|
|
||||||
Self {
|
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],
|
filters: [filter::Biquad::default(); MAX_NUM_FILTERS],
|
||||||
|
bypass_smoother,
|
||||||
|
|
||||||
should_update_filters,
|
should_update_filters,
|
||||||
next_filter_smoothing_in: 1,
|
next_filter_smoothing_in: 1,
|
||||||
|
@ -148,12 +164,23 @@ impl Default for Diopser {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiopserParams {
|
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 {
|
Self {
|
||||||
editor_state: editor::default_state(),
|
editor_state: editor::default_state(),
|
||||||
safe_mode: Arc::new(AtomicBool::new(true)),
|
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: IntParam::new(
|
||||||
"Filter Stages",
|
"Filter Stages",
|
||||||
|
@ -284,7 +311,8 @@ impl Plugin for Diopser {
|
||||||
buffer_config: &BufferConfig,
|
buffer_config: &BufferConfig,
|
||||||
_context: &mut impl InitContext<Self>,
|
_context: &mut impl InitContext<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.sample_rate = buffer_config.sample_rate;
|
self.sample_rate
|
||||||
|
.store(buffer_config.sample_rate, Ordering::Relaxed);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -292,6 +320,8 @@ impl Plugin for Diopser {
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
// Initialize and/or reset the filters on the next process call
|
// Initialize and/or reset the filters on the next process call
|
||||||
self.should_update_filters.store(true, Ordering::Release);
|
self.should_update_filters.store(true, Ordering::Release);
|
||||||
|
self.bypass_smoother
|
||||||
|
.reset(if self.params.bypass.value() { 1.0 } else { 0.0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
|
@ -306,25 +336,61 @@ impl Plugin for Diopser {
|
||||||
let smoothing_interval =
|
let smoothing_interval =
|
||||||
unnormalize_automation_precision(self.params.automation_precision.value());
|
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
|
// The bypass parameter controls a smoother so we can crossfade between the dry and the wet
|
||||||
// for the filter stages.
|
// signals as needed
|
||||||
if !self.params.bypass.value() {
|
if !self.params.bypass.value() || self.bypass_smoother.is_smoothing() {
|
||||||
for mut channel_samples in buffer.iter_samples() {
|
// 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);
|
self.maybe_update_filters(smoothing_interval);
|
||||||
|
|
||||||
// We can compute the filters for both channels at once. The SIMD version thus now
|
// We can compute the filters for both channels at once. The SIMD version thus now
|
||||||
// only supports steroo audio.
|
// 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
|
for filter in self
|
||||||
.filters
|
.filters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.take(self.params.filter_stages.value() as usize)
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sample_rate = self.sample_rate.load(Ordering::Relaxed);
|
||||||
let frequency = self
|
let frequency = self
|
||||||
.params
|
.params
|
||||||
.filter_frequency
|
.filter_frequency
|
||||||
|
@ -395,7 +462,7 @@ impl Diopser {
|
||||||
|
|
||||||
// TODO: This wrecks the DSP load at high smoothing accuracy, perhaps also use SIMD here
|
// TODO: This wrecks the DSP load at high smoothing accuracy, perhaps also use SIMD here
|
||||||
const MIN_FREQUENCY: f32 = 5.0;
|
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 {
|
for filter_idx in 0..self.params.filter_stages.value() as usize {
|
||||||
// The index of the filter normalized to range [-1, 1]
|
// The index of the filter normalized to range [-1, 1]
|
||||||
let filter_proportion =
|
let filter_proportion =
|
||||||
|
@ -410,7 +477,7 @@ impl Diopser {
|
||||||
.clamp(MIN_FREQUENCY, max_frequency);
|
.clamp(MIN_FREQUENCY, max_frequency);
|
||||||
|
|
||||||
self.filters[filter_idx].coefficients =
|
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 {
|
if reset_filters {
|
||||||
self.filters[filter_idx].reset();
|
self.filters[filter_idx].reset();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue