From fc6fcf4cf5d41f26a6146ffc71f72e30cf8498ac Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 22 Nov 2022 18:06:59 +0100 Subject: [PATCH] Move Diopser param structs and helpers to module We'll need to expose some more ranges here so we can limit the ranges in safe mode. --- plugins/diopser/src/editor.rs | 3 +- plugins/diopser/src/editor/analyzer.rs | 3 +- plugins/diopser/src/editor/xy_pad.rs | 4 +- plugins/diopser/src/lib.rs | 203 +----------------------- plugins/diopser/src/params.rs | 204 +++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 201 deletions(-) create mode 100644 plugins/diopser/src/params.rs diff --git a/plugins/diopser/src/editor.rs b/plugins/diopser/src/editor.rs index ff15f9e8..36ca3202 100644 --- a/plugins/diopser/src/editor.rs +++ b/plugins/diopser/src/editor.rs @@ -24,8 +24,9 @@ use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use self::button::SafeModeButton; +use crate::params::DiopserParams; use crate::spectrum::SpectrumOutput; -use crate::{Diopser, DiopserParams}; +use crate::Diopser; mod analyzer; mod button; diff --git a/plugins/diopser/src/editor/analyzer.rs b/plugins/diopser/src/editor/analyzer.rs index a5db20ce..3e01825f 100644 --- a/plugins/diopser/src/editor/analyzer.rs +++ b/plugins/diopser/src/editor/analyzer.rs @@ -22,6 +22,7 @@ use nih_plug_vizia::vizia::vg; use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; +use crate::params; use crate::spectrum::SpectrumOutput; /// A very abstract spectrum analyzer. This draws the magnitude spectrum's bins as vertical lines @@ -51,7 +52,7 @@ impl SpectrumAnalyzer { spectrum: spectrum.get(cx), sample_rate: sample_rate.get(cx), - frequency_range: crate::filter_frequency_range(), + frequency_range: params::filter_frequency_range(), } .build( cx, diff --git a/plugins/diopser/src/editor/xy_pad.rs b/plugins/diopser/src/editor/xy_pad.rs index 622c7747..2d612c57 100644 --- a/plugins/diopser/src/editor/xy_pad.rs +++ b/plugins/diopser/src/editor/xy_pad.rs @@ -19,6 +19,8 @@ use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::param_base::ParamWidgetBase; use nih_plug_vizia::widgets::util::{self, ModifiersExt}; +use crate::params; + /// When shift+dragging the X-Y pad, one pixel dragged corresponds to this much change in the /// normalized parameter. const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; @@ -108,7 +110,7 @@ impl XyPad { x_param_base: ParamWidgetBase::new(cx, params.clone(), params_to_x_param), y_param_base: ParamWidgetBase::new(cx, params.clone(), params_to_y_param), - frequency_range: crate::filter_frequency_range(), + frequency_range: params::filter_frequency_range(), text_input_active: false, drag_active: false, diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 276e09da..ac464a4d 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -21,38 +21,21 @@ 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; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use crate::params::{DiopserParams, SpreadStyle}; use crate::spectrum::{SpectrumInput, SpectrumOutput}; mod editor; mod filter; +mod params; mod spectrum; -/// How many all-pass filters we can have in series at most. The filter stages parameter determines -/// how many filters are actually active. -const MAX_NUM_FILTERS: usize = 512; -/// The minimum step size for smoothing the filter parameters. -const MIN_AUTOMATION_STEP_SIZE: u32 = 1; -/// The maximum step size for smoothing the filter parameters. Updating these parameters can be -/// 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; -/// The filter frequency parameter's range. Also used in the `SpectrumAnalyzer` widget. -pub(crate) fn filter_frequency_range() -> FloatRange { - FloatRange::Skewed { - min: 5.0, // This must never reach 0 - max: 20_000.0, - factor: FloatRange::skew_factor(-2.5), - } -} - // 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 @@ -67,7 +50,7 @@ pub struct Diopser { /// 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], + filters: [filter::Biquad; params::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. @@ -90,57 +73,6 @@ pub struct Diopser { spectrum_output: Arc>, } -#[derive(Params)] -struct DiopserParams { - /// The editor state, saved together with the parameter state so the custom scaling can be - /// restored. - #[persist = "editor-state"] - editor_state: Arc, - /// If this option is enabled, then the filter stages parameter is limited to `[0, 40]`. This is - /// editor-only state, and doesn't affect host automation. - #[persist = "safe-mode"] - safe_mode: Arc, - - /// This plugin really doesn't need its own bypass parameter, but it's still useful to have a - /// dedicated one so it can be shown in the GUI. This is linked to the host's bypass if the host - /// supports it. - #[id = "bypass"] - bypass: BoolParam, - - /// The number of all-pass filters applied in series. - #[id = "stages"] - filter_stages: IntParam, - - /// The filter's center frequqency. When this is applied, the filters are spread around this - /// frequency. - #[id = "cutoff"] - filter_frequency: FloatParam, - /// The Q parameter for the filters. - #[id = "res"] - 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, - - /// 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 - /// precise so having this be an integer would result in odd situations. - #[id = "autopr"] - automation_precision: FloatParam, - - /// Very important. - #[id = "ignore"] - very_important: BoolParam, -} - impl Default for Diopser { fn default() -> Self { let sample_rate = Arc::new(AtomicF32::new(1.0)); @@ -160,7 +92,7 @@ impl Default for Diopser { sample_rate, - filters: [filter::Biquad::default(); MAX_NUM_FILTERS], + filters: [filter::Biquad::default(); params::MAX_NUM_FILTERS], bypass_smoother, should_update_filters, @@ -172,121 +104,6 @@ impl Default for Diopser { } } -impl DiopserParams { - 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) - .with_callback(Arc::new(move |value| { - bypass_smoother.set_target( - sample_rate.load(Ordering::Relaxed), - if value { 1.0 } else { 0.0 }, - ); - })) - .with_value_to_string(formatters::v2s_bool_bypass()) - .with_string_to_value(formatters::s2v_bool_bypass()) - .make_bypass(), - - filter_stages: IntParam::new( - "Filter Stages", - 0, - IntRange::Linear { - min: 0, - max: MAX_NUM_FILTERS as i32, - }, - ) - .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 - // smoother is still smoothing - filter_frequency: FloatParam::new( - "Filter Frequency", - 200.0, - // This value is also used in the spectrum analyzer to match the spectrum analyzer - // with this parameter which is bound to the X-Y pad's X-axis - filter_frequency_range(), - ) - // This needs quite a bit of smoothing to avoid artifacts - .with_smoother(SmoothingStyle::Logarithmic(100.0)) - // This includes the unit - .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(0, true)) - .with_string_to_value(formatters::s2v_f32_hz_then_khz()), - filter_resonance: FloatParam::new( - "Filter Resonance", - // The actual default neutral Q-value would be `sqrt(2) / 2`, but this value - // produces slightly less ringing. - 0.5, - FloatRange::Skewed { - min: 0.01, // This must also never reach 0 - max: 30.0, - factor: FloatRange::skew_factor(-2.5), - }, - ) - .with_smoother(SmoothingStyle::Logarithmic(100.0)) - .with_value_to_string(formatters::v2s_f32_rounded(2)), - filter_spread_octaves: FloatParam::new( - "Filter Spread", - 0.0, - FloatRange::SymmetricalSkewed { - min: -5.0, - max: 5.0, - factor: FloatRange::skew_factor(-1.0), - center: 0.0, - }, - ) - .with_unit(" octaves") - .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" }) - })) - .with_string_to_value(Arc::new(|string| { - let string = string.trim(); - if string.eq_ignore_ascii_case("please don't") { - Some(true) - } else if string.eq_ignore_ascii_case("stop it") { - Some(false) - } else { - None - } - })) - .hide_in_generic_ui(), - - automation_precision: FloatParam::new( - "Automation precision", - normalize_automation_precision(128), - FloatRange::Linear { min: 0.0, max: 1.0 }, - ) - .with_unit("%") - .with_value_to_string(formatters::v2s_f32_percentage(0)) - .with_string_to_value(formatters::s2v_f32_percentage()), - } - } -} - -#[derive(Enum, Debug, PartialEq)] -enum SpreadStyle { - #[id = "octaves"] - Octaves, - #[id = "linear"] - Linear, -} - impl Plugin for Diopser { const NAME: &'static str = "Diopser"; const VENDOR: &'static str = "Robbert van der Helm"; @@ -357,7 +174,7 @@ impl Plugin for Diopser { // necessary, and allow smoothing only every n samples using the automation precision // parameter let smoothing_interval = - unnormalize_automation_precision(self.params.automation_precision.value()); + params::unnormalize_automation_precision(self.params.automation_precision.value()); // The bypass parameter controls a smoother so we can crossfade between the dry and the wet // signals as needed @@ -508,16 +325,6 @@ impl Diopser { } } -fn normalize_automation_precision(step_size: u32) -> f32 { - (MAX_AUTOMATION_STEP_SIZE - step_size) as f32 - / (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32 -} - -fn unnormalize_automation_precision(normalized: f32) -> u32 { - MAX_AUTOMATION_STEP_SIZE - - (normalized * (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32).round() as u32 -} - impl ClapPlugin for Diopser { const CLAP_ID: &'static str = "nl.robbertvanderhelm.diopser"; const CLAP_DESCRIPTION: Option<&'static str> = Some("A totally original phase rotation plugin"); diff --git a/plugins/diopser/src/params.rs b/plugins/diopser/src/params.rs new file mode 100644 index 00000000..d15199fb --- /dev/null +++ b/plugins/diopser/src/params.rs @@ -0,0 +1,204 @@ +//! Diopser's parameter structs. +//! +//! This is moved to a module to avoid cluttering up `lib.rs` because we also need to expose the +//! ranges separately for some of the GUI abstractions to work. + +use atomic_float::AtomicF32; +use nih_plug::prelude::*; +use nih_plug_vizia::ViziaState; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// How many all-pass filters we can have in series at most. The filter stages parameter determines +/// how many filters are actually active. +pub const MAX_NUM_FILTERS: usize = 512; +/// The minimum step size for smoothing the filter parameters. +pub const MIN_AUTOMATION_STEP_SIZE: u32 = 1; +/// The maximum step size for smoothing the filter parameters. Updating these parameters can be +/// expensive, so updating them in larger steps can be useful. +pub const MAX_AUTOMATION_STEP_SIZE: u32 = 512; + +/// The filter frequency parameter's range. Also used in the `SpectrumAnalyzer` widget. +pub fn filter_frequency_range() -> FloatRange { + FloatRange::Skewed { + min: 5.0, // This must never reach 0 + max: 20_000.0, + factor: FloatRange::skew_factor(-2.5), + } +} + +pub fn normalize_automation_precision(step_size: u32) -> f32 { + (MAX_AUTOMATION_STEP_SIZE - step_size) as f32 + / (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32 +} + +pub fn unnormalize_automation_precision(normalized: f32) -> u32 { + MAX_AUTOMATION_STEP_SIZE + - (normalized * (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32).round() as u32 +} + +#[derive(Params)] +pub struct DiopserParams { + /// The editor state, saved together with the parameter state so the custom scaling can be + /// restored. + #[persist = "editor-state"] + pub editor_state: Arc, + /// If this option is enabled, then the filter stages parameter is limited to `[0, 40]`. This is + /// editor-only state, and doesn't affect host automation. + #[persist = "safe-mode"] + pub safe_mode: Arc, + + /// This plugin really doesn't need its own bypass parameter, but it's still useful to have a + /// dedicated one so it can be shown in the GUI. This is linked to the host's bypass if the host + /// supports it. + #[id = "bypass"] + pub bypass: BoolParam, + + /// The number of all-pass filters applied in series. + #[id = "stages"] + pub filter_stages: IntParam, + + /// The filter's center frequqency. When this is applied, the filters are spread around this + /// frequency. + #[id = "cutoff"] + pub filter_frequency: FloatParam, + /// The Q parameter for the filters. + #[id = "res"] + pub 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"] + pub 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"] + pub filter_spread_style: EnumParam, + + /// 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 + /// precise so having this be an integer would result in odd situations. + #[id = "autopr"] + pub automation_precision: FloatParam, + + /// Very important. + #[id = "ignore"] + pub very_important: BoolParam, +} + +#[derive(Enum, Debug, PartialEq)] +pub enum SpreadStyle { + #[id = "octaves"] + Octaves, + #[id = "linear"] + Linear, +} + +impl DiopserParams { + pub fn new( + sample_rate: Arc, + should_update_filters: Arc, + bypass_smoother: Arc>, + ) -> Self { + Self { + editor_state: crate::editor::default_state(), + safe_mode: Arc::new(AtomicBool::new(true)), + + 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 }, + ); + })) + .with_value_to_string(formatters::v2s_bool_bypass()) + .with_string_to_value(formatters::s2v_bool_bypass()) + .make_bypass(), + + filter_stages: IntParam::new( + "Filter Stages", + 0, + IntRange::Linear { + min: 0, + max: MAX_NUM_FILTERS as i32, + }, + ) + .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 + // smoother is still smoothing + filter_frequency: FloatParam::new( + "Filter Frequency", + 200.0, + // This value is also used in the spectrum analyzer to match the spectrum analyzer + // with this parameter which is bound to the X-Y pad's X-axis + filter_frequency_range(), + ) + // This needs quite a bit of smoothing to avoid artifacts + .with_smoother(SmoothingStyle::Logarithmic(100.0)) + // This includes the unit + .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(0, true)) + .with_string_to_value(formatters::s2v_f32_hz_then_khz()), + filter_resonance: FloatParam::new( + "Filter Resonance", + // The actual default neutral Q-value would be `sqrt(2) / 2`, but this value + // produces slightly less ringing. + 0.5, + FloatRange::Skewed { + min: 0.01, // This must also never reach 0 + max: 30.0, + factor: FloatRange::skew_factor(-2.5), + }, + ) + .with_smoother(SmoothingStyle::Logarithmic(100.0)) + .with_value_to_string(formatters::v2s_f32_rounded(2)), + filter_spread_octaves: FloatParam::new( + "Filter Spread", + 0.0, + FloatRange::SymmetricalSkewed { + min: -5.0, + max: 5.0, + factor: FloatRange::skew_factor(-1.0), + center: 0.0, + }, + ) + .with_unit(" octaves") + .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" }) + })) + .with_string_to_value(Arc::new(|string| { + let string = string.trim(); + if string.eq_ignore_ascii_case("please don't") { + Some(true) + } else if string.eq_ignore_ascii_case("stop it") { + Some(false) + } else { + None + } + })) + .hide_in_generic_ui(), + + automation_precision: FloatParam::new( + "Automation precision", + normalize_automation_precision(128), + FloatRange::Linear { min: 0.0, max: 1.0 }, + ) + .with_unit("%") + .with_value_to_string(formatters::v2s_f32_percentage(0)) + .with_string_to_value(formatters::s2v_f32_percentage()), + } + } +}