From 836a72dbc44d8bc52e0888b953cf83b83911d154 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 29 Nov 2022 19:47:32 +0100 Subject: [PATCH] Implement Diopser's safe mode for filter stages These are now limited to 40 by default. --- plugins/diopser/src/editor.rs | 9 +- plugins/diopser/src/editor/button.rs | 2 +- plugins/diopser/src/editor/safe_mode.rs | 86 +++++++++++++++++-- plugins/diopser/src/editor/slider.rs | 107 ++++++++++++++---------- plugins/diopser/src/params.rs | 5 ++ 5 files changed, 158 insertions(+), 51 deletions(-) diff --git a/plugins/diopser/src/editor.rs b/plugins/diopser/src/editor.rs index 1faa0318..37ca83f9 100644 --- a/plugins/diopser/src/editor.rs +++ b/plugins/diopser/src/editor.rs @@ -179,7 +179,14 @@ fn other_params(cx: &mut Context) { cx, Data::params, |params| ¶ms.filter_stages, - Data::safe_mode_clamper, + { + let safe_mode_clamper = Data::safe_mode_clamper.get(cx); + move |t| safe_mode_clamper.filter_stages_renormalize_display(t) + }, + { + let safe_mode_clamper = Data::safe_mode_clamper.get(cx); + move |t| safe_mode_clamper.filter_stages_renormalize_event(t) + }, ); }) .bottom(Pixels(10.0)); diff --git a/plugins/diopser/src/editor/button.rs b/plugins/diopser/src/editor/button.rs index 0f65de1b..76c199f7 100644 --- a/plugins/diopser/src/editor/button.rs +++ b/plugins/diopser/src/editor/button.rs @@ -77,7 +77,7 @@ impl> View for SafeModeButton { safe_mode_clamper.enable(cx); self.scrolled_lines -= 1.0; } else { - safe_mode_clamper.disable(cx); + safe_mode_clamper.disable(); self.scrolled_lines += 1.0; } } diff --git a/plugins/diopser/src/editor/safe_mode.rs b/plugins/diopser/src/editor/safe_mode.rs index 240a654e..3548b915 100644 --- a/plugins/diopser/src/editor/safe_mode.rs +++ b/plugins/diopser/src/editor/safe_mode.rs @@ -3,22 +3,42 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use nih_plug::prelude::Param; use nih_plug_vizia::vizia::prelude::EventContext; +use nih_plug_vizia::widgets::ParamEvent; -use crate::params::DiopserParams; +use crate::params::{self, DiopserParams}; /// Restricts the ranges of several parameters when enabled. This makes it more difficult to /// generate load resonances with Diopser's default settings. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct SafeModeClamper { /// Whether the safe mode toggle has been enabled. enabled: Arc, + /// The rest of the parameters struct. Used to restrict the parameter ranges when safe mode gets + /// enabled. + params: Arc, + + /// The minimum value for the filter stages parameter when safe mode is enabled, normalized as a + /// `[0, 1]` value of the original full range. + filter_stages_restricted_normalized_min: f32, + /// The maximum value for the filter stages parameter when safe mode is enabled, normalized as a + /// `[0, 1]` value of the original full range. + filter_stages_restricted_normalized_max: f32, } impl SafeModeClamper { pub fn new(params: Arc) -> Self { + let filter_stages_range = params::filter_stages_range(); + Self { enabled: params.safe_mode.clone(), + params, + + filter_stages_restricted_normalized_min: filter_stages_range + .normalize(params::FILTER_STAGES_RESTRICTED_MIN), + filter_stages_restricted_normalized_max: filter_stages_range + .normalize(params::FILTER_STAGES_RESTRICTED_MAX), } } @@ -30,19 +50,71 @@ impl SafeModeClamper { /// Enable or disable safe mode. Enabling safe mode immediately clamps the parameters to their /// new restricted ranges. pub fn toggle(&self, cx: &mut EventContext) { - // TODO: Restrict the parameter ranges when the button is enabled - self.enabled.fetch_xor(true, Ordering::Relaxed); + if !self.enabled.fetch_xor(true, Ordering::Relaxed) { + self.restrict_range(cx); + } } /// Enable safe mode. Enabling safe mode immediately clamps the parameters to their new /// restricted ranges. pub fn enable(&self, cx: &mut EventContext) { - // TODO: Restrict the parameter ranges when the button is enabled - self.enabled.store(true, Ordering::Relaxed); + if !self.enabled.swap(true, Ordering::Relaxed) { + self.restrict_range(cx); + } } /// Disable safe mode. - pub fn disable(&self, cx: &mut EventContext) { + pub fn disable(&self) { + // Disablign safe mode never needs to modify any parameters self.enabled.store(false, Ordering::Relaxed); } + + /// Depending on whether the safe mode is enabled or not this either returns `t` + /// as is, or the range gets translated to the restricted range when safe mode is enabled. This + /// is used for displaying the value. When handling events the range should be expanded again to + /// the origianl values. + pub fn filter_stages_renormalize_display(&self, t: f32) -> f32 { + if self.status() { + let renormalized = (t - self.filter_stages_restricted_normalized_min) + / (self.filter_stages_restricted_normalized_max + - self.filter_stages_restricted_normalized_min); + + // This clamping may be necessary when safe mode is enabled but the effects from + // `restrict_range()` have not been processed yet + renormalized.clamp(0.0, 1.0) + } else { + t + } + } + + /// Depending on whether the safe mode is enabled or not this either returns `t` + /// as is, or the restricted range gets translated back to the original range when safe mode is + /// enabled. + pub fn filter_stages_renormalize_event(&self, t: f32) -> f32 { + if self.status() { + // This is the opposite of `filter_stages_renormalize_display` + t * (self.filter_stages_restricted_normalized_max + - self.filter_stages_restricted_normalized_min) + + self.filter_stages_restricted_normalized_min + } else { + t + } + } + + /// CLamp the parameter values to the restricted range when enabling safe mode. This assumes + /// there's no active automation gesture for these parameters. + fn restrict_range(&self, cx: &mut EventContext) { + cx.emit(ParamEvent::BeginSetParameter(&self.params.filter_stages).upcast()); + cx.emit( + ParamEvent::SetParameter( + &self.params.filter_stages, + self.params.filter_stages.unmodulated_plain_value().clamp( + params::FILTER_STAGES_RESTRICTED_MIN, + params::FILTER_STAGES_RESTRICTED_MAX, + ), + ) + .upcast(), + ); + cx.emit(ParamEvent::EndSetParameter(&self.params.filter_stages).upcast()); + } } diff --git a/plugins/diopser/src/editor/slider.rs b/plugins/diopser/src/editor/slider.rs index 2a1f08f5..749e77cc 100644 --- a/plugins/diopser/src/editor/slider.rs +++ b/plugins/diopser/src/editor/slider.rs @@ -21,8 +21,6 @@ use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::param_base::ParamWidgetBase; use nih_plug_vizia::widgets::util::{self, ModifiersExt}; -use super::SafeModeClamper; - /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the /// normalized parameter. const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; @@ -30,10 +28,18 @@ const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; /// A simplified version of `ParamSlider` that works with Diopser's safe mode. The slider's range is /// restricted when safe mode is enabled. #[derive(Lens)] -pub struct RestrictedParamSlider> { - safe_mode: LSafeMode, +pub struct RestrictedParamSlider { param_base: ParamWidgetBase, + /// Renormalizes the parameter's normalized value to a `[0, 1]` value that is used to display + /// the parameter. This range may end up zooming in on a part of the parameter's original range + /// when safe mode is enabled. + renormalize_display: Box f32>, + /// The inverse of `renormalize_display`. This is used to map a normalized `[0, 1]` screen + /// coordinate back to a `[0, 1]` normalized parameter value. These values may be different when + /// safe mode is enabled. + renormalize_event: Box f32>, + /// Will be set to `true` when the field gets Alt+Click'ed which will replace the label with a /// text box. text_input_active: bool, @@ -68,27 +74,28 @@ pub struct GranularDragStatus { pub starting_value: f32, } -impl> RestrictedParamSlider { +impl RestrictedParamSlider { /// See the original `ParamSlider`. - pub fn new( + pub fn new( cx: &mut Context, - params: LParams, + params: L, params_to_param: FMap, - safe_mode: LSafeMode, + renormalize_display: impl Fn(f32) -> f32 + Clone + 'static, + renormalize_event: impl Fn(f32) -> f32 + 'static, ) -> Handle where - LParams: Lens + Clone, + L: Lens + Clone, Params: 'static, P: Param + 'static, FMap: Fn(&Params) -> &P + Copy + 'static, { - // We'll visualize the difference between the current value and the default value if the - // default value lies somewhere in the middle and the parameter is continuous. Otherwise - // this approach looks a bit jarring. + // See the original `ParamSlider` implementation for more details. Self { - safe_mode, param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param), + renormalize_display: Box::new(renormalize_display.clone()), + renormalize_event: Box::new(renormalize_event), + text_input_active: false, drag_active: false, granular_drag_status: None, @@ -100,8 +107,6 @@ impl> RestrictedParamSlider cx, ParamWidgetBase::build_view(params, params_to_param, move |cx, param_data| { // Can't use `.to_string()` here as that would include the modulation. - let unmodulated_normalized_value_lens = - param_data.make_lens(|param| param.unmodulated_normalized_value()); let display_value_lens = param_data.make_lens(|param| { param.normalized_value_to_string(param.unmodulated_normalized_value(), true) }); @@ -110,9 +115,14 @@ impl> RestrictedParamSlider // signed width of the bar. `start_t` is in `[0, 1]`, and `delta` is in // `[-1, 1]`. let fill_start_delta_lens = { - let param_data = param_data.clone(); - unmodulated_normalized_value_lens.map(move |current_value| { - Self::compute_fill_start_delta(param_data.param(), *current_value) + let renormalize_display = renormalize_display.clone(); + + param_data.make_lens(move |param| { + Self::compute_fill_start_delta( + renormalize_display(param.default_normalized_value()), + param.step_count(), + renormalize_display(param.unmodulated_normalized_value()), + ) }) }; @@ -120,8 +130,12 @@ impl> RestrictedParamSlider // plugins with hosts that support this), then this is the difference // between the 'true' value and the current value after modulation has been // applied. This follows the same format as `fill_start_delta_lens`. - let modulation_start_delta_lens = param_data - .make_lens(move |param| Self::compute_modulation_fill_start_delta(param)); + let modulation_start_delta_lens = param_data.make_lens(move |param| { + Self::compute_modulation_fill_start_delta( + renormalize_display(param.modulated_normalized_value()), + renormalize_display(param.unmodulated_normalized_value()), + ) + }); // Only draw the text input widget when it gets focussed. Otherwise, overlay the // label with the slider. Creating the textbox based on @@ -129,7 +143,7 @@ impl> RestrictedParamSlider // created. Binding::new( cx, - RestrictedParamSlider::::text_input_active, + RestrictedParamSlider::text_input_active, move |cx, text_input_active| { if text_input_active.get(cx) { Self::text_input_view(cx, display_value_lens.clone()); @@ -244,9 +258,11 @@ impl> RestrictedParamSlider /// style, the parameter's current value, and the parameter's step sizes. The resulting tuple /// `(start_t, delta)` corresponds to the start and the signed width of the bar. `start_t` is in /// `[0, 1]`, and `delta` is in `[-1, 1]`. - fn compute_fill_start_delta(param: &P, current_value: f32) -> (f32, f32) { - let default_value = param.default_normalized_value(); - let step_count = param.step_count(); + fn compute_fill_start_delta( + default_value: f32, + step_count: Option, + current_value: f32, + ) -> (f32, f32) { let draw_fill_from_default = step_count.is_none() && (0.45..=0.55).contains(&default_value); if draw_fill_from_default { @@ -264,25 +280,23 @@ impl> RestrictedParamSlider } /// The same as `compute_fill_start_delta`, but just showing the modulation offset. - fn compute_modulation_fill_start_delta(param: &P) -> (f32, f32) { - let modulation_start = param.unmodulated_normalized_value(); - - ( - modulation_start, - param.modulated_normalized_value() - modulation_start, - ) + fn compute_modulation_fill_start_delta( + modulation_start: f32, + current_value: f32, + ) -> (f32, f32) { + (modulation_start, current_value - modulation_start) } - /// `self.param_base.set_normalized_value()`, but resulting from a mouse drag. When using the - /// 'even' stepped slider styles from [`ParamSliderStyle`] this will remap the normalized range - /// to match up with the fill value display. This still needs to be wrapped in a parameter - /// automation gesture. + /// `self.param_base.set_normalized_value()`, but resulting from a mouse drag. This uses the + /// restricted range if safe mode is enabled. fn set_normalized_value_drag(&self, cx: &mut EventContext, normalized_value: f32) { - self.param_base.set_normalized_value(cx, normalized_value); + let restricted_normalized_value = (self.renormalize_event)(normalized_value); + self.param_base + .set_normalized_value(cx, restricted_normalized_value); } } -impl> View for RestrictedParamSlider { +impl View for RestrictedParamSlider { fn element(&self) -> Option<&'static str> { // We'll reuse the original ParamSlider's styling Some("param-slider") @@ -394,13 +408,22 @@ impl> View for RestrictedParamSlider IntRange { } } +/// The filter stages parameters minimum value in safe mode. +pub const FILTER_STAGES_RESTRICTED_MIN: i32 = 0; +/// The filter stages parameters maximum value in safe mode. +pub const FILTER_STAGES_RESTRICTED_MAX: i32 = 40; + /// The filter frequency parameter's range. Also used in the `SpectrumAnalyzer` widget. pub fn filter_frequency_range() -> FloatRange { FloatRange::Skewed {