1
0
Fork 0

💥 Use interior mutability for parameters

Instead of the previous technically-unsound approach. While it wouldn't
cause any issues in practice, it did break Rust's guarantees. That was a
design choice after adding support for editors in NIH-plug, but this is
probably the better long term solution.

The downside is that all uses of `param.value` now need to be changed to
`param.value()`.
This commit is contained in:
Robbert van der Helm 2022-09-06 21:55:14 +02:00
parent 5966e353da
commit c566888fa3
17 changed files with 262 additions and 238 deletions

View file

@ -6,6 +6,17 @@ new and what's changed, this document lists all breaking changes in reverse
chronological order. If a new feature did not require any changes to existing
code then it will not be listed here.
## [2022-09-06]
- Parameter values are now accessed using `param.value()` instead of
`param.value`, with `param.value()` being an alias for the existing
`param.plain_value()` function. The old approach, while perfectly safe in
practice, was technically unsound because it used mutable pointers to
parameters that may also be simultaneously read from in an editor GUI. With
this change the parameters now use actual relaxed atomic stores and loads to
avoid mutable aliasing, which means the value fields are now no longer
directly accessible.
## [2022-09-04]
- `Smoother::next_block_mapped()` and `Smoother::next_block_exact_mapped()` have

View file

@ -392,7 +392,7 @@ impl Plugin for Crisp {
}
}
if self.params.wet_only.value {
if self.params.wet_only.value() {
for (channel_samples, rm_outputs) in block.iter_samples().zip(&mut rm_outputs) {
let output_gain = self.params.output_gain.smoothed.next();
for (sample, rm_output) in channel_samples.into_iter().zip(rm_outputs) {

View file

@ -291,7 +291,7 @@ impl Crossover {
}
self.iir_crossover.process(
self.params.num_bands.value as usize,
self.params.num_bands.value() as usize,
&main_channel_samples,
bands,
);
@ -331,7 +331,7 @@ impl Crossover {
];
self.fir_crossover.process(
self.params.num_bands.value as usize,
self.params.num_bands.value() as usize,
main_io,
band_outputs,
channel_idx,
@ -371,12 +371,12 @@ impl Crossover {
match self.params.crossover_type.value() {
CrossoverType::LinkwitzRiley24 => self.iir_crossover.update(
self.buffer_config.sample_rate,
self.params.num_bands.value as usize,
self.params.num_bands.value() as usize,
crossover_frequencies,
),
CrossoverType::LinkwitzRiley24LinearPhase => self.fir_crossover.update(
self.buffer_config.sample_rate,
self.params.num_bands.value as usize,
self.params.num_bands.value() as usize,
crossover_frequencies,
),
}

View file

@ -61,7 +61,7 @@ struct Diopser {
should_update_filters: Arc<AtomicBool>,
/// If this is 1 and any of the filter parameters are still smoothing, thenn the filter
/// coefficients should be recalculated on the next sample. After that, this gets reset to
/// `unnormalize_automation_precision(self.params.automation_precision.value)`. This is to
/// `unnormalize_automation_precision(self.params.automation_precision.value())`. This is to
/// reduce the DSP load of automation parameters. It can also cause some fun sounding glitchy
/// effects when the precision is low.
next_filter_smoothing_in: i32,
@ -291,7 +291,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);
unnormalize_automation_precision(self.params.automation_precision.value());
for mut channel_samples in buffer.iter_samples() {
self.maybe_update_filters(smoothing_interval);
@ -303,7 +303,7 @@ impl Plugin for Diopser {
for filter in self
.filters
.iter_mut()
.take(self.params.filter_stages.value as usize)
.take(self.params.filter_stages.value() as usize)
{
samples = filter.process(samples);
}
@ -379,10 +379,10 @@ impl Diopser {
// TODO: This wrecks the DSP load at high smoothing accuracy, perhaps also use SIMD here
const MIN_FREQUENCY: f32 = 5.0;
let max_frequency = self.sample_rate / 2.05;
for filter_idx in 0..self.params.filter_stages.value as usize {
for filter_idx in 0..self.params.filter_stages.value() as usize {
// The index of the filter normalized to range [-1, 1]
let filter_proportion =
(filter_idx as f32 / self.params.filter_stages.value as f32) * 2.0 - 1.0;
(filter_idx as f32 / self.params.filter_stages.value() as f32) * 2.0 - 1.0;
// The spread parameter adds an offset to the frequency depending on the number of the
// filter

View file

@ -120,7 +120,7 @@ impl Plugin for Gain {
setter.end_set_parameter(&params.gain);
new_value
}
None => params.gain.value as f64,
None => params.gain.value() as f64,
}
})
.suffix(" dB"),

View file

@ -212,8 +212,8 @@ impl Plugin for PolyModSynth {
} => {
let initial_phase: f32 = self.prng.gen();
// This starts with the attack portion of the amplitude envelope
let mut amp_envelope = Smoother::new(SmoothingStyle::Exponential(
self.params.amp_attack_ms.value,
let amp_envelope = Smoother::new(SmoothingStyle::Exponential(
self.params.amp_attack_ms.value(),
));
amp_envelope.reset(0.0);
amp_envelope.set_target(sample_rate, 1.0);
@ -522,7 +522,7 @@ impl PolyModSynth {
{
*releasing = true;
amp_envelope.style =
SmoothingStyle::Exponential(self.params.amp_release_ms.value);
SmoothingStyle::Exponential(self.params.amp_release_ms.value());
amp_envelope.set_target(sample_rate, 0.0);
// If this targetted a single voice ID, we're done here. Otherwise there may be

View file

@ -150,7 +150,7 @@ impl Plugin for Sine {
let gain = self.params.gain.smoothed.next();
// This plugin can be either triggered by MIDI or controleld by a parameter
let sine = if self.params.use_midi.value {
let sine = if self.params.use_midi.value() {
// Act on the next MIDI event
while let Some(event) = next_event {
if event.timing() > sample_id as u32 {

View file

@ -245,7 +245,7 @@ impl Plugin for PubertySimulator {
// These plans have already been made during initialization we can switch between versions
// without reallocating
let fft_plan = &mut self.plan_for_order.as_mut().unwrap()
[self.params.window_size_order.value as usize - MIN_WINDOW_ORDER];
[self.params.window_size_order.value() as usize - MIN_WINDOW_ORDER];
let mut smoothed_pitch_value = 0.0;
self.stft
@ -395,11 +395,11 @@ impl Plugin for PubertySimulator {
impl PubertySimulator {
fn window_size(&self) -> usize {
1 << self.params.window_size_order.value as usize
1 << self.params.window_size_order.value() as usize
}
fn overlap_times(&self) -> usize {
1 << self.params.overlap_times_order.value as usize
1 << self.params.overlap_times_order.value() as usize
}
/// `window_size` should not exceed `MAX_WINDOW_SIZE` or this will allocate.

View file

@ -209,7 +209,7 @@ impl Plugin for SafetyLimiter {
let mut is_peaking = false;
for sample in channel_samples.iter_mut() {
if sample.is_finite() {
is_peaking |= sample.abs() > self.params.threshold_gain.value;
is_peaking |= sample.abs() > self.params.threshold_gain.value();
} else {
// Infinity or NaN values need to be completely filtered out, because otherwise
// we'll try to mix them back into the signal later
@ -253,7 +253,7 @@ impl Plugin for SafetyLimiter {
// This phase runs from 0 to `2 * pi` as an optimization, so we can use it
// directly. And the sine wave is scaled down to the threshold minus 24 dB
let sine_sample =
self.osc_phase_tau.sin() * (self.params.threshold_gain.value * 0.125);
self.osc_phase_tau.sin() * (self.params.threshold_gain.value() * 0.125);
self.osc_phase_tau += self.osc_phase_tau_dt;
if self.osc_phase_tau >= std::f32::consts::TAU {
self.osc_phase_tau -= std::f32::consts::TAU;

View file

@ -570,18 +570,18 @@ impl CompressorBank {
// for every 512 samples.
let effective_sample_rate =
self.sample_rate / (self.window_size as f32 / overlap_times as f32);
let attack_old_t = if params.global.compressor_attack_ms.value == 0.0 {
let attack_old_t = if params.global.compressor_attack_ms.value() == 0.0 {
0.0
} else {
(-1.0 / (params.global.compressor_attack_ms.value / 1000.0 * effective_sample_rate))
(-1.0 / (params.global.compressor_attack_ms.value() / 1000.0 * effective_sample_rate))
.exp()
};
let attack_new_t = 1.0 - attack_old_t;
// The same as `attack_old_t`, but for the release phase of the envelope follower
let release_old_t = if params.global.compressor_release_ms.value == 0.0 {
let release_old_t = if params.global.compressor_release_ms.value() == 0.0 {
0.0
} else {
(-1.0 / (params.global.compressor_release_ms.value / 1000.0 * effective_sample_rate))
(-1.0 / (params.global.compressor_release_ms.value() / 1000.0 * effective_sample_rate))
.exp()
};
let release_new_t = 1.0 - release_old_t;
@ -611,24 +611,24 @@ impl CompressorBank {
// See `update_envelopes()`
let effective_sample_rate =
self.sample_rate / (self.window_size as f32 / overlap_times as f32);
let attack_old_t = if params.global.compressor_attack_ms.value == 0.0 {
let attack_old_t = if params.global.compressor_attack_ms.value() == 0.0 {
0.0
} else {
(-1.0 / (params.global.compressor_attack_ms.value / 1000.0 * effective_sample_rate))
(-1.0 / (params.global.compressor_attack_ms.value() / 1000.0 * effective_sample_rate))
.exp()
};
let attack_new_t = 1.0 - attack_old_t;
let release_old_t = if params.global.compressor_release_ms.value == 0.0 {
let release_old_t = if params.global.compressor_release_ms.value() == 0.0 {
0.0
} else {
(-1.0 / (params.global.compressor_release_ms.value / 1000.0 * effective_sample_rate))
(-1.0 / (params.global.compressor_release_ms.value() / 1000.0 * effective_sample_rate))
.exp()
};
let release_new_t = 1.0 - release_old_t;
// For the channel linking
let num_channels = self.sidechain_spectrum_magnitudes.len() as f32;
let other_channels_t = params.threshold.sc_channel_link.value / num_channels;
let other_channels_t = params.threshold.sc_channel_link.value() / num_channels;
let this_channel_t = 1.0 - (other_channels_t * (num_channels - 1.0));
for (bin_idx, envelope) in self.envelopes[channel_idx].iter_mut().enumerate() {
@ -689,11 +689,11 @@ impl CompressorBank {
// bandwidths, the middle values needs to be pushed more towards the post-knee threshold
// than with lower knee values. These scaling factors are used as exponents.
let downwards_knee_scaling_factor =
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value);
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value());
// Note the square root here, since the curve needs to go the other way for the upwards
// version
let upwards_knee_scaling_factor =
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value).sqrt();
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value()).sqrt();
assert!(self.downwards_thresholds.len() == buffer.len());
assert!(self.downwards_ratio_recips.len() == buffer.len());
@ -768,13 +768,13 @@ impl CompressorBank {
) {
// See `compress` for more details
let downwards_knee_scaling_factor =
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value);
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value());
let upwards_knee_scaling_factor =
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value).sqrt();
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value()).sqrt();
// For the channel linking
let num_channels = self.sidechain_spectrum_magnitudes.len() as f32;
let other_channels_t = params.threshold.sc_channel_link.value / num_channels;
let other_channels_t = params.threshold.sc_channel_link.value() / num_channels;
let this_channel_t = 1.0 - (other_channels_t * (num_channels - 1.0));
assert!(self.sidechain_spectrum_magnitudes[channel_idx].len() == buffer.len());
@ -859,23 +859,23 @@ impl CompressorBank {
fn update_if_needed(&mut self, params: &SpectralCompressorParams) {
// The threshold curve is a polynomial in log-log (decibels-octaves) space. The reuslt from
// evaluating this needs to be converted to linear gain for the compressors.
let intercept = params.threshold.threshold_db.value;
let intercept = params.threshold.threshold_db.value();
// The cheeky 3 additional dB/octave attenuation is to match pink noise with the default
// settings. When using sidechaining we explicitly don't want this because the curve should
// be a flat offset to the sidechain input at the default settings.
let slope = match params.threshold.mode.value() {
ThresholdMode::Internal => params.threshold.curve_slope.value - 3.0,
ThresholdMode::Internal => params.threshold.curve_slope.value() - 3.0,
ThresholdMode::SidechainMatch | ThresholdMode::SidechainCompress => {
params.threshold.curve_slope.value
params.threshold.curve_slope.value()
}
};
let curve = params.threshold.curve_curve.value;
let log2_center_freq = params.threshold.center_frequency.value.log2();
let curve = params.threshold.curve_curve.value();
let log2_center_freq = params.threshold.center_frequency.value().log2();
let downwards_high_freq_ratio_rolloff =
params.compressors.downwards.high_freq_ratio_rolloff.value;
params.compressors.downwards.high_freq_ratio_rolloff.value();
let upwards_high_freq_ratio_rolloff =
params.compressors.upwards.high_freq_ratio_rolloff.value;
params.compressors.upwards.high_freq_ratio_rolloff.value();
let log2_nyquist_freq = self
.log2_freqs
.last()
@ -886,7 +886,7 @@ impl CompressorBank {
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
let intercept = intercept + params.compressors.downwards.threshold_offset_db.value;
let intercept = intercept + params.compressors.downwards.threshold_offset_db.value();
for ((log2_freq, threshold), (knee_start, knee_end)) in self
.log2_freqs
.iter()
@ -900,9 +900,9 @@ impl CompressorBank {
let offset = log2_freq - log2_center_freq;
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
let knee_start_db =
threshold_db - (params.compressors.downwards.knee_width_db.value / 2.0);
threshold_db - (params.compressors.downwards.knee_width_db.value() / 2.0);
let knee_end_db =
threshold_db + (params.compressors.downwards.knee_width_db.value / 2.0);
threshold_db + (params.compressors.downwards.knee_width_db.value() / 2.0);
// This threshold must never reach zero as it's used in divisions to get a gain ratio
// above the threshold
@ -917,7 +917,7 @@ impl CompressorBank {
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
let intercept = intercept + params.compressors.upwards.threshold_offset_db.value;
let intercept = intercept + params.compressors.upwards.threshold_offset_db.value();
for ((log2_freq, threshold), (knee_start, knee_end)) in self
.log2_freqs
.iter()
@ -931,9 +931,9 @@ impl CompressorBank {
let offset = log2_freq - log2_center_freq;
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
let knee_start_db =
threshold_db - (params.compressors.upwards.knee_width_db.value / 2.0);
threshold_db - (params.compressors.upwards.knee_width_db.value() / 2.0);
let knee_end_db =
threshold_db + (params.compressors.upwards.knee_width_db.value / 2.0);
threshold_db + (params.compressors.upwards.knee_width_db.value() / 2.0);
*threshold = util::db_to_gain(threshold_db).max(f32::EPSILON);
*knee_start = util::db_to_gain(knee_start_db).max(f32::EPSILON);
@ -948,7 +948,7 @@ impl CompressorBank {
{
// If the high-frequency rolloff is enabled then higher frequency bins will have their
// ratios reduced to reduce harshness. This follows the octave scale.
let target_ratio_recip = params.compressors.downwards.ratio.value.recip();
let target_ratio_recip = params.compressors.downwards.ratio.value().recip();
if downwards_high_freq_ratio_rolloff == 0.0 {
self.downwards_ratio_recips.fill(target_ratio_recip);
} else {
@ -971,7 +971,7 @@ impl CompressorBank {
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
let target_ratio_recip = params.compressors.upwards.ratio.value.recip();
let target_ratio_recip = params.compressors.upwards.ratio.value().recip();
if upwards_high_freq_ratio_rolloff == 0.0 {
self.upwards_ratio_recips.fill(target_ratio_recip);
} else {

View file

@ -357,7 +357,7 @@ impl Plugin for SpectralCompressor {
// These plans have already been made during initialization we can switch between versions
// without reallocating
let fft_plan = &mut self.plan_for_order.as_mut().unwrap()
[self.params.global.window_size_order.value as usize - MIN_WINDOW_ORDER];
[self.params.global.window_size_order.value() as usize - MIN_WINDOW_ORDER];
let num_bins = self.complex_fft_buffer.len();
// The Hann window function spreads the DC signal out slightly, so we'll clear all 0-20 Hz
// bins for this. With small window sizes you probably don't want this as it would result in
@ -378,7 +378,7 @@ impl Plugin for SpectralCompressor {
// threshold option. When sidechaining is enabled this is used to gain up the sidechain
// signal instead.
let input_gain = gain_compensation.sqrt();
let output_gain = self.params.global.output_gain.value * gain_compensation.sqrt();
let output_gain = self.params.global.output_gain.value() * gain_compensation.sqrt();
// TODO: Auto makeup gain
// This is mixed in later with latency compensation applied
@ -459,11 +459,11 @@ impl Plugin for SpectralCompressor {
impl SpectralCompressor {
fn window_size(&self) -> usize {
1 << self.params.global.window_size_order.value as usize
1 << self.params.global.window_size_order.value() as usize
}
fn overlap_times(&self) -> usize {
1 << self.params.global.overlap_times_order.value as usize
1 << self.params.global.overlap_times_order.value() as usize
}
/// `window_size` should not exceed `MAX_WINDOW_SIZE` or this will allocate.
@ -531,12 +531,12 @@ fn process_stft_main(
// The DC and other low frequency bins doesn't contain much semantic meaning anymore after all
// of this, so it only ends up consuming headroom. Otherwise they're gained down by the output
// gain to prevent makeup gain from making these bins too loud.
if params.global.dc_filter.value {
if params.global.dc_filter.value() {
complex_fft_buffer[..first_non_dc_bin_idx].fill(Complex32::default());
} else {
// The `output_gain` parameter also contains gain compensation for the windowingq, we don't
// want to compensate for that
let output_gain_recip = params.global.output_gain.value.recip();
let output_gain_recip = params.global.output_gain.value().recip();
for bin in complex_fft_buffer[..first_non_dc_bin_idx].iter_mut() {
*bin *= output_gain_recip;
}

View file

@ -171,7 +171,7 @@ pub(crate) trait ParamMut: Param {
/// value then this offset is taken into account to form the effective value.
///
/// This does **not** update the smoother.
fn set_plain_value(&mut self, plain: Self::Plain);
fn set_plain_value(&self, plain: Self::Plain);
/// Set this parameter based on a normalized value. The normalized value will be snapped to the
/// step size for continuous parameters (i.e. [`FloatParam`]). If
@ -179,7 +179,7 @@ pub(crate) trait ParamMut: Param {
/// value then this offset is taken into account to form the effective value.
///
/// This does **not** update the smoother.
fn set_normalized_value(&mut self, normalized: f32);
fn set_normalized_value(&self, normalized: f32);
/// Add a modulation offset to the value's unmodulated value. This value sticks until this
/// function is called again with a 0.0 value. Out of bound values will be clamped to the
@ -187,10 +187,10 @@ pub(crate) trait ParamMut: Param {
/// parameters (i.e. [`FloatParam`]).
///
/// This does **not** update the smoother.
fn modulate_value(&mut self, modulation_offset: f32);
fn modulate_value(&self, modulation_offset: f32);
/// Update the smoother state to point to the current value. Also used when initializing and
/// restoring a plugin so everything is in sync. In that case the smoother should completely
/// reset to the current value.
fn update_smoother(&mut self, sample_rate: f32, reset: bool);
fn update_smoother(&self, sample_rate: f32, reset: bool);
}

View file

@ -1,28 +1,29 @@
//! Simple boolean parameters.
use atomic_float::AtomicF32;
use std::fmt::Display;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use super::internals::ParamPtr;
use super::{Param, ParamFlags, ParamMut};
/// A simple boolean parameter.
#[repr(C, align(4))]
pub struct BoolParam {
/// The field's current value, after monophonic modulation has been applied.
pub value: bool,
value: AtomicBool,
/// The field's current value normalized to the `[0, 1]` range.
normalized_value: f32,
normalized_value: AtomicF32,
/// The field's value before any monophonic automation coming from the host has been applied.
/// This will always be the same as `value` for VST3 plugins.
unmodulated_value: bool,
unmodulated_value: AtomicBool,
/// The field's value normalized to the `[0, 1]` range before any monophonic automation coming
/// from the host has been applied. This will always be the same as `value` for VST3 plugins.
unmodulated_normalized_value: f32,
unmodulated_normalized_value: AtomicF32,
/// A value in `[-1, 1]` indicating the amount of modulation applied to
/// `unmodulated_normalized_`. This needs to be stored separately since the normalied values are
/// clamped, and this value persists after new automation events.
modulation_offset: f32,
modulation_offset: AtomicF32,
/// The field's default value.
default: bool,
@ -52,7 +53,7 @@ pub struct BoolParam {
impl Display for BoolParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.value, &self.value_to_string) {
match (self.value(), &self.value_to_string) {
(v, Some(func)) => write!(f, "{}", func(v)),
(true, None) => write!(f, "On"),
(false, None) => write!(f, "Off"),
@ -77,22 +78,22 @@ impl Param for BoolParam {
#[inline]
fn plain_value(&self) -> Self::Plain {
self.value
self.value.load(Ordering::Relaxed)
}
#[inline]
fn normalized_value(&self) -> f32 {
self.normalized_value
self.normalized_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_plain_value(&self) -> Self::Plain {
self.unmodulated_value
self.unmodulated_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_normalized_value(&self) -> f32 {
self.unmodulated_normalized_value
self.unmodulated_normalized_value.load(Ordering::Relaxed)
}
#[inline]
@ -153,48 +154,50 @@ impl Param for BoolParam {
}
impl ParamMut for BoolParam {
fn set_plain_value(&mut self, plain: Self::Plain) {
self.unmodulated_value = plain;
self.unmodulated_normalized_value = self.preview_normalized(plain);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
fn set_plain_value(&self, plain: Self::Plain) {
let unmodulated_value = plain;
let unmodulated_normalized_value = self.preview_normalized(plain);
let modulation_offset = self.modulation_offset.load(Ordering::Relaxed);
let (value, normalized_value) = if modulation_offset == 0.0 {
(unmodulated_value, unmodulated_normalized_value)
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
let normalized_value =
(unmodulated_normalized_value + modulation_offset).clamp(0.0, 1.0);
(self.preview_plain(normalized_value), normalized_value)
};
self.value.store(value, Ordering::Relaxed);
self.normalized_value
.store(normalized_value, Ordering::Relaxed);
self.unmodulated_value
.store(unmodulated_value, Ordering::Relaxed);
self.unmodulated_normalized_value
.store(unmodulated_normalized_value, Ordering::Relaxed);
if let Some(f) = &self.value_changed {
f(self.value);
f(value);
}
}
fn set_normalized_value(&mut self, normalized: f32) {
fn set_normalized_value(&self, normalized: f32) {
// NOTE: The double conversion here is to make sure the state is reproducible. State is
// saved and restored using plain values, and the new normalized value will be
// different from `normalized`. This is not necesasry for the modulation as these
// values are never shown to the host.
self.unmodulated_value = self.preview_plain(normalized);
self.unmodulated_normalized_value = self.preview_normalized(self.unmodulated_value);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
if let Some(f) = &self.value_changed {
f(self.value);
}
self.set_plain_value(self.preview_plain(normalized))
}
fn modulate_value(&mut self, modulation_offset: f32) {
self.modulation_offset = modulation_offset;
self.set_normalized_value(self.unmodulated_normalized_value);
fn modulate_value(&self, modulation_offset: f32) {
self.modulation_offset
.store(modulation_offset, Ordering::Relaxed);
// TODO: This renormalizes this value, which is not necessary
self.set_plain_value(self.plain_value());
}
fn update_smoother(&mut self, _sample_rate: f32, _init: bool) {
fn update_smoother(&self, _sample_rate: f32, _init: bool) {
// Can't really smooth a binary parameter now can you
}
}
@ -204,11 +207,11 @@ impl BoolParam {
/// parameter.
pub fn new(name: impl Into<String>, default: bool) -> Self {
Self {
value: default,
normalized_value: if default { 1.0 } else { 0.0 },
unmodulated_value: default,
unmodulated_normalized_value: if default { 1.0 } else { 0.0 },
modulation_offset: 0.0,
value: AtomicBool::new(default),
normalized_value: AtomicF32::new(if default { 1.0 } else { 0.0 }),
unmodulated_value: AtomicBool::new(default),
unmodulated_normalized_value: AtomicF32::new(if default { 1.0 } else { 0.0 }),
modulation_offset: AtomicF32::new(0.0),
default,
flags: ParamFlags::default(),
@ -221,6 +224,13 @@ impl BoolParam {
}
}
/// The field's current plain value, after monophonic modulation has been applied. Equivalent to
/// calling `param.plain_value()`.
#[inline]
pub fn value(&self) -> bool {
self.plain_value()
}
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the

View file

@ -267,37 +267,37 @@ impl Param for EnumParamInner {
}
impl<T: Enum + PartialEq> ParamMut for EnumParam<T> {
fn set_plain_value(&mut self, plain: Self::Plain) {
fn set_plain_value(&self, plain: Self::Plain) {
self.inner.set_plain_value(T::to_index(plain) as i32)
}
fn set_normalized_value(&mut self, normalized: f32) {
fn set_normalized_value(&self, normalized: f32) {
self.inner.set_normalized_value(normalized)
}
fn modulate_value(&mut self, modulation_offset: f32) {
fn modulate_value(&self, modulation_offset: f32) {
self.inner.modulate_value(modulation_offset)
}
fn update_smoother(&mut self, sample_rate: f32, reset: bool) {
fn update_smoother(&self, sample_rate: f32, reset: bool) {
self.inner.update_smoother(sample_rate, reset)
}
}
impl ParamMut for EnumParamInner {
fn set_plain_value(&mut self, plain: Self::Plain) {
fn set_plain_value(&self, plain: Self::Plain) {
self.inner.set_plain_value(plain)
}
fn set_normalized_value(&mut self, normalized: f32) {
fn set_normalized_value(&self, normalized: f32) {
self.inner.set_normalized_value(normalized)
}
fn modulate_value(&mut self, modulation_offset: f32) {
fn modulate_value(&self, modulation_offset: f32) {
self.inner.modulate_value(modulation_offset)
}
fn update_smoother(&mut self, sample_rate: f32, reset: bool) {
fn update_smoother(&self, sample_rate: f32, reset: bool) {
self.inner.update_smoother(sample_rate, reset)
}
}
@ -326,6 +326,12 @@ impl<T: Enum + PartialEq + 'static> EnumParam<T> {
}
}
/// Get the active enum variant.
#[inline]
pub fn value(&self) -> T {
self.plain_value()
}
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
@ -376,11 +382,6 @@ impl<T: Enum + PartialEq + 'static> EnumParam<T> {
self.inner.inner = self.inner.inner.hide_in_generic_ui();
self
}
/// Get the active enum variant.
pub fn value(&self) -> T {
self.plain_value()
}
}
impl EnumParamInner {
@ -402,7 +403,7 @@ impl EnumParamInner {
/// Set the parameter based on a serialized stable string identifier. Return whether the ID was
/// known and the parameter was set.
pub fn set_from_id(&mut self, id: &str) -> bool {
pub fn set_from_id(&self, id: &str) -> bool {
match self
.ids
.and_then(|ids| ids.iter().position(|candidate| *candidate == id))

View file

@ -1,6 +1,8 @@
//! Continuous (or discrete, with a step size) floating point parameters.
use atomic_float::AtomicF32;
use std::fmt::Display;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::internals::ParamPtr;
@ -10,31 +12,21 @@ use super::{Param, ParamFlags, ParamMut};
/// A floating point parameter that's stored unnormalized. The range is used for the normalization
/// process.
//
// XXX: To keep the API simple and to allow the optimizer to do its thing, the values are stored as
// plain primitive values that are modified through the `*mut` pointers from the plugin's
// `Params` object. Technically modifying these while the GUI is open is unsound. We could
// remedy this by changing `value` to be an atomic type and adding a function also called
// `value()` to load that value, but in practice that should not be necessary if we don't do
// anything crazy other than modifying this value. On both AArch64 and x86(_64) reads and
// writes to naturally aligned values up to word size are atomic, so there's no risk of reading
// a partially written to value here. We should probably reconsider this at some point though.
#[repr(C, align(4))]
pub struct FloatParam {
/// The field's current plain value, after monophonic modulation has been applied.
pub value: f32,
value: AtomicF32,
/// The field's current value normalized to the `[0, 1]` range.
normalized_value: f32,
normalized_value: AtomicF32,
/// The field's plain, unnormalized value before any monophonic automation coming from the host
/// has been applied. This will always be the same as `value` for VST3 plugins.
unmodulated_value: f32,
unmodulated_value: AtomicF32,
/// The field's value normalized to the `[0, 1]` range before any monophonic automation coming
/// from the host has been applied. This will always be the same as `value` for VST3 plugins.
unmodulated_normalized_value: f32,
unmodulated_normalized_value: AtomicF32,
/// A value in `[-1, 1]` indicating the amount of modulation applied to
/// `unmodulated_normalized_`. This needs to be stored separately since the normalied values are
/// clamped, and this value persists after new automation events.
modulation_offset: f32,
modulation_offset: AtomicF32,
/// The field's default plain, unnormalized value.
default: f32,
/// An optional smoother that will automatically interpolate between the new automation values
@ -84,12 +76,12 @@ pub struct FloatParam {
impl Display for FloatParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.value_to_string, &self.step_size) {
(Some(func), _) => write!(f, "{}{}", func(self.value), self.unit),
(Some(func), _) => write!(f, "{}{}", func(self.value()), self.unit),
(None, Some(step_size)) => {
let num_digits = decimals_from_step_size(*step_size);
write!(f, "{:.num_digits$}{}", self.value, self.unit)
write!(f, "{:.num_digits$}{}", self.value(), self.unit)
}
_ => write!(f, "{}{}", self.value, self.unit),
_ => write!(f, "{}{}", self.value(), self.unit),
}
}
}
@ -111,22 +103,22 @@ impl Param for FloatParam {
#[inline]
fn plain_value(&self) -> Self::Plain {
self.value
self.value.load(Ordering::Relaxed)
}
#[inline]
fn normalized_value(&self) -> Self::Plain {
self.normalized_value
fn normalized_value(&self) -> f32 {
self.normalized_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_plain_value(&self) -> Self::Plain {
self.unmodulated_value
self.unmodulated_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_normalized_value(&self) -> f32 {
self.unmodulated_normalized_value
self.unmodulated_normalized_value.load(Ordering::Relaxed)
}
#[inline]
@ -196,52 +188,54 @@ impl Param for FloatParam {
}
impl ParamMut for FloatParam {
fn set_plain_value(&mut self, plain: Self::Plain) {
self.unmodulated_value = plain;
self.unmodulated_normalized_value = self.preview_normalized(plain);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
fn set_plain_value(&self, plain: Self::Plain) {
let unmodulated_value = plain;
let unmodulated_normalized_value = self.preview_normalized(plain);
let modulation_offset = self.modulation_offset.load(Ordering::Relaxed);
let (value, normalized_value) = if modulation_offset == 0.0 {
(unmodulated_value, unmodulated_normalized_value)
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
let normalized_value =
(unmodulated_normalized_value + modulation_offset).clamp(0.0, 1.0);
(self.preview_plain(normalized_value), normalized_value)
};
self.value.store(value, Ordering::Relaxed);
self.normalized_value
.store(normalized_value, Ordering::Relaxed);
self.unmodulated_value
.store(unmodulated_value, Ordering::Relaxed);
self.unmodulated_normalized_value
.store(unmodulated_normalized_value, Ordering::Relaxed);
if let Some(f) = &self.value_changed {
f(self.value);
f(value);
}
}
fn set_normalized_value(&mut self, normalized: f32) {
fn set_normalized_value(&self, normalized: f32) {
// NOTE: The double conversion here is to make sure the state is reproducible. State is
// saved and restored using plain values, and the new normalized value will be
// different from `normalized`. This is not necesasry for the modulation as these
// values are never shown to the host.
self.unmodulated_value = self.preview_plain(normalized);
self.unmodulated_normalized_value = self.preview_normalized(self.unmodulated_value);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
if let Some(f) = &self.value_changed {
f(self.value);
}
self.set_plain_value(self.preview_plain(normalized))
}
fn modulate_value(&mut self, modulation_offset: f32) {
self.modulation_offset = modulation_offset;
self.set_normalized_value(self.unmodulated_normalized_value);
fn modulate_value(&self, modulation_offset: f32) {
self.modulation_offset
.store(modulation_offset, Ordering::Relaxed);
// TODO: This renormalizes this value, which is not necessary
self.set_plain_value(self.plain_value());
}
fn update_smoother(&mut self, sample_rate: f32, reset: bool) {
fn update_smoother(&self, sample_rate: f32, reset: bool) {
if reset {
self.smoothed.reset(self.value);
self.smoothed.reset(self.plain_value());
} else {
self.smoothed.set_target(sample_rate, self.value);
self.smoothed.set_target(sample_rate, self.plain_value());
}
}
}
@ -251,11 +245,11 @@ impl FloatParam {
/// parameter.
pub fn new(name: impl Into<String>, default: f32, range: FloatRange) -> Self {
Self {
value: default,
normalized_value: range.normalize(default),
unmodulated_value: default,
unmodulated_normalized_value: range.normalize(default),
modulation_offset: 0.0,
value: AtomicF32::new(default),
normalized_value: AtomicF32::new(range.normalize(default)),
unmodulated_value: AtomicF32::new(default),
unmodulated_normalized_value: AtomicF32::new(range.normalize(default)),
modulation_offset: AtomicF32::new(0.0),
default,
smoothed: Smoother::none(),
@ -272,6 +266,13 @@ impl FloatParam {
}
}
/// The field's current plain value, after monophonic modulation has been applied. Equivalent to
/// calling `param.plain_value()`.
#[inline]
pub fn value(&self) -> f32 {
self.plain_value()
}
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the

View file

@ -1,6 +1,8 @@
//! Stepped integer parameters.
use atomic_float::AtomicF32;
use std::fmt::Display;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use super::internals::ParamPtr;
@ -10,31 +12,21 @@ use super::{Param, ParamFlags, ParamMut};
/// A discrete integer parameter that's stored unnormalized. The range is used for the normalization
/// process.
//
// XXX: To keep the API simple and to allow the optimizer to do its thing, the values are stored as
// plain primitive values that are modified through the `*mut` pointers from the plugin's
// `Params` object. Technically modifying these while the GUI is open is unsound. We could
// remedy this by changing `value` to be an atomic type and adding a function also called
// `value()` to load that value, but in practice that should not be necessary if we don't do
// anything crazy other than modifying this value. On both AArch64 and x86(_64) reads and
// writes to naturally aligned values up to word size are atomic, so there's no risk of reading
// a partially written to value here. We should probably reconsider this at some point though.
#[repr(C, align(4))]
pub struct IntParam {
/// The field's current plain value, after monophonic modulation has been applied.
pub value: i32,
value: AtomicI32,
/// The field's current value normalized to the `[0, 1]` range.
normalized_value: f32,
normalized_value: AtomicF32,
/// The field's plain, unnormalized value before any monophonic automation coming from the host
/// has been applied. This will always be the same as `value` for VST3 plugins.
unmodulated_value: i32,
unmodulated_value: AtomicI32,
/// The field's value normalized to the `[0, 1]` range before any monophonic automation coming
/// from the host has been applied. This will always be the same as `value` for VST3 plugins.
unmodulated_normalized_value: f32,
unmodulated_normalized_value: AtomicF32,
/// A value in `[-1, 1]` indicating the amount of modulation applied to
/// `unmodulated_normalized_`. This needs to be stored separately since the normalied values are
/// clamped, and this value persists after new automation events.
modulation_offset: f32,
modulation_offset: AtomicF32,
/// The field's default plain, unnormalized value.
default: i32,
/// An optional smoother that will automatically interpolate between the new automation values
@ -80,8 +72,8 @@ pub struct IntParam {
impl Display for IntParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.value_to_string {
Some(func) => write!(f, "{}{}", func(self.value), self.unit),
_ => write!(f, "{}{}", self.value, self.unit),
Some(func) => write!(f, "{}{}", func(self.value()), self.unit),
_ => write!(f, "{}{}", self.value(), self.unit),
}
}
}
@ -103,22 +95,22 @@ impl Param for IntParam {
#[inline]
fn plain_value(&self) -> Self::Plain {
self.value
self.value.load(Ordering::Relaxed)
}
#[inline]
fn normalized_value(&self) -> f32 {
self.normalized_value
self.normalized_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_plain_value(&self) -> Self::Plain {
self.unmodulated_value
self.unmodulated_value.load(Ordering::Relaxed)
}
#[inline]
fn unmodulated_normalized_value(&self) -> f32 {
self.unmodulated_normalized_value
self.unmodulated_normalized_value.load(Ordering::Relaxed)
}
#[inline]
@ -176,52 +168,54 @@ impl Param for IntParam {
}
impl ParamMut for IntParam {
fn set_plain_value(&mut self, plain: Self::Plain) {
self.unmodulated_value = plain;
self.unmodulated_normalized_value = self.preview_normalized(plain);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
fn set_plain_value(&self, plain: Self::Plain) {
let unmodulated_value = plain;
let unmodulated_normalized_value = self.preview_normalized(plain);
let modulation_offset = self.modulation_offset.load(Ordering::Relaxed);
let (value, normalized_value) = if modulation_offset == 0.0 {
(unmodulated_value, unmodulated_normalized_value)
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
let normalized_value =
(unmodulated_normalized_value + modulation_offset).clamp(0.0, 1.0);
(self.preview_plain(normalized_value), normalized_value)
};
self.value.store(value, Ordering::Relaxed);
self.normalized_value
.store(normalized_value, Ordering::Relaxed);
self.unmodulated_value
.store(unmodulated_value, Ordering::Relaxed);
self.unmodulated_normalized_value
.store(unmodulated_normalized_value, Ordering::Relaxed);
if let Some(f) = &self.value_changed {
f(self.value);
f(value);
}
}
fn set_normalized_value(&mut self, normalized: f32) {
fn set_normalized_value(&self, normalized: f32) {
// NOTE: The double conversion here is to make sure the state is reproducible. State is
// saved and restored using plain values, and the new normalized value will be
// different from `normalized`. This is not necesasry for the modulation as these
// values are never shown to the host.
self.unmodulated_value = self.preview_plain(normalized);
self.unmodulated_normalized_value = self.preview_normalized(self.unmodulated_value);
if self.modulation_offset == 0.0 {
self.value = self.unmodulated_value;
self.normalized_value = self.unmodulated_normalized_value;
} else {
self.normalized_value =
(self.unmodulated_normalized_value + self.modulation_offset).clamp(0.0, 1.0);
self.value = self.preview_plain(self.normalized_value);
}
if let Some(f) = &self.value_changed {
f(self.value);
}
self.set_plain_value(self.preview_plain(normalized))
}
fn modulate_value(&mut self, modulation_offset: f32) {
self.modulation_offset = modulation_offset;
self.set_normalized_value(self.unmodulated_normalized_value);
fn modulate_value(&self, modulation_offset: f32) {
self.modulation_offset
.store(modulation_offset, Ordering::Relaxed);
// TODO: This renormalizes this value, which is not necessary
self.set_plain_value(self.plain_value());
}
fn update_smoother(&mut self, sample_rate: f32, reset: bool) {
fn update_smoother(&self, sample_rate: f32, reset: bool) {
if reset {
self.smoothed.reset(self.value);
self.smoothed.reset(self.plain_value());
} else {
self.smoothed.set_target(sample_rate, self.value);
self.smoothed.set_target(sample_rate, self.plain_value());
}
}
}
@ -231,11 +225,11 @@ impl IntParam {
/// parameter.
pub fn new(name: impl Into<String>, default: i32, range: IntRange) -> Self {
Self {
value: default,
normalized_value: range.normalize(default),
unmodulated_value: default,
unmodulated_normalized_value: range.normalize(default),
modulation_offset: 0.0,
value: AtomicI32::new(default),
normalized_value: AtomicF32::new(range.normalize(default)),
unmodulated_value: AtomicI32::new(default),
unmodulated_normalized_value: AtomicF32::new(range.normalize(default)),
modulation_offset: AtomicF32::new(0.0),
default,
smoothed: Smoother::none(),
@ -251,6 +245,13 @@ impl IntParam {
}
}
/// The field's current plain value, after monophonic modulation has been applied. Equivalent to
/// calling `param.plain_value()`.
#[inline]
pub fn value(&self) -> i32 {
self.plain_value()
}
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the

View file

@ -115,12 +115,12 @@ unsafe impl<P: Params> Params for Arc<P> {
/// erasure.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ParamPtr {
FloatParam(*mut super::FloatParam),
IntParam(*mut super::IntParam),
BoolParam(*mut super::BoolParam),
FloatParam(*const super::FloatParam),
IntParam(*const super::IntParam),
BoolParam(*const super::BoolParam),
/// Since we can't encode the actual enum here, this inner parameter struct contains all of the
/// relevant information from the enum so it can be type erased.
EnumParam(*mut super::enums::EnumParamInner),
EnumParam(*const super::enums::EnumParamInner),
}
// These pointers only point to fields on structs kept in an `Arc<dyn Params>`, and the caller