Add the filter spread parameters to Diopser
This includes a much better octave-based version instead of the old 'exponential' version.
This commit is contained in:
parent
5d9f268d4c
commit
b84022caed
|
@ -17,10 +17,11 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate nih_plug;
|
extern crate nih_plug;
|
||||||
|
|
||||||
use nih_plug::{formatters, BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle};
|
|
||||||
use nih_plug::{
|
use nih_plug::{
|
||||||
Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, Vst3Plugin,
|
formatters, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, Vst3Plugin,
|
||||||
};
|
};
|
||||||
|
use nih_plug::{BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle};
|
||||||
|
use nih_plug::{Enum, EnumParam};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -36,10 +37,8 @@ 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;
|
||||||
|
|
||||||
// An incomplete list of unported features includes:
|
// All features from the original Diopser have been implemented (and the spread control has been
|
||||||
// - Filter spread
|
// improved). Other features I want to implement are:
|
||||||
//
|
|
||||||
// After that the 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
|
||||||
// - A GUI
|
// - A GUI
|
||||||
struct Diopser {
|
struct Diopser {
|
||||||
|
@ -71,13 +70,24 @@ struct DiopserParams {
|
||||||
#[id = "stages"]
|
#[id = "stages"]
|
||||||
filter_stages: IntParam,
|
filter_stages: IntParam,
|
||||||
|
|
||||||
/// The filter's cutoff frequqency. When this is applied, the filters are spread around this
|
/// The filter's center frequqency. When this is applied, the filters are spread around this
|
||||||
/// frequency.
|
/// frequency.
|
||||||
#[id = "cutoff"]
|
#[id = "cutoff"]
|
||||||
filter_frequency: FloatParam,
|
filter_frequency: FloatParam,
|
||||||
/// The Q parameter for the filters.
|
/// The Q parameter for the filters.
|
||||||
#[id = "res"]
|
#[id = "res"]
|
||||||
filter_resonance: FloatParam,
|
filter_resonance: FloatParam,
|
||||||
|
/// Controls a frequency spread between the filter stages in octaves. When this value is 0, the
|
||||||
|
/// same coefficients are used for every filter. Otherwise, the earliest stage's frequency will
|
||||||
|
/// be offset by `-filter_spread_octave_amount`, while the latest stage will be offset by
|
||||||
|
/// `filter_spread_octave_amount`. If the filter spread style is set to linear then the negative
|
||||||
|
/// range will cover the same frequency range as the positive range.
|
||||||
|
#[id = "spread"]
|
||||||
|
filter_spread_octaves: FloatParam,
|
||||||
|
/// How the spread range should be distributed. The octaves mode will sound more musical while
|
||||||
|
/// the linear mode can be useful for sound design purposes.
|
||||||
|
#[id = "spstyl"]
|
||||||
|
filter_spread_style: EnumParam<SpreadStyle>,
|
||||||
|
|
||||||
/// The precision of the automation, determines the step size. This is presented to the userq as
|
/// The precision of the automation, determines the step size. This is presented to the userq as
|
||||||
/// a percentage, and it's stored here as `[0, 1]` float because smaller step sizes are more
|
/// a percentage, and it's stored here as `[0, 1]` float because smaller step sizes are more
|
||||||
|
@ -108,9 +118,6 @@ impl Default for Diopser {
|
||||||
|
|
||||||
impl DiopserParams {
|
impl DiopserParams {
|
||||||
pub fn new(should_update_filters: Arc<AtomicBool>) -> Self {
|
pub fn new(should_update_filters: Arc<AtomicBool>) -> Self {
|
||||||
let trigger_filter_update =
|
|
||||||
Arc::new(move |_| should_update_filters.store(true, Ordering::Release));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
filter_stages: IntParam::new(
|
filter_stages: IntParam::new(
|
||||||
"Filter Stages",
|
"Filter Stages",
|
||||||
|
@ -120,7 +127,10 @@ impl DiopserParams {
|
||||||
max: MAX_NUM_FILTERS as i32,
|
max: MAX_NUM_FILTERS as i32,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_callback(trigger_filter_update),
|
.with_callback({
|
||||||
|
let should_update_filters = should_update_filters.clone();
|
||||||
|
Arc::new(move |_| should_update_filters.store(true, Ordering::Release))
|
||||||
|
}),
|
||||||
|
|
||||||
// Smoothed parameters don't need the callback as we can just look at whether the
|
// Smoothed parameters don't need the callback as we can just look at whether the
|
||||||
// smoother is still smoothing
|
// smoother is still smoothing
|
||||||
|
@ -150,6 +160,26 @@ impl DiopserParams {
|
||||||
)
|
)
|
||||||
.with_smoother(SmoothingStyle::Logarithmic(100.0))
|
.with_smoother(SmoothingStyle::Logarithmic(100.0))
|
||||||
.with_value_to_string(formatters::f32_rounded(2)),
|
.with_value_to_string(formatters::f32_rounded(2)),
|
||||||
|
filter_spread_octaves: FloatParam::new(
|
||||||
|
"Filter Spread Octaves",
|
||||||
|
0.0,
|
||||||
|
Range::SymmetricalSkewed {
|
||||||
|
min: -5.0,
|
||||||
|
max: 5.0,
|
||||||
|
factor: Range::skew_factor(-1.0),
|
||||||
|
center: 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_step_size(0.01)
|
||||||
|
.with_smoother(SmoothingStyle::Linear(100.0)),
|
||||||
|
filter_spread_style: EnumParam::new("Filter Spread Style", SpreadStyle::Octaves)
|
||||||
|
.with_callback(Arc::new(move |_| {
|
||||||
|
should_update_filters.store(true, Ordering::Release)
|
||||||
|
})),
|
||||||
|
|
||||||
|
very_important: BoolParam::new("Don't touch this", true).with_value_to_string(
|
||||||
|
Arc::new(|value| String::from(if value { "please don't" } else { "stop it" })),
|
||||||
|
),
|
||||||
|
|
||||||
automation_precision: FloatParam::new(
|
automation_precision: FloatParam::new(
|
||||||
"Automation precision",
|
"Automation precision",
|
||||||
|
@ -158,14 +188,16 @@ impl DiopserParams {
|
||||||
)
|
)
|
||||||
.with_unit("%")
|
.with_unit("%")
|
||||||
.with_value_to_string(Arc::new(|value| format!("{:.0}", value * 100.0))),
|
.with_value_to_string(Arc::new(|value| format!("{:.0}", value * 100.0))),
|
||||||
|
|
||||||
very_important: BoolParam::new("Don't touch this", true).with_value_to_string(
|
|
||||||
Arc::new(|value| String::from(if value { "please don't" } else { "stop it" })),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Enum, Debug)]
|
||||||
|
enum SpreadStyle {
|
||||||
|
Octaves,
|
||||||
|
Linear,
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for Diopser {
|
impl Plugin for Diopser {
|
||||||
const NAME: &'static str = "Diopser (WIP port)";
|
const NAME: &'static str = "Diopser (WIP port)";
|
||||||
const VENDOR: &'static str = "Robbert van der Helm";
|
const VENDOR: &'static str = "Robbert van der Helm";
|
||||||
|
@ -239,7 +271,8 @@ impl Diopser {
|
||||||
.compare_exchange(true, false, Ordering::Acquire, Ordering::Relaxed)
|
.compare_exchange(true, false, Ordering::Acquire, Ordering::Relaxed)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|| ((self.params.filter_frequency.smoothed.is_smoothing()
|
|| ((self.params.filter_frequency.smoothed.is_smoothing()
|
||||||
|| self.params.filter_resonance.smoothed.is_smoothing())
|
|| self.params.filter_resonance.smoothed.is_smoothing()
|
||||||
|
|| self.params.filter_spread_octaves.smoothed.is_smoothing())
|
||||||
&& self.next_filter_smoothing_in <= 1);
|
&& self.next_filter_smoothing_in <= 1);
|
||||||
if should_update_filters {
|
if should_update_filters {
|
||||||
self.update_filters(smoothing_interval);
|
self.update_filters(smoothing_interval);
|
||||||
|
@ -252,23 +285,46 @@ impl Diopser {
|
||||||
/// Recompute the filter coefficients based on the smoothed paraetersm. We can skip forwardq in
|
/// Recompute the filter coefficients based on the smoothed paraetersm. We can skip forwardq in
|
||||||
/// larger steps to reduce the DSP load.
|
/// larger steps to reduce the DSP load.
|
||||||
fn update_filters(&mut self, smoothing_interval: u32) {
|
fn update_filters(&mut self, smoothing_interval: u32) {
|
||||||
let coefficients = filter::BiquadCoefficients::allpass(
|
if self.filters.is_empty() {
|
||||||
self.sample_rate,
|
return;
|
||||||
self.params
|
}
|
||||||
.filter_frequency
|
|
||||||
.smoothed
|
let frequency = self
|
||||||
.next_step(smoothing_interval),
|
.params
|
||||||
self.params
|
.filter_frequency
|
||||||
.filter_resonance
|
.smoothed
|
||||||
.smoothed
|
.next_step(smoothing_interval);
|
||||||
.next_step(smoothing_interval),
|
let resonance = self
|
||||||
);
|
.params
|
||||||
for channel in self.filters.iter_mut() {
|
.filter_resonance
|
||||||
for filter in channel
|
.smoothed
|
||||||
.iter_mut()
|
.next_step(smoothing_interval);
|
||||||
.take(self.params.filter_stages.value as usize)
|
let spread_octaves = self
|
||||||
{
|
.params
|
||||||
filter.coefficients = coefficients;
|
.filter_spread_octaves
|
||||||
|
.smoothed
|
||||||
|
.next_step(smoothing_interval);
|
||||||
|
let spread_style = self.params.filter_spread_style.value();
|
||||||
|
|
||||||
|
const MIN_FREQUENCY: f32 = 5.0;
|
||||||
|
let max_frequency = self.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 =
|
||||||
|
(filter_idx as f32 / self.params.filter_stages.value as f32) * 2.0 - 1.0;
|
||||||
|
|
||||||
|
// The spread parameter adds an offset to the frequency depending on the number of the
|
||||||
|
// filter
|
||||||
|
let filter_frequency = match spread_style {
|
||||||
|
SpreadStyle::Octaves => frequency * 2.0f32.powf(spread_octaves * filter_proportion),
|
||||||
|
SpreadStyle::Linear => frequency * 2.0f32.powf(spread_octaves) * filter_proportion,
|
||||||
|
}
|
||||||
|
.clamp(MIN_FREQUENCY, max_frequency);
|
||||||
|
|
||||||
|
let coefficients =
|
||||||
|
filter::BiquadCoefficients::allpass(self.sample_rate, filter_frequency, resonance);
|
||||||
|
for channel in self.filters.iter_mut() {
|
||||||
|
channel[filter_idx].coefficients = coefficients;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue