1
0
Fork 0

Limit Diopser frequency range in safe mode

This commit is contained in:
Robbert van der Helm 2022-11-29 20:45:06 +01:00
parent afd8830636
commit 035e345e21
5 changed files with 190 additions and 41 deletions

View file

@ -142,7 +142,10 @@ fn spectrum_analyzer(cx: &mut Context) {
VStack::new(cx, |cx| { VStack::new(cx, |cx| {
ZStack::new(cx, |cx| { ZStack::new(cx, |cx| {
analyzer::SpectrumAnalyzer::new(cx, Data::spectrum, Data::sample_rate) analyzer::SpectrumAnalyzer::new(cx, Data::spectrum, Data::sample_rate, {
let safe_mode_clamper = Data::safe_mode_clamper.get(cx);
move |t| safe_mode_clamper.filter_frequency_renormalize_display(t)
})
.width(Percentage(100.0)) .width(Percentage(100.0))
.height(Percentage(100.0)); .height(Percentage(100.0));
@ -151,6 +154,14 @@ fn spectrum_analyzer(cx: &mut Context) {
Data::params, Data::params,
|params| &params.filter_frequency, |params| &params.filter_frequency,
|params| &params.filter_resonance, |params| &params.filter_resonance,
{
let safe_mode_clamper = Data::safe_mode_clamper.get(cx);
move |t| safe_mode_clamper.filter_frequency_renormalize_display(t)
},
{
let safe_mode_clamper = Data::safe_mode_clamper.get(cx);
move |t| safe_mode_clamper.filter_frequency_renormalize_event(t)
},
) )
.width(Percentage(100.0)) .width(Percentage(100.0))
.height(Percentage(100.0)); .height(Percentage(100.0));

View file

@ -31,6 +31,11 @@ pub struct SpectrumAnalyzer {
spectrum: Arc<Mutex<SpectrumOutput>>, spectrum: Arc<Mutex<SpectrumOutput>>,
sample_rate: Arc<AtomicF32>, sample_rate: Arc<AtomicF32>,
/// A function that the x-parameter's/frequency 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.
x_renormalize_display: Box<dyn Fn(f32) -> f32>,
/// The same range as that used by the filter frequency parameter. We'll use this to make sure /// The same range as that used by the filter frequency parameter. We'll use this to make sure
/// we draw the spectrum analyzer's ticks at locations that match the frequency parameter linked /// we draw the spectrum analyzer's ticks at locations that match the frequency parameter linked
/// to the X-Y pad's X-axis. /// to the X-Y pad's X-axis.
@ -43,6 +48,7 @@ impl SpectrumAnalyzer {
cx: &mut Context, cx: &mut Context,
spectrum: LSpectrum, spectrum: LSpectrum,
sample_rate: LRate, sample_rate: LRate,
x_renormalize_display: impl Fn(f32) -> f32 + Clone + 'static,
) -> Handle<Self> ) -> Handle<Self>
where where
LSpectrum: Lens<Target = Arc<Mutex<SpectrumOutput>>>, LSpectrum: Lens<Target = Arc<Mutex<SpectrumOutput>>>,
@ -53,6 +59,7 @@ impl SpectrumAnalyzer {
sample_rate: sample_rate.get(cx), sample_rate: sample_rate.get(cx),
frequency_range: params::filter_frequency_range(), frequency_range: params::filter_frequency_range(),
x_renormalize_display: Box::new(x_renormalize_display),
} }
.build( .build(
cx, cx,
@ -86,7 +93,9 @@ impl View for SpectrumAnalyzer {
for (bin_idx, magnetude) in spectrum.iter().enumerate() { for (bin_idx, magnetude) in spectrum.iter().enumerate() {
// We'll match up the bin's x-coordinate with the filter frequency parameter // We'll match up the bin's x-coordinate with the filter frequency parameter
let frequency = (bin_idx as f32 / spectrum.len() as f32) * nyquist; let frequency = (bin_idx as f32 / spectrum.len() as f32) * nyquist;
let t = self.frequency_range.normalize(frequency); // NOTE: This takes the safe-mode switch into acocunt. When it is enabled, the range is
// zoomed in to match the X-Y pad.
let t = (self.x_renormalize_display)(self.frequency_range.normalize(frequency));
if t <= 0.0 || t >= 1.0 { if t <= 0.0 || t >= 1.0 {
continue; continue;
} }

View file

@ -25,11 +25,19 @@ pub struct SafeModeClamper {
/// The maximum value for the filter stages parameter when safe mode is enabled, normalized as a /// The maximum value for the filter stages parameter when safe mode is enabled, normalized as a
/// `[0, 1]` value of the original full range. /// `[0, 1]` value of the original full range.
filter_stages_restricted_normalized_max: f32, filter_stages_restricted_normalized_max: f32,
/// The minimum value for the filter frequency parameter when safe mode is enabled, normalized
/// as a `[0, 1]` value of the original full range.
filter_frequency_restricted_normalized_min: f32,
/// The maximum value for the filter frequency parameter when safe mode is enabled, normalized
/// as a `[0, 1]` value of the original full range.
filter_frequency_restricted_normalized_max: f32,
} }
impl SafeModeClamper { impl SafeModeClamper {
pub fn new(params: Arc<DiopserParams>) -> Self { pub fn new(params: Arc<DiopserParams>) -> Self {
let filter_stages_range = params::filter_stages_range(); let filter_stages_range = params::filter_stages_range();
let filter_frequency_range = params::filter_frequency_range();
Self { Self {
enabled: params.safe_mode.clone(), enabled: params.safe_mode.clone(),
@ -39,6 +47,11 @@ impl SafeModeClamper {
.normalize(params::FILTER_STAGES_RESTRICTED_MIN), .normalize(params::FILTER_STAGES_RESTRICTED_MIN),
filter_stages_restricted_normalized_max: filter_stages_range filter_stages_restricted_normalized_max: filter_stages_range
.normalize(params::FILTER_STAGES_RESTRICTED_MAX), .normalize(params::FILTER_STAGES_RESTRICTED_MAX),
filter_frequency_restricted_normalized_min: filter_frequency_range
.normalize(params::FILTER_FREQUENCY_RESTRICTED_MIN),
filter_frequency_restricted_normalized_max: filter_frequency_range
.normalize(params::FILTER_FREQUENCY_RESTRICTED_MAX),
} }
} }
@ -101,6 +114,35 @@ impl SafeModeClamper {
} }
} }
/// The same as
/// [`filter_stages_renormalize_display()`][Self::filter_stages_renormalize_display()], but for
/// filter freqnecy.
pub fn filter_frequency_renormalize_display(&self, t: f32) -> f32 {
if self.status() {
let renormalized = (t - self.filter_frequency_restricted_normalized_min)
/ (self.filter_frequency_restricted_normalized_max
- self.filter_frequency_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
}
}
/// The same as [`filter_stages_renormalize_event()`][Self::filter_stages_renormalize_event()],
/// but for filter freqnecy.
pub fn filter_frequency_renormalize_event(&self, t: f32) -> f32 {
if self.status() {
t * (self.filter_frequency_restricted_normalized_max
- self.filter_frequency_restricted_normalized_min)
+ self.filter_frequency_restricted_normalized_min
} else {
t
}
}
/// CLamp the parameter values to the restricted range when enabling safe mode. This assumes /// CLamp the parameter values to the restricted range when enabling safe mode. This assumes
/// there's no active automation gesture for these parameters. /// there's no active automation gesture for these parameters.
fn restrict_range(&self, cx: &mut EventContext) { fn restrict_range(&self, cx: &mut EventContext) {
@ -116,5 +158,21 @@ impl SafeModeClamper {
.upcast(), .upcast(),
); );
cx.emit(ParamEvent::EndSetParameter(&self.params.filter_stages).upcast()); cx.emit(ParamEvent::EndSetParameter(&self.params.filter_stages).upcast());
cx.emit(ParamEvent::BeginSetParameter(&self.params.filter_frequency).upcast());
cx.emit(
ParamEvent::SetParameter(
&self.params.filter_frequency,
self.params
.filter_frequency
.unmodulated_plain_value()
.clamp(
params::FILTER_FREQUENCY_RESTRICTED_MIN,
params::FILTER_FREQUENCY_RESTRICTED_MAX,
),
)
.upcast(),
);
cx.emit(ParamEvent::EndSetParameter(&self.params.filter_frequency).upcast());
} }
} }

View file

@ -32,6 +32,9 @@ const HANDLE_WIDTH_PX: f32 = 20.0;
/// An X-Y pad that controlers two parameters at the same time by binding them to one of the two /// An X-Y pad that controlers two parameters at the same time by binding them to one of the two
/// axes. This specific implementation has a tooltip for the X-axis parmaeter and allows /// axes. This specific implementation has a tooltip for the X-axis parmaeter and allows
/// Alt+clicking to enter a specific value. /// Alt+clicking to enter a specific value.
///
/// The x-parameter's range is restricted when safe mode is enabled. See `RestrictedParamSlider` for
/// more details.
#[derive(Lens)] #[derive(Lens)]
pub struct XyPad { pub struct XyPad {
x_param_base: ParamWidgetBase, x_param_base: ParamWidgetBase,
@ -41,6 +44,14 @@ pub struct XyPad {
/// frequencies when holding Alt while dragging. /// frequencies when holding Alt while dragging.
/// NOTE: This is hardcoded to work with the filter frequency parameter. /// NOTE: This is hardcoded to work with the filter frequency parameter.
frequency_range: FloatRange, frequency_range: FloatRange,
/// Renormalizes the x-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.
x_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.
x_renormalize_event: Box<dyn Fn(f32) -> f32>,
/// Will be set to `true` when the X-Y pad gets Alt+Click'ed. This will replace the handle with /// Will be set to `true` when the X-Y pad gets Alt+Click'ed. This will replace the handle with
/// a text input box. /// a text input box.
@ -92,11 +103,16 @@ impl XyPad {
/// Creates a new [`XyPad`] for the given parameter. See /// Creates a new [`XyPad`] for the given parameter. See
/// [`ParamSlider`][nih_plug_vizia::widgets::ParamSlider] for more information on this /// [`ParamSlider`][nih_plug_vizia::widgets::ParamSlider] for more information on this
/// function's arguments. /// function's arguments.
///
/// The x-parameter's range is restricted when safe mode is enabled. See `RestrictedParamSlider`
/// for more details.
pub fn new<L, Params, P1, P2, FMap1, FMap2>( pub fn new<L, Params, P1, P2, FMap1, FMap2>(
cx: &mut Context, cx: &mut Context,
params: L, params: L,
params_to_x_param: FMap1, params_to_x_param: FMap1,
params_to_y_param: FMap2, params_to_y_param: FMap2,
x_renormalize_display: impl Fn(f32) -> f32 + Clone + 'static,
x_renormalize_event: impl Fn(f32) -> f32 + 'static,
) -> Handle<Self> ) -> Handle<Self>
where where
L: Lens<Target = Params> + Clone, L: Lens<Target = Params> + Clone,
@ -111,6 +127,8 @@ impl XyPad {
y_param_base: ParamWidgetBase::new(cx, params.clone(), params_to_y_param), y_param_base: ParamWidgetBase::new(cx, params.clone(), params_to_y_param),
frequency_range: params::filter_frequency_range(), frequency_range: params::filter_frequency_range(),
x_renormalize_display: Box::new(x_renormalize_display.clone()),
x_renormalize_event: Box::new(x_renormalize_event),
text_input_active: false, text_input_active: false,
drag_active: false, drag_active: false,
@ -133,9 +151,16 @@ impl XyPad {
params, params,
params_to_y_param, params_to_y_param,
move |cx, y_param_data| { move |cx, y_param_data| {
let x_position_lens = x_param_data.make_lens(|param| { // The x-parameter's range is clamped when safe mode is enabled
Percentage(param.unmodulated_normalized_value() * 100.0) let x_position_lens = {
}); let x_renormalize_display = x_renormalize_display.clone();
x_param_data.make_lens(move |param| {
Percentage(
x_renormalize_display(param.unmodulated_normalized_value())
* 100.0,
)
})
};
let y_position_lens = y_param_data.make_lens(|param| { let y_position_lens = y_param_data.make_lens(|param| {
// NOTE: The y-axis increments downards, and we want high values at // NOTE: The y-axis increments downards, and we want high values at
// the top and low values at the bottom // the top and low values at the bottom
@ -144,8 +169,11 @@ impl XyPad {
// Another handle is drawn below the regular handle to show the // Another handle is drawn below the regular handle to show the
// modualted value // modualted value
let modulated_x_position_lens = x_param_data.make_lens(|param| { let modulated_x_position_lens = x_param_data.make_lens(move |param| {
Percentage(param.modulated_normalized_value() * 100.0) Percentage(
x_renormalize_display(param.modulated_normalized_value())
* 100.0,
)
}); });
let modulated_y_position_lens = y_param_data.make_lens(|param| { let modulated_y_position_lens = y_param_data.make_lens(|param| {
Percentage((1.0 - param.modulated_normalized_value()) * 100.0) Percentage((1.0 - param.modulated_normalized_value()) * 100.0)
@ -311,8 +339,10 @@ impl XyPad {
snap_to_whole_notes: bool, snap_to_whole_notes: bool,
) { ) {
// When snapping to whole notes, we'll transform the normalized value back to unnormalized // When snapping to whole notes, we'll transform the normalized value back to unnormalized
// (this is hardcoded for the filter frequency parameter) // (this is hardcoded for the filter frequency parameter). These coordinate mappings also
let mut x_value = util::remap_current_entity_x_coordinate(cx, x_pos); // need to respect the restricted ranges from the safe mode button.
let mut x_value =
(self.x_renormalize_event)(util::remap_current_entity_x_coordinate(cx, x_pos));
if snap_to_whole_notes { if snap_to_whole_notes {
let x_freq = self.frequency_range.unnormalize(x_value); let x_freq = self.frequency_range.unnormalize(x_value);
@ -523,14 +553,16 @@ impl View for XyPad {
}); });
// These positions should be compensated for the DPI scale so it remains // These positions should be compensated for the DPI scale so it remains
// consistent // consistent. When the range is restricted the `delta_x` should also change
// accordingly.
let start_x = util::remap_current_entity_x_t( let start_x = util::remap_current_entity_x_t(
cx, cx,
granular_drag_status.x_starting_value, (self.x_renormalize_display)(granular_drag_status.x_starting_value),
); );
let delta_x = ((*x - granular_drag_status.starting_x_coordinate) let delta_x = (*x - granular_drag_status.starting_x_coordinate)
* GRANULAR_DRAG_MULTIPLIER) * GRANULAR_DRAG_MULTIPLIER
* dpi_scale; * dpi_scale;
let start_y = util::remap_current_entity_y_t( let start_y = util::remap_current_entity_y_t(
cx, cx,
// NOTE: Just like above, the corodinates go from top to bottom // NOTE: Just like above, the corodinates go from top to bottom
@ -576,38 +608,72 @@ impl View for XyPad {
*remaining_scroll_x += scroll_x; *remaining_scroll_x += scroll_x;
*remaining_scroll_y += scroll_y; *remaining_scroll_y += scroll_y;
for (param_base, scrolled_lines) in [ // This is a pretty crude way to avoid scrolling outside of the safe mode range
(&self.x_param_base, remaining_scroll_x), let clamp_x_value =
(&self.y_param_base, remaining_scroll_y), |value| (self.x_renormalize_event)((self.x_renormalize_display)(value));
] {
if scrolled_lines.abs() >= 1.0 { if remaining_scroll_x.abs() >= 1.0 {
let use_finer_steps = cx.modifiers.shift(); let use_finer_steps = cx.modifiers.shift();
// Scrolling while dragging needs to be taken into account here // Scrolling while dragging needs to be taken into account here
if !self.drag_active { if !self.drag_active {
param_base.begin_set_parameter(cx); self.x_param_base.begin_set_parameter(cx);
} }
let mut current_value = param_base.unmodulated_normalized_value(); let mut current_value = self.x_param_base.unmodulated_normalized_value();
while *scrolled_lines >= 1.0 { while *remaining_scroll_x >= 1.0 {
current_value = current_value = self
param_base.next_normalized_step(current_value, use_finer_steps); .x_param_base
param_base.set_normalized_value(cx, current_value); .next_normalized_step(current_value, use_finer_steps);
*scrolled_lines -= 1.0; self.x_param_base
.set_normalized_value(cx, clamp_x_value(current_value));
*remaining_scroll_x -= 1.0;
} }
while *scrolled_lines <= -1.0 { while *remaining_scroll_x <= -1.0 {
current_value = current_value = self
param_base.previous_normalized_step(current_value, use_finer_steps); .x_param_base
param_base.set_normalized_value(cx, current_value); .previous_normalized_step(current_value, use_finer_steps);
*scrolled_lines += 1.0; self.x_param_base
.set_normalized_value(cx, clamp_x_value(current_value));
*remaining_scroll_x += 1.0;
} }
if !self.drag_active { if !self.drag_active {
param_base.end_set_parameter(cx); self.x_param_base.end_set_parameter(cx);
} }
} }
if remaining_scroll_y.abs() >= 1.0 {
let use_finer_steps = cx.modifiers.shift();
// Scrolling while dragging needs to be taken into account here
if !self.drag_active {
self.y_param_base.begin_set_parameter(cx);
}
let mut current_value = self.y_param_base.unmodulated_normalized_value();
while *remaining_scroll_y >= 1.0 {
current_value = self
.y_param_base
.next_normalized_step(current_value, use_finer_steps);
self.y_param_base.set_normalized_value(cx, current_value);
*remaining_scroll_y -= 1.0;
}
while *remaining_scroll_y <= -1.0 {
current_value = self
.y_param_base
.previous_normalized_step(current_value, use_finer_steps);
self.y_param_base.set_normalized_value(cx, current_value);
*remaining_scroll_y += 1.0;
}
if !self.drag_active {
self.y_param_base.end_set_parameter(cx);
}
} }
meta.consume(); meta.consume();

View file

@ -40,6 +40,11 @@ pub fn filter_frequency_range() -> FloatRange {
} }
} }
/// The filter frequency parameters minimum value in safe mode.
pub const FILTER_FREQUENCY_RESTRICTED_MIN: f32 = 20.0;
/// The filter frequency parameters maximum value in safe mode.
pub const FILTER_FREQUENCY_RESTRICTED_MAX: f32 = 22_000.0;
pub fn normalize_automation_precision(step_size: u32) -> f32 { pub fn normalize_automation_precision(step_size: u32) -> f32 {
(MAX_AUTOMATION_STEP_SIZE - step_size) as f32 (MAX_AUTOMATION_STEP_SIZE - step_size) as f32
/ (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32 / (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32