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