1
0
Fork 0

Implement Diopser's safe mode for filter stages

These are now limited to 40 by default.
This commit is contained in:
Robbert van der Helm 2022-11-29 19:47:32 +01:00
parent 32f123b47d
commit 836a72dbc4
5 changed files with 158 additions and 51 deletions

View file

@ -179,7 +179,14 @@ fn other_params(cx: &mut Context) {
cx,
Data::params,
|params| &params.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));

View file

@ -77,7 +77,7 @@ impl<L: Lens<Target = SafeModeClamper>> View for SafeModeButton<L> {
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;
}
}

View file

@ -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<AtomicBool>,
/// The rest of the parameters struct. Used to restrict the parameter ranges when safe mode gets
/// enabled.
params: Arc<DiopserParams>,
/// 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<DiopserParams>) -> 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());
}
}

View file

@ -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<LSafeMode: Lens<Target = SafeModeClamper>> {
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<dyn Fn(f32) -> 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<dyn Fn(f32) -> 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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode> {
impl RestrictedParamSlider {
/// See the original `ParamSlider`.
pub fn new<LParams, Params, P, FMap>(
pub fn new<L, Params, P, FMap>(
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<Self>
where
LParams: Lens<Target = Params> + Clone,
L: Lens<Target = Params> + 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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
// 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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
// 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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
// created.
Binding::new(
cx,
RestrictedParamSlider::<LSafeMode>::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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
/// 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<P: Param>(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<usize>,
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<LSafeMode: Lens<Target = SafeModeClamper>> RestrictedParamSlider<LSafeMode>
}
/// The same as `compute_fill_start_delta`, but just showing the modulation offset.
fn compute_modulation_fill_start_delta<P: Param>(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<LSafeMode: Lens<Target = SafeModeClamper>> View for RestrictedParamSlider<LSafeMode> {
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<LSafeMode: Lens<Target = SafeModeClamper>> View for RestrictedParamSlider<L
// consistent
let start_x =
util::remap_current_entity_x_t(cx, granular_drag_status.starting_value);
let delta_x = ((*x - granular_drag_status.starting_x_coordinate)
* GRANULAR_DRAG_MULTIPLIER)
* cx.style.dpi_factor as f32;
self.set_normalized_value_drag(
// When the range is restricted the `delta_x` should also change
// accordingly
let min_x = (self.renormalize_event)(0.0);
let max_x = (self.renormalize_event)(1.0);
let delta_x = (*x - granular_drag_status.starting_x_coordinate)
* GRANULAR_DRAG_MULTIPLIER
* cx.style.dpi_factor as f32
* (max_x - min_x);
// We don't use `set_normalized_value_drag` because these values are already
// in the correct 'remapped' domain
self.param_base.set_normalized_value(
cx,
util::remap_current_entity_x_coordinate(cx, start_x + delta_x),
util::remap_current_entity_x_coordinate(cx, start_x + delta_x)
.clamp(min_x, max_x),
);
} else {
self.granular_drag_status = None;

View file

@ -26,6 +26,11 @@ pub fn filter_stages_range() -> 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 {