Limit Diopser frequency range in safe mode
This commit is contained in:
parent
afd8830636
commit
035e345e21
|
@ -142,7 +142,10 @@ fn spectrum_analyzer(cx: &mut Context) {
|
|||
|
||||
VStack::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))
|
||||
.height(Percentage(100.0));
|
||||
|
||||
|
@ -151,6 +154,14 @@ fn spectrum_analyzer(cx: &mut Context) {
|
|||
Data::params,
|
||||
|params| ¶ms.filter_frequency,
|
||||
|params| ¶ms.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))
|
||||
.height(Percentage(100.0));
|
||||
|
|
|
@ -31,6 +31,11 @@ pub struct SpectrumAnalyzer {
|
|||
spectrum: Arc<Mutex<SpectrumOutput>>,
|
||||
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
|
||||
/// we draw the spectrum analyzer's ticks at locations that match the frequency parameter linked
|
||||
/// to the X-Y pad's X-axis.
|
||||
|
@ -43,6 +48,7 @@ impl SpectrumAnalyzer {
|
|||
cx: &mut Context,
|
||||
spectrum: LSpectrum,
|
||||
sample_rate: LRate,
|
||||
x_renormalize_display: impl Fn(f32) -> f32 + Clone + 'static,
|
||||
) -> Handle<Self>
|
||||
where
|
||||
LSpectrum: Lens<Target = Arc<Mutex<SpectrumOutput>>>,
|
||||
|
@ -53,6 +59,7 @@ impl SpectrumAnalyzer {
|
|||
sample_rate: sample_rate.get(cx),
|
||||
|
||||
frequency_range: params::filter_frequency_range(),
|
||||
x_renormalize_display: Box::new(x_renormalize_display),
|
||||
}
|
||||
.build(
|
||||
cx,
|
||||
|
@ -86,7 +93,9 @@ impl View for SpectrumAnalyzer {
|
|||
for (bin_idx, magnetude) in spectrum.iter().enumerate() {
|
||||
// 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 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 {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -25,11 +25,19 @@ pub struct SafeModeClamper {
|
|||
/// 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,
|
||||
|
||||
/// 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 {
|
||||
pub fn new(params: Arc<DiopserParams>) -> Self {
|
||||
let filter_stages_range = params::filter_stages_range();
|
||||
let filter_frequency_range = params::filter_frequency_range();
|
||||
|
||||
Self {
|
||||
enabled: params.safe_mode.clone(),
|
||||
|
@ -39,6 +47,11 @@ impl SafeModeClamper {
|
|||
.normalize(params::FILTER_STAGES_RESTRICTED_MIN),
|
||||
filter_stages_restricted_normalized_max: filter_stages_range
|
||||
.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
|
||||
/// there's no active automation gesture for these parameters.
|
||||
fn restrict_range(&self, cx: &mut EventContext) {
|
||||
|
@ -116,5 +158,21 @@ impl SafeModeClamper {
|
|||
.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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/// axes. This specific implementation has a tooltip for the X-axis parmaeter and allows
|
||||
/// 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)]
|
||||
pub struct XyPad {
|
||||
x_param_base: ParamWidgetBase,
|
||||
|
@ -41,6 +44,14 @@ pub struct XyPad {
|
|||
/// frequencies when holding Alt while dragging.
|
||||
/// NOTE: This is hardcoded to work with the filter frequency parameter.
|
||||
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
|
||||
/// a text input box.
|
||||
|
@ -92,11 +103,16 @@ impl XyPad {
|
|||
/// Creates a new [`XyPad`] for the given parameter. See
|
||||
/// [`ParamSlider`][nih_plug_vizia::widgets::ParamSlider] for more information on this
|
||||
/// 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>(
|
||||
cx: &mut Context,
|
||||
params: L,
|
||||
params_to_x_param: FMap1,
|
||||
params_to_y_param: FMap2,
|
||||
x_renormalize_display: impl Fn(f32) -> f32 + Clone + 'static,
|
||||
x_renormalize_event: impl Fn(f32) -> f32 + 'static,
|
||||
) -> Handle<Self>
|
||||
where
|
||||
L: Lens<Target = Params> + Clone,
|
||||
|
@ -111,6 +127,8 @@ impl XyPad {
|
|||
y_param_base: ParamWidgetBase::new(cx, params.clone(), params_to_y_param),
|
||||
|
||||
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,
|
||||
drag_active: false,
|
||||
|
@ -133,9 +151,16 @@ impl XyPad {
|
|||
params,
|
||||
params_to_y_param,
|
||||
move |cx, y_param_data| {
|
||||
let x_position_lens = x_param_data.make_lens(|param| {
|
||||
Percentage(param.unmodulated_normalized_value() * 100.0)
|
||||
});
|
||||
// The x-parameter's range is clamped when safe mode is enabled
|
||||
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| {
|
||||
// NOTE: The y-axis increments downards, and we want high values at
|
||||
// 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
|
||||
// modualted value
|
||||
let modulated_x_position_lens = x_param_data.make_lens(|param| {
|
||||
Percentage(param.modulated_normalized_value() * 100.0)
|
||||
let modulated_x_position_lens = x_param_data.make_lens(move |param| {
|
||||
Percentage(
|
||||
x_renormalize_display(param.modulated_normalized_value())
|
||||
* 100.0,
|
||||
)
|
||||
});
|
||||
let modulated_y_position_lens = y_param_data.make_lens(|param| {
|
||||
Percentage((1.0 - param.modulated_normalized_value()) * 100.0)
|
||||
|
@ -311,8 +339,10 @@ impl XyPad {
|
|||
snap_to_whole_notes: bool,
|
||||
) {
|
||||
// When snapping to whole notes, we'll transform the normalized value back to unnormalized
|
||||
// (this is hardcoded for the filter frequency parameter)
|
||||
let mut x_value = util::remap_current_entity_x_coordinate(cx, x_pos);
|
||||
// (this is hardcoded for the filter frequency parameter). These coordinate mappings also
|
||||
// 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 {
|
||||
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
|
||||
// consistent
|
||||
// consistent. When the range is restricted the `delta_x` should also change
|
||||
// accordingly.
|
||||
let start_x = util::remap_current_entity_x_t(
|
||||
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)
|
||||
* GRANULAR_DRAG_MULTIPLIER)
|
||||
let delta_x = (*x - granular_drag_status.starting_x_coordinate)
|
||||
* GRANULAR_DRAG_MULTIPLIER
|
||||
* dpi_scale;
|
||||
|
||||
let start_y = util::remap_current_entity_y_t(
|
||||
cx,
|
||||
// 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_y += scroll_y;
|
||||
|
||||
for (param_base, scrolled_lines) in [
|
||||
(&self.x_param_base, remaining_scroll_x),
|
||||
(&self.y_param_base, remaining_scroll_y),
|
||||
] {
|
||||
if scrolled_lines.abs() >= 1.0 {
|
||||
// This is a pretty crude way to avoid scrolling outside of the safe mode range
|
||||
let clamp_x_value =
|
||||
|value| (self.x_renormalize_event)((self.x_renormalize_display)(value));
|
||||
|
||||
if remaining_scroll_x.abs() >= 1.0 {
|
||||
let use_finer_steps = cx.modifiers.shift();
|
||||
|
||||
// Scrolling while dragging needs to be taken into account here
|
||||
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 {
|
||||
current_value =
|
||||
param_base.next_normalized_step(current_value, use_finer_steps);
|
||||
param_base.set_normalized_value(cx, current_value);
|
||||
*scrolled_lines -= 1.0;
|
||||
while *remaining_scroll_x >= 1.0 {
|
||||
current_value = self
|
||||
.x_param_base
|
||||
.next_normalized_step(current_value, use_finer_steps);
|
||||
self.x_param_base
|
||||
.set_normalized_value(cx, clamp_x_value(current_value));
|
||||
*remaining_scroll_x -= 1.0;
|
||||
}
|
||||
|
||||
while *scrolled_lines <= -1.0 {
|
||||
current_value =
|
||||
param_base.previous_normalized_step(current_value, use_finer_steps);
|
||||
param_base.set_normalized_value(cx, current_value);
|
||||
*scrolled_lines += 1.0;
|
||||
while *remaining_scroll_x <= -1.0 {
|
||||
current_value = self
|
||||
.x_param_base
|
||||
.previous_normalized_step(current_value, use_finer_steps);
|
||||
self.x_param_base
|
||||
.set_normalized_value(cx, clamp_x_value(current_value));
|
||||
*remaining_scroll_x += 1.0;
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -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 {
|
||||
(MAX_AUTOMATION_STEP_SIZE - step_size) as f32
|
||||
/ (MAX_AUTOMATION_STEP_SIZE - MIN_AUTOMATION_STEP_SIZE) as f32
|
||||
|
|
Loading…
Reference in a new issue