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}