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 (0 = continuous)
26    pub step_count: i32,
27    /// Whether the parameter can be automated
28    pub can_automate: bool,
29    /// Whether the parameter is read-only
30    pub is_read_only: bool,
31    /// Whether the parameter is a bypass control
32    pub is_bypass: bool,
33    /// Parameter flags
34    pub flags: u32,
35}
36
37impl Parameter {
38    /// Convert normalized value (0.0-1.0) to plain value
39    pub fn normalized_to_plain(&self, normalized: f64) -> f64 {
40        if self.step_count > 1 {
41            // Discrete parameter
42            let steps = self.step_count as f64;
43            let step = (normalized * steps).round();
44            self.min + (step / steps) * (self.max - self.min)
45        } else {
46            // Continuous parameter
47            self.min + normalized * (self.max - self.min)
48        }
49    }
50
51    /// Convert plain value to normalized value (0.0-1.0)
52    pub fn plain_to_normalized(&self, plain: f64) -> f64 {
53        if (self.max - self.min).abs() < f64::EPSILON {
54            0.0
55        } else {
56            ((plain - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
57        }
58    }
59
60    /// Approximate a human-readable value string from normalized space.
61    ///
62    /// This cannot know the plugin's internal mapping (VST3 keeps that private), so
63    /// for continuous parameters it just reports the normalized number with the unit.
64    /// For accurate display (e.g. `"440.00 Hz"`), use
65    /// [`crate::Plugin::format_parameter`], which asks the plugin to format it.
66    pub fn format_value(&self, normalized: f64) -> String {
67        let plain = self.normalized_to_plain(normalized);
68
69        if self.step_count == 2 {
70            // Boolean parameter
71            if plain > 0.5 {
72                "On".to_string()
73            } else {
74                "Off".to_string()
75            }
76        } else if self.step_count > 2 {
77            // Discrete parameter
78            format!("{:.0} {}", plain, self.unit)
79        } else {
80            // Continuous parameter
81            if self.unit.is_empty() {
82                format!("{:.3}", plain)
83            } else {
84                format!("{:.3} {}", plain, self.unit)
85            }
86        }
87    }
88
89    /// Check if this is a discrete/stepped parameter
90    pub fn is_discrete(&self) -> bool {
91        self.step_count > 1
92    }
93
94    /// Check if this is a boolean/switch parameter
95    pub fn is_boolean(&self) -> bool {
96        self.step_count == 2
97    }
98}
99
100/// Parameter change event
101#[derive(Debug, Clone)]
102pub struct ParameterChange {
103    /// Parameter ID
104    pub id: u32,
105    /// New normalized value (0.0 to 1.0)
106    pub value: f64,
107    /// Sample offset within the current block
108    pub sample_offset: i32,
109}
110
111/// Batch parameter update
112pub struct ParameterUpdate<'a> {
113    updates: Vec<(u32, f64)>,
114    plugin: &'a mut crate::Plugin,
115}
116
117impl<'a> ParameterUpdate<'a> {
118    pub(crate) fn new(plugin: &'a mut crate::Plugin) -> Self {
119        Self {
120            updates: Vec::new(),
121            plugin,
122        }
123    }
124
125    /// Set a parameter value
126    pub fn set(&mut self, id: u32, value: f64) -> &mut Self {
127        self.updates.push((id, value));
128        self
129    }
130
131    /// Apply all parameter updates
132    pub fn apply(self) -> Result<()> {
133        for (id, value) in self.updates {
134            self.plugin.set_parameter(id, value)?;
135        }
136        Ok(())
137    }
138}
139
140/// Parameter automation curve types
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum AutomationCurve {
143    /// Linear interpolation
144    Linear,
145    /// Exponential curve
146    Exponential,
147    /// Logarithmic curve
148    Logarithmic,
149    /// Step (no interpolation)
150    Step,
151}
152
153/// Parameter automation point
154#[derive(Debug, Clone)]
155pub struct AutomationPoint {
156    /// Time in seconds
157    pub time: f64,
158    /// Normalized value (0.0 to 1.0)
159    pub value: f64,
160    /// Curve type to next point
161    pub curve: AutomationCurve,
162}
163
164/// Parameter automation data
165#[derive(Debug, Clone)]
166pub struct ParameterAutomation {
167    /// Automation points
168    pub points: Vec<AutomationPoint>,
169    /// Whether to loop the automation
170    pub looping: bool,
171}
172
173impl ParameterAutomation {
174    /// Create new automation
175    pub fn new() -> Self {
176        Self {
177            points: Vec::new(),
178            looping: false,
179        }
180    }
181
182    /// Add an automation point
183    pub fn add_point(mut self, time: f64, value: f64) -> Self {
184        self.points.push(AutomationPoint {
185            time,
186            value,
187            curve: AutomationCurve::Linear,
188        });
189        // `total_cmp` orders NaN deterministically instead of panicking like
190        // `partial_cmp(..).unwrap()` would on a NaN time from this public API.
191        self.points.sort_by(|a, b| a.time.total_cmp(&b.time));
192        self
193    }
194
195    /// Set the curve type
196    pub fn with_curve(mut self, curve: AutomationCurve) -> Self {
197        for point in &mut self.points {
198            point.curve = curve;
199        }
200        self
201    }
202
203    /// Enable looping
204    pub fn with_loop(mut self, looping: bool) -> Self {
205        self.looping = looping;
206        self
207    }
208
209    /// Get value at specific time
210    pub fn value_at_time(&self, time: f64) -> Option<f64> {
211        if self.points.is_empty() {
212            return None;
213        }
214
215        // Handle looping
216        let time = if self.looping && !self.points.is_empty() {
217            let duration = self.points.last().unwrap().time;
218            if duration > 0.0 {
219                time % duration
220            } else {
221                time
222            }
223        } else {
224            time
225        };
226
227        // Find surrounding points
228        let mut prev = None;
229        let mut next = None;
230
231        for (i, point) in self.points.iter().enumerate() {
232            if point.time <= time {
233                prev = Some(i);
234            } else {
235                next = Some(i);
236                break;
237            }
238        }
239
240        match (prev, next) {
241            (None, _) => Some(self.points[0].value),
242            (Some(i), None) => Some(self.points[i].value),
243            (Some(i), Some(j)) => {
244                let p1 = &self.points[i];
245                let p2 = &self.points[j];
246
247                let t = (time - p1.time) / (p2.time - p1.time);
248
249                let value = match p1.curve {
250                    AutomationCurve::Linear => p1.value + (p2.value - p1.value) * t,
251                    AutomationCurve::Exponential => p1.value + (p2.value - p1.value) * t * t,
252                    AutomationCurve::Logarithmic => p1.value + (p2.value - p1.value) * t.sqrt(),
253                    AutomationCurve::Step => p1.value,
254                };
255
256                Some(value.clamp(0.0, 1.0))
257            }
258        }
259    }
260
261    /// Sample this automation across one audio block, returning `(sample_offset, value)`
262    /// points suitable for sample-accurate scheduling (e.g. [`Plugin::set_parameter_at`]).
263    ///
264    /// `block_start_secs` is the block's start on the automation timeline; `frames` is the
265    /// block length; `points_per_block` is the sub-block resolution (1 = one value at the
266    /// block start; higher = finer ramps, capped at `frames`). Returns empty if the
267    /// automation has no points.
268    ///
269    /// [`Plugin::set_parameter_at`]: crate::Plugin::set_parameter_at
270    pub fn points_for_block(
271        &self,
272        block_start_secs: f64,
273        frames: usize,
274        sample_rate: f64,
275        points_per_block: usize,
276    ) -> Vec<(i32, f64)> {
277        if self.points.is_empty() || frames == 0 {
278            return Vec::new();
279        }
280        let n = points_per_block.clamp(1, frames);
281        let mut out = Vec::with_capacity(n);
282        for i in 0..n {
283            let offset = (i * frames) / n;
284            let time = block_start_secs + offset as f64 / sample_rate;
285            if let Some(value) = self.value_at_time(time) {
286                out.push((offset as i32, value));
287            }
288        }
289        out
290    }
291}
292
293impl Default for ParameterAutomation {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn add_point_with_nan_time_does_not_panic() {
305        // A NaN time used to panic via `partial_cmp(..).unwrap()` in the sort; `total_cmp`
306        // orders it deterministically instead.
307        let auto = ParameterAutomation::new()
308            .add_point(0.0, 0.1)
309            .add_point(f64::NAN, 0.5)
310            .add_point(1.0, 0.9);
311        assert_eq!(auto.points.len(), 3);
312    }
313}