Skip to main content

vst3_host/
parameters.rs

1//! Parameter types and utilities for VST3 host
2
3use crate::Result;
4use serde::{Deserialize, Serialize};
5
6/// Plugin parameter information
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Parameter {
9    /// Parameter ID
10    pub id: u32,
11    /// Parameter name
12    pub name: String,
13    /// Current normalized value (0.0 to 1.0)
14    pub value: f64,
15    /// Minimum value in normalized space (VST3 parameters are always 0.0..=1.0;
16    /// the plain/engineering range is private to the plugin — use
17    /// [`crate::Plugin::format_parameter`] for human-readable values).
18    pub min: f64,
19    /// Maximum value in normalized space (always 1.0 for VST3 parameters).
20    pub max: f64,
21    /// Default value
22    pub default: f64,
23    /// Parameter unit (e.g., "Hz", "dB", "%")
24    pub unit: String,
25    /// Step count, straight from VST3 `ParameterInfo::stepCount`, which counts the *gaps* between
26    /// discrete values rather than the values themselves: `0` is continuous, `1` is a two-state
27    /// toggle, and `n` is a list of `n + 1` values. So a three-way selector reports `2`, not `3`.
28    pub step_count: i32,
29    /// Whether the parameter can be automated
30    pub can_automate: bool,
31    /// Whether the parameter is read-only
32    pub is_read_only: bool,
33    /// Whether the parameter is a bypass control
34    pub is_bypass: bool,
35    /// Parameter flags
36    pub flags: u32,
37}
38
39impl Parameter {
40    /// Convert normalized value (0.0-1.0) to plain value
41    pub fn normalized_to_plain(&self, normalized: f64) -> f64 {
42        if self.step_count >= 1 {
43            let step = self.step_index(normalized).unwrap_or_default() as f64;
44            self.min + (step / self.step_count as f64) * (self.max - self.min)
45        } else {
46            // Continuous parameter
47            self.min + normalized * (self.max - self.min)
48        }
49    }
50
51    /// Which discrete value a normalized position selects, for a stepped parameter: `0..=step_count`
52    /// (so `step_count + 1` possible values). `None` for a continuous parameter.
53    pub fn step_index(&self, normalized: f64) -> Option<i32> {
54        if self.step_count >= 1 {
55            let value = normalized.clamp(0.0, 1.0);
56            // `step_count` comes straight from the plugin, so the "+1 values" arithmetic is done
57            // in `f64` — `step_count + 1` on an `i32` overflows for a plugin that reports
58            // `i32::MAX`. The float-to-int cast saturates, and `min` puts it back in range.
59            Some(((value * (f64::from(self.step_count) + 1.0)) as i32).min(self.step_count))
60        } else {
61            None
62        }
63    }
64
65    /// Convert plain value to normalized value (0.0-1.0)
66    pub fn plain_to_normalized(&self, plain: f64) -> f64 {
67        if (self.max - self.min).abs() < f64::EPSILON {
68            0.0
69        } else {
70            ((plain - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
71        }
72    }
73
74    /// Approximate a human-readable value string from normalized space.
75    ///
76    /// This cannot know the plugin's internal mapping (VST3 keeps that private), so
77    /// for continuous parameters it just reports the normalized number with the unit.
78    /// For accurate display (e.g. `"440.00 Hz"`), use
79    /// [`crate::Plugin::format_parameter`], which asks the plugin to format it.
80    pub fn format_value(&self, normalized: f64) -> String {
81        match self.step_index(normalized) {
82            // Toggle: one step, so two states.
83            Some(index) if self.step_count == 1 => {
84                if index >= 1 { "On" } else { "Off" }.to_string()
85            }
86            // Stepped: report which value is selected. The plain value lives in normalized space
87            // (VST3 keeps the engineering range private), so the index is the only meaningful
88            // number to show — use `Plugin::format_parameter` for the plugin's own label.
89            Some(index) => {
90                if self.unit.is_empty() {
91                    format!("{index}")
92                } else {
93                    format!("{} {}", index, self.unit)
94                }
95            }
96            None => {
97                let plain = self.normalized_to_plain(normalized);
98                if self.unit.is_empty() {
99                    format!("{plain:.3}")
100                } else {
101                    format!("{:.3} {}", plain, self.unit)
102                }
103            }
104        }
105    }
106
107    /// Whether this parameter takes discrete steps rather than a continuous range.
108    ///
109    /// True for toggles too — a toggle is just the one-step case. See [`Self::step_count`] for the
110    /// VST3 counting convention.
111    pub fn is_discrete(&self) -> bool {
112        self.step_count >= 1
113    }
114
115    /// Whether this parameter is a two-state toggle (VST3 `stepCount == 1`).
116    pub fn is_boolean(&self) -> bool {
117        self.step_count == 1
118    }
119}
120
121/// Parameter change event
122#[derive(Debug, Clone)]
123pub struct ParameterChange {
124    /// Parameter ID
125    pub id: u32,
126    /// New normalized value (0.0 to 1.0)
127    pub value: f64,
128    /// Sample offset within the current block
129    pub sample_offset: i32,
130}
131
132/// Batch parameter update
133pub struct ParameterUpdate<'a> {
134    updates: Vec<(u32, f64)>,
135    plugin: &'a mut crate::Plugin,
136}
137
138impl<'a> ParameterUpdate<'a> {
139    pub(crate) fn new(plugin: &'a mut crate::Plugin) -> Self {
140        Self {
141            updates: Vec::new(),
142            plugin,
143        }
144    }
145
146    /// Set a parameter value
147    pub fn set(&mut self, id: u32, value: f64) -> &mut Self {
148        self.updates.push((id, value));
149        self
150    }
151
152    /// Apply the queued parameter updates, in the order they were [`set`](Self::set).
153    ///
154    /// # This batch is not atomic
155    ///
156    /// The first failure stops the batch and is returned. The updates queued *before* it have
157    /// already reached the plugin and are **not** rolled back; the ones after it were never
158    /// attempted, and the error does not say how far the batch got. Call
159    /// [`Plugin::set_parameter`](crate::Plugin::set_parameter) per parameter if you need to
160    /// know which landed, or re-read them with
161    /// [`Plugin::get_parameters`](crate::Plugin::get_parameters) afterwards.
162    pub fn apply(self) -> Result<()> {
163        for (id, value) in self.updates {
164            self.plugin.set_parameter(id, value)?;
165        }
166        Ok(())
167    }
168}
169
170/// Parameter automation curve types
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum AutomationCurve {
173    /// Linear interpolation
174    Linear,
175    /// Exponential curve
176    Exponential,
177    /// Logarithmic curve
178    Logarithmic,
179    /// Step (no interpolation)
180    Step,
181}
182
183/// Parameter automation point
184#[derive(Debug, Clone)]
185pub struct AutomationPoint {
186    /// Time in seconds
187    pub time: f64,
188    /// Normalized value (0.0 to 1.0)
189    pub value: f64,
190    /// Curve type to next point
191    pub curve: AutomationCurve,
192}
193
194/// Parameter automation data
195#[derive(Debug, Clone)]
196pub struct ParameterAutomation {
197    /// Automation points
198    pub points: Vec<AutomationPoint>,
199    /// Whether to loop the automation
200    pub looping: bool,
201}
202
203impl ParameterAutomation {
204    /// Create new automation
205    pub fn new() -> Self {
206        Self {
207            points: Vec::new(),
208            looping: false,
209        }
210    }
211
212    /// Add an automation point
213    pub fn add_point(mut self, time: f64, value: f64) -> Self {
214        self.points.push(AutomationPoint {
215            time,
216            value,
217            curve: AutomationCurve::Linear,
218        });
219        // `total_cmp` orders NaN deterministically instead of panicking like
220        // `partial_cmp(..).unwrap()` would on a NaN time from this public API.
221        self.points.sort_by(|a, b| a.time.total_cmp(&b.time));
222        self
223    }
224
225    /// Set the curve type
226    pub fn with_curve(mut self, curve: AutomationCurve) -> Self {
227        for point in &mut self.points {
228            point.curve = curve;
229        }
230        self
231    }
232
233    /// Enable looping
234    pub fn with_loop(mut self, looping: bool) -> Self {
235        self.looping = looping;
236        self
237    }
238
239    /// Get value at specific time
240    pub fn value_at_time(&self, time: f64) -> Option<f64> {
241        if self.points.is_empty() {
242            return None;
243        }
244
245        // Handle looping
246        let time = if self.looping && !self.points.is_empty() {
247            let duration = self.points.last().unwrap().time;
248            if duration > 0.0 {
249                time % duration
250            } else {
251                time
252            }
253        } else {
254            time
255        };
256
257        // Find surrounding points
258        let mut prev = None;
259        let mut next = None;
260
261        for (i, point) in self.points.iter().enumerate() {
262            if point.time <= time {
263                prev = Some(i);
264            } else {
265                next = Some(i);
266                break;
267            }
268        }
269
270        // Every branch clamps to the normalized range and maps non-finite to a usable value.
271        // `add_point` accepts any `f64`, and the consumers don't tolerate out-of-range input:
272        // `Plugin::set_parameter_at` rejects it, and `Timeline::drive_block` propagates that
273        // rejection *before* processing audio — so one bad automation point stopped rendering
274        // entirely, and the block's MIDI (already consumed by `advance_block`) was lost with it.
275        fn sanitize(value: f64) -> f64 {
276            if value.is_finite() {
277                value.clamp(0.0, 1.0)
278            } else {
279                0.0
280            }
281        }
282
283        match (prev, next) {
284            (None, _) => Some(sanitize(self.points[0].value)),
285            (Some(i), None) => Some(sanitize(self.points[i].value)),
286            (Some(i), Some(j)) => {
287                let p1 = &self.points[i];
288                let p2 = &self.points[j];
289
290                let t = (time - p1.time) / (p2.time - p1.time);
291
292                let value = match p1.curve {
293                    AutomationCurve::Linear => p1.value + (p2.value - p1.value) * t,
294                    AutomationCurve::Exponential => p1.value + (p2.value - p1.value) * t * t,
295                    AutomationCurve::Logarithmic => p1.value + (p2.value - p1.value) * t.sqrt(),
296                    AutomationCurve::Step => p1.value,
297                };
298
299                Some(sanitize(value))
300            }
301        }
302    }
303
304    /// Sample this automation across one audio block, returning `(sample_offset, value)`
305    /// points suitable for sample-accurate scheduling (e.g. [`Plugin::set_parameter_at`]).
306    ///
307    /// `block_start_secs` is the block's start on the automation timeline; `frames` is the
308    /// block length; `points_per_block` is the sub-block resolution (1 = one value at the
309    /// block start; higher = finer ramps, capped at `frames`). Returns empty if the
310    /// automation has no points.
311    ///
312    /// [`Plugin::set_parameter_at`]: crate::Plugin::set_parameter_at
313    pub fn points_for_block(
314        &self,
315        block_start_secs: f64,
316        frames: usize,
317        sample_rate: f64,
318        points_per_block: usize,
319    ) -> Vec<(i32, f64)> {
320        if self.points.is_empty() || frames == 0 {
321            return Vec::new();
322        }
323        let n = points_per_block.clamp(1, frames);
324        let mut out = Vec::with_capacity(n);
325        for i in 0..n {
326            // Widened: `i * frames` overflows `usize` for an absurd `frames`, and this is
327            // reachable from `Timeline::advance_block`'s caller-supplied block length.
328            let offset = ((i as u128 * frames as u128) / n as u128) as usize;
329            let time = block_start_secs + offset as f64 / sample_rate;
330            if let Some(value) = self.value_at_time(time) {
331                // Saturating: a `frames` past `i32::MAX` wraps into a negative sample offset,
332                // which `Plugin::set_parameter_at` would carry into the plugin's event list.
333                out.push((offset.min(i32::MAX as usize) as i32, value));
334            }
335        }
336        out
337    }
338}
339
340impl Default for ParameterAutomation {
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    /// Every branch of `value_at_time` must return a usable normalized value. Only the
351    /// interpolating branch clamped, so a curve whose first or last point was out of range handed
352    /// that raw value straight to `Plugin::set_parameter_at`, which rejects it — and
353    /// `Timeline::drive_block` propagates the rejection *before* processing audio, so rendering
354    /// stopped dead once the playhead reached the last point (losing that block's MIDI too).
355    #[test]
356    fn value_at_time_is_always_a_valid_normalized_value() {
357        let a = ParameterAutomation::new()
358            .add_point(0.0, 99.0)
359            .add_point(1.0, -50.0);
360        // Before the first point, exactly on it, between, on the last, and past it.
361        for t in [-1.0, 0.0, 0.5, 1.0, 2.0] {
362            let v = a.value_at_time(t).expect("some value");
363            assert!(
364                (0.0..=1.0).contains(&v),
365                "value_at_time({t}) = {v}, outside 0..=1"
366            );
367        }
368
369        // A NaN point time must not produce NaN values either.
370        let b = ParameterAutomation::new()
371            .add_point(0.0, 0.0)
372            .add_point(f64::NAN, 1.0);
373        for t in [0.0, 0.5, 1.0] {
374            let v = b.value_at_time(t).expect("some value");
375            assert!(v.is_finite(), "value_at_time({t}) = {v}");
376            assert!((0.0..=1.0).contains(&v), "value_at_time({t}) = {v}");
377        }
378
379        // And the block sampler inherits it.
380        for (offset, value) in b.points_for_block(0.0, 2, 1.0, 2) {
381            assert!(
382                value.is_finite() && (0.0..=1.0).contains(&value),
383                "{offset} -> {value}"
384            );
385        }
386    }
387
388    /// `frames` reaches here from `Timeline::advance_block`, so the sub-block offset maths must
389    /// not overflow on an absurd block length — and the offsets it emits must stay a valid
390    /// sample offset, not a value wrapped negative by the `as i32` cast.
391    #[test]
392    fn points_for_block_survives_an_absurd_block_length() {
393        let a = ParameterAutomation::new()
394            .add_point(0.0, 0.0)
395            .add_point(1.0, 1.0);
396        let points = a.points_for_block(0.0, usize::MAX, 48_000.0, 4);
397        assert!(points.iter().all(|(_, v)| v.is_finite()));
398        assert!(
399            points.iter().all(|(offset, _)| *offset >= 0),
400            "offsets wrapped negative: {points:?}"
401        );
402        // The offsets are still ordered, saturating at the largest offset VST3 can express.
403        assert!(points.windows(2).all(|w| w[0].0 <= w[1].0));
404        assert_eq!(points.last().map(|(offset, _)| *offset), Some(i32::MAX));
405    }
406
407    #[test]
408    fn add_point_with_nan_time_does_not_panic() {
409        // A NaN time is ordered deterministically by `total_cmp` in the sort, never panicking.
410        let auto = ParameterAutomation::new()
411            .add_point(0.0, 0.1)
412            .add_point(f64::NAN, 0.5)
413            .add_point(1.0, 0.9);
414        assert_eq!(auto.points.len(), 3);
415
416        // Sane ordering: the finite points keep ascending-time order, and the NaN is placed
417        // deterministically (`total_cmp` sorts a positive NaN after all finite values) rather
418        // than corrupting the sequence or panicking.
419        let finite: Vec<f64> = auto
420            .points
421            .iter()
422            .map(|p| p.time)
423            .filter(|t| t.is_finite())
424            .collect();
425        assert_eq!(finite, vec![0.0, 1.0]);
426        assert!(auto.points.last().unwrap().time.is_nan());
427    }
428
429    fn stepped(step_count: i32) -> Parameter {
430        Parameter {
431            id: 0,
432            name: "stepped".to_string(),
433            value: 0.0,
434            min: 0.0,
435            max: 1.0,
436            default: 0.0,
437            unit: String::new(),
438            step_count,
439            can_automate: true,
440            is_read_only: false,
441            is_bypass: false,
442            flags: 0,
443        }
444    }
445
446    /// `step_count` is whatever the plugin reported, so the `+ 1` for "step_count + 1 values"
447    /// has to survive `i32::MAX` — in `i32` that arithmetic overflows and panics in debug.
448    #[test]
449    fn step_index_survives_an_absurd_step_count() {
450        let param = stepped(i32::MAX);
451        for normalized in [0.0, 0.5, 1.0] {
452            let index = param.step_index(normalized).expect("stepped");
453            assert!(
454                (0..=i32::MAX).contains(&index),
455                "step_index({normalized}) = {index}"
456            );
457        }
458        assert_eq!(param.step_index(1.0), Some(i32::MAX));
459        // And `format_value`, which goes through the same maths, still renders.
460        assert!(!param.format_value(1.0).is_empty());
461    }
462
463    #[test]
464    fn step_index_covers_every_value_of_an_ordinary_stepped_parameter() {
465        let param = stepped(2); // three values
466        assert_eq!(param.step_index(0.0), Some(0));
467        assert_eq!(param.step_index(0.5), Some(1));
468        assert_eq!(param.step_index(1.0), Some(2));
469        assert_eq!(stepped(0).step_index(0.5), None);
470    }
471
472    #[test]
473    fn add_point_with_nan_value_is_not_used_in_ordering() {
474        // The sort keys on time only, so a NaN *value* can never reach the comparator and
475        // can never break ordering or panic. Lock that in.
476        let auto = ParameterAutomation::new()
477            .add_point(2.0, f64::NAN)
478            .add_point(1.0, 0.5)
479            .add_point(0.0, 0.25);
480        let times: Vec<f64> = auto.points.iter().map(|p| p.time).collect();
481        assert_eq!(times, vec![0.0, 1.0, 2.0]);
482    }
483}