Skip to main content

nice_plug_core/params/
boolean.rs

1//! Simple boolean parameters.
2
3use atomic_float::AtomicF32;
4use std::fmt::{Debug, Display};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use super::internals::ParamPtr;
9use super::{InternalParamMut, Param, ParamFlags, ParamInfo};
10
11/// A simple boolean parameter.
12pub struct BoolParam {
13    /// The field's current value, after monophonic modulation has been applied.
14    value: AtomicBool,
15    /// The field's current value normalized to the `[0, 1]` range.
16    normalized_value: AtomicF32,
17    /// The field's value before any monophonic automation coming from the host has been applied.
18    /// This will always be the same as `value` for VST3 plugins.
19    unmodulated_value: AtomicBool,
20    /// The field's value normalized to the `[0, 1]` range before any monophonic automation coming
21    /// from the host has been applied. This will always be the same as `value` for VST3 plugins.
22    unmodulated_normalized_value: AtomicF32,
23    /// A value in `[-1, 1]` indicating the amount of modulation applied to
24    /// `unmodulated_normalized_`. This needs to be stored separately since the normalized values are
25    /// clamped, and this value persists after new automation events.
26    modulation_offset: AtomicF32,
27    /// The field's default value.
28    default: bool,
29
30    /// Optional callback for listening to value changes. The argument passed to this function is
31    /// the parameter's new value. This should not do anything expensive as it may be called
32    /// multiple times in rapid succession, and it can be run from both the GUI and the audio
33    /// thread.
34    value_changed: Option<Arc<dyn Fn(bool) + Send + Sync>>,
35
36    /// Metadata and conversion callbacks that are not used by the DSP hot path.
37    info: Box<ParamInfo<bool>>,
38    /// If this parameter has been marked as polyphonically modulatable, then this will be a unique
39    /// integer identifying the parameter. Because this value is determined by the plugin itself,
40    /// the plugin can easily map
41    /// [`NoteEvent::PolyModulation`][crate::prelude::NoteEvent::PolyModulation] events to the
42    /// correct parameter by pattern matching on a constant.
43    poly_modulation_id: Option<u32>,
44}
45
46impl Display for BoolParam {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match (self.value(), &self.info.value_to_string) {
49            (v, Some(func)) => write!(f, "{}", func(v)),
50            (true, None) => write!(f, "On"),
51            (false, None) => write!(f, "Off"),
52        }
53    }
54}
55
56impl Debug for BoolParam {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        // This uses the above `Display` instance to show the value
59        if self.value.load(Ordering::Relaxed) != self.unmodulated_value.load(Ordering::Relaxed) {
60            write!(f, "{}: {} (modulated)", self.info.name, self)
61        } else {
62            write!(f, "{}: {}", self.info.name, self)
63        }
64    }
65}
66
67// `Params` can not be implemented outside of nice-plug itself because `ParamPtr` is also closed
68impl super::Sealed for BoolParam {}
69
70impl Param for BoolParam {
71    type Plain = bool;
72
73    fn name(&self) -> &str {
74        &self.info.name
75    }
76
77    fn unit(&self) -> &'static str {
78        self.info.unit
79    }
80
81    fn poly_modulation_id(&self) -> Option<u32> {
82        self.poly_modulation_id
83    }
84
85    #[inline]
86    fn modulated_plain_value(&self) -> Self::Plain {
87        self.value.load(Ordering::Relaxed)
88    }
89
90    #[inline]
91    fn modulated_normalized_value(&self) -> f32 {
92        self.normalized_value.load(Ordering::Relaxed)
93    }
94
95    #[inline]
96    fn unmodulated_plain_value(&self) -> Self::Plain {
97        self.unmodulated_value.load(Ordering::Relaxed)
98    }
99
100    #[inline]
101    fn unmodulated_normalized_value(&self) -> f32 {
102        self.unmodulated_normalized_value.load(Ordering::Relaxed)
103    }
104
105    #[inline]
106    fn default_plain_value(&self) -> Self::Plain {
107        self.default
108    }
109
110    fn step_count(&self) -> Option<usize> {
111        Some(1)
112    }
113
114    fn previous_step(&self, _from: Self::Plain, _finer: bool) -> Self::Plain {
115        false
116    }
117
118    fn next_step(&self, _from: Self::Plain, _finer: bool) -> Self::Plain {
119        true
120    }
121
122    fn normalized_value_to_string(&self, normalized: f32, _include_unit: bool) -> String {
123        let value = self.preview_plain(normalized);
124        match (value, &self.info.value_to_string) {
125            (v, Some(f)) => f(v),
126            (true, None) => String::from("On"),
127            (false, None) => String::from("Off"),
128        }
129    }
130
131    fn string_to_normalized_value(&self, string: &str) -> Option<f32> {
132        let string = string.trim();
133        let value = match &self.info.string_to_value {
134            Some(f) => f(string),
135            None => Some(string.eq_ignore_ascii_case("true") || string.eq_ignore_ascii_case("on")),
136        }?;
137
138        Some(self.preview_normalized(value))
139    }
140
141    #[inline]
142    fn preview_normalized(&self, plain: Self::Plain) -> f32 {
143        if plain { 1.0 } else { 0.0 }
144    }
145
146    #[inline]
147    fn preview_plain(&self, normalized: f32) -> Self::Plain {
148        normalized > 0.5
149    }
150
151    fn flags(&self) -> ParamFlags {
152        self.info.flags
153    }
154
155    fn as_ptr(&self) -> ParamPtr {
156        ParamPtr::BoolParam(self as *const BoolParam as *mut BoolParam)
157    }
158}
159
160impl InternalParamMut for BoolParam {
161    unsafe fn _internal_set_plain_value(&self, plain: Self::Plain) -> bool {
162        let unmodulated_value = plain;
163        let unmodulated_normalized_value = self.preview_normalized(plain);
164
165        let modulation_offset = self.modulation_offset.load(Ordering::Relaxed);
166        let (value, normalized_value) = if modulation_offset == 0.0 {
167            (unmodulated_value, unmodulated_normalized_value)
168        } else {
169            let normalized_value =
170                (unmodulated_normalized_value + modulation_offset).clamp(0.0, 1.0);
171
172            (self.preview_plain(normalized_value), normalized_value)
173        };
174
175        // REAPER spams automation events with the same value. This prevents callbacks from firing
176        // multiple times. This can be problematic when they're used to trigger expensive
177        // computations when a parameter changes.
178        let old_value = self.value.swap(value, Ordering::Relaxed);
179        if value != old_value {
180            self.normalized_value
181                .store(normalized_value, Ordering::Relaxed);
182            self.unmodulated_value
183                .store(unmodulated_value, Ordering::Relaxed);
184            self.unmodulated_normalized_value
185                .store(unmodulated_normalized_value, Ordering::Relaxed);
186            if let Some(f) = &self.value_changed {
187                f(value);
188            }
189
190            true
191        } else {
192            false
193        }
194    }
195
196    unsafe fn _internal_set_normalized_value(&self, normalized: f32) -> bool {
197        // NOTE: The double conversion here is to make sure the state is reproducible. State is
198        //       saved and restored using plain values, and the new normalized value will be
199        //       different from `normalized`. This is not necessary for the modulation as these
200        //       values are never shown to the host.
201        unsafe { self._internal_set_plain_value(self.preview_plain(normalized)) }
202    }
203
204    unsafe fn _internal_modulate_value(&self, modulation_offset: f32) -> bool {
205        self.modulation_offset
206            .store(modulation_offset, Ordering::Relaxed);
207
208        // TODO: This renormalizes this value, which is not necessary
209        unsafe { self._internal_set_plain_value(self.unmodulated_plain_value()) }
210    }
211
212    unsafe fn _internal_update_smoother(&self, _sample_rate: f32, _init: bool) {
213        // Can't really smooth a binary parameter now can you
214    }
215}
216
217impl BoolParam {
218    /// Build a new [`BoolParam`]. Use the other associated functions to modify the behavior of the
219    /// parameter.
220    pub fn new(name: impl Into<String>, default: bool) -> Self {
221        Self {
222            value: AtomicBool::new(default),
223            normalized_value: AtomicF32::new(if default { 1.0 } else { 0.0 }),
224            unmodulated_value: AtomicBool::new(default),
225            unmodulated_normalized_value: AtomicF32::new(if default { 1.0 } else { 0.0 }),
226            modulation_offset: AtomicF32::new(0.0),
227            default,
228
229            value_changed: None,
230
231            info: Box::new(ParamInfo::new(name)),
232            poly_modulation_id: None,
233        }
234    }
235
236    /// The field's current plain value, after monophonic modulation has been applied. Equivalent to
237    /// calling `param.plain_value()`.
238    #[inline]
239    pub fn value(&self) -> bool {
240        self.modulated_plain_value()
241    }
242
243    /// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
244    /// parameter in [`NoteEvent::PolyModulation`][crate::midi::NoteEvent::PolyModulation]
245    /// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
246    /// event's documentation on how to use polyphonic modulation. Also consider configuring the
247    /// `ClapPlugin::CLAP_POLY_MODULATION_CONFIG` constant when enabling this.
248    ///
249    /// # Important
250    ///
251    /// After enabling polyphonic modulation, the plugin **must** start sending
252    /// [`NoteEvent::VoiceTerminated`][crate::midi::NoteEvent::VoiceTerminated] events to the
253    /// host when a voice has fully ended. This allows the host to reuse its modulation resources.
254    pub fn with_poly_modulation_id(mut self, id: u32) -> Self {
255        self.poly_modulation_id = Some(id);
256        self
257    }
258
259    /// Run a callback whenever this parameter's value changes. The argument passed to this function
260    /// is the parameter's new value. This should not do anything expensive as it may be called
261    /// multiple times in rapid succession, and it can be run from both the GUI and the audio
262    /// thread.
263    pub fn with_callback(mut self, callback: Arc<dyn Fn(bool) + Send + Sync>) -> Self {
264        self.value_changed = Some(callback);
265        self
266    }
267
268    /// Use a custom conversion function to convert the boolean value to a string.
269    pub fn with_value_to_string(
270        mut self,
271        callback: Arc<dyn Fn(bool) -> String + Send + Sync>,
272    ) -> Self {
273        self.info.value_to_string = Some(callback);
274        self
275    }
276
277    /// Use a custom conversion function to convert from a string to a boolean value. If the string
278    /// cannot be parsed, then this should return a `None`. If this happens while the parameter is
279    /// being updated then the update will be canceled.
280    pub fn with_string_to_value(
281        mut self,
282        callback: Arc<dyn Fn(&str) -> Option<bool> + Send + Sync>,
283    ) -> Self {
284        self.info.string_to_value = Some(callback);
285        self
286    }
287
288    /// Mark this parameter as a bypass parameter. Plugin hosts can integrate this parameter into
289    /// their UI. Only a single [`BoolParam`] can be a bypass parameter, and nice-plug will add one
290    /// if you don't create one yourself. You will need to implement this yourself if your plugin
291    /// introduces latency.
292    pub fn make_bypass(mut self) -> Self {
293        self.info.flags.insert(ParamFlags::BYPASS);
294        self
295    }
296
297    /// Mark the parameter as non-automatable. This means that the parameter cannot be changed from
298    /// an automation lane. The parameter can however still be manually changed by the user from
299    /// either the plugin's own GUI or from the host's generic UI.
300    pub fn non_automatable(mut self) -> Self {
301        self.info.flags.insert(ParamFlags::NON_AUTOMATABLE);
302        self
303    }
304
305    /// Hide the parameter in the host's generic UI for this plugin. This also implies
306    /// `NON_AUTOMATABLE`. Setting this does not prevent you from changing the parameter in the
307    /// plugin's editor GUI.
308    pub fn hide(mut self) -> Self {
309        self.info.flags.insert(ParamFlags::HIDDEN);
310        self
311    }
312
313    /// Don't show this parameter when generating a generic UI for the plugin using one of
314    /// nice-plug's generic UI widgets.
315    pub fn hide_in_generic_ui(mut self) -> Self {
316        self.info.flags.insert(ParamFlags::HIDE_IN_GENERIC_UI);
317        self
318    }
319}