Skip to main content

sim_lib_plugin_vst3/
descriptor.rs

1use sim_kernel::{Error, Result};
2use sim_lib_audio_graph_core::{PortDecl, PortDir, PortMedia};
3use sim_lib_plugin_core::{
4    ParameterDescriptor, ParameterKind, PluginDescriptor, PluginFormat, PluginId,
5};
6
7/// The medium carried by a VST3 bus.
8///
9/// Maps onto the graph port media when a [`Vst3Bus`] is lowered to a `PortDecl`.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Vst3BusKind {
12    /// An audio bus carrying sample lanes.
13    Audio,
14    /// An event bus carrying note and control events.
15    Event,
16}
17
18impl Vst3BusKind {
19    fn media(self) -> PortMedia {
20        match self {
21            Self::Audio => PortMedia::Audio,
22            Self::Event => PortMedia::Event,
23        }
24    }
25}
26
27/// A VST3 input or output bus.
28///
29/// Describes one audio or event bus by name, direction, and lane count;
30/// [`to_port_decl`](Vst3Bus::to_port_decl) lowers it into a graph `PortDecl`.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct Vst3Bus {
33    /// The display name of the bus.
34    pub name: String,
35    /// Whether the bus carries audio or events.
36    pub kind: Vst3BusKind,
37    /// The data direction (input or output) of the bus.
38    pub dir: PortDir,
39    /// The number of graph lanes (channels) the bus exposes.
40    pub channels: u16,
41}
42
43impl Vst3Bus {
44    /// Builds a bus, validating that `name` is non-empty and `channels` is
45    /// non-zero.
46    ///
47    /// Returns an error when the name is blank or when no lane is requested.
48    pub fn new(
49        name: impl Into<String>,
50        kind: Vst3BusKind,
51        dir: PortDir,
52        channels: u16,
53    ) -> Result<Self> {
54        let name = name.into();
55        if name.trim().is_empty() {
56            return Err(Error::Eval("VST3 bus name cannot be empty".to_owned()));
57        }
58        if channels == 0 {
59            return Err(Error::Eval(format!(
60                "VST3 bus {name} must expose at least one graph lane"
61            )));
62        }
63        Ok(Self {
64            name,
65            kind,
66            dir,
67            channels,
68        })
69    }
70
71    /// Builds an audio input bus named `name` with `channels` lanes.
72    pub fn audio_input(name: impl Into<String>, channels: u16) -> Result<Self> {
73        Self::new(name, Vst3BusKind::Audio, PortDir::In, channels)
74    }
75
76    /// Builds an audio output bus named `name` with `channels` lanes.
77    pub fn audio_output(name: impl Into<String>, channels: u16) -> Result<Self> {
78        Self::new(name, Vst3BusKind::Audio, PortDir::Out, channels)
79    }
80
81    /// Builds a single-lane event input bus named `name`.
82    pub fn event_input(name: impl Into<String>) -> Result<Self> {
83        Self::new(name, Vst3BusKind::Event, PortDir::In, 1)
84    }
85
86    /// Lowers this bus into a graph `PortDecl` with matching media, direction,
87    /// and lane count.
88    pub fn to_port_decl(&self) -> PortDecl {
89        PortDecl::new(
90            self.name.clone(),
91            self.kind.media(),
92            self.dir,
93            self.channels,
94        )
95    }
96}
97
98/// A VST3 parameter declaration.
99///
100/// Captures the host-facing identity and value range of one automatable
101/// parameter; [`to_parameter_descriptor`](Vst3ParamInfo::to_parameter_descriptor)
102/// lowers it into the shared plugin-core `ParameterDescriptor`.
103#[derive(Clone, Debug, PartialEq)]
104pub struct Vst3ParamInfo {
105    /// The numeric VST3 parameter id.
106    pub id: u32,
107    /// The stable string id used for persistence and lookup.
108    pub stable_id: String,
109    /// The human-readable parameter title.
110    pub title: String,
111    /// The display units, empty when the parameter is unitless.
112    pub units: String,
113    /// The minimum parameter value.
114    pub min: f64,
115    /// The maximum parameter value.
116    pub max: f64,
117    /// The default value, clamped into the `min..=max` range.
118    pub default: f64,
119    /// Whether the host may automate the parameter.
120    pub automatable: bool,
121}
122
123impl Vst3ParamInfo {
124    /// Builds a parameter, validating ids and range.
125    ///
126    /// Requires non-empty `stable_id` and `title` and `min <= max`; `default`
127    /// is clamped into the range, `units` starts empty, and `automatable`
128    /// defaults to `true`. Returns an error when validation fails.
129    pub fn new(
130        id: u32,
131        stable_id: impl Into<String>,
132        title: impl Into<String>,
133        min: f64,
134        max: f64,
135        default: f64,
136    ) -> Result<Self> {
137        let stable_id = stable_id.into();
138        let title = title.into();
139        if stable_id.trim().is_empty() {
140            return Err(Error::Eval(
141                "VST3 parameter stable id cannot be empty".to_owned(),
142            ));
143        }
144        if title.trim().is_empty() {
145            return Err(Error::Eval(
146                "VST3 parameter title cannot be empty".to_owned(),
147            ));
148        }
149        if min > max {
150            return Err(Error::Eval(format!(
151                "VST3 parameter {stable_id} min {min} exceeds max {max}"
152            )));
153        }
154        Ok(Self {
155            id,
156            stable_id,
157            title,
158            units: String::new(),
159            min,
160            max,
161            default: default.clamp(min, max),
162            automatable: true,
163        })
164    }
165
166    /// Lowers this parameter into a plugin-core `ParameterDescriptor` with a
167    /// `Float` kind.
168    pub fn to_parameter_descriptor(&self) -> Result<ParameterDescriptor> {
169        Ok(ParameterDescriptor::new(
170            self.id,
171            self.stable_id.clone(),
172            self.title.clone(),
173            self.min,
174            self.max,
175            self.default,
176        )?
177        .with_kind(ParameterKind::Float))
178    }
179}
180
181/// A VST3-shaped plugin descriptor.
182///
183/// Collects the class identity, bus layout, and parameter set of a VST3 export;
184/// [`to_plugin_descriptor`](Vst3PluginDescriptor::to_plugin_descriptor) lowers
185/// it into the shared plugin-core `PluginDescriptor` consumed by the host.
186#[derive(Clone, Debug, PartialEq)]
187pub struct Vst3PluginDescriptor {
188    /// The VST3 class id identifying the plugin.
189    pub class_id: String,
190    /// The plugin display name.
191    pub name: String,
192    /// The vendor name (defaults to `sim`).
193    pub vendor: String,
194    /// The plugin version (defaults to this crate's package version).
195    pub version: String,
196    /// The declared input and output buses.
197    pub buses: Vec<Vst3Bus>,
198    /// The declared parameters.
199    pub parameters: Vec<Vst3ParamInfo>,
200}
201
202impl Vst3PluginDescriptor {
203    /// Builds an empty descriptor with the given `class_id` and `name`.
204    ///
205    /// Validates that both are non-empty, sets `vendor` to `sim` and `version`
206    /// to this crate's package version, and starts with no buses or parameters.
207    pub fn new(class_id: impl Into<String>, name: impl Into<String>) -> Result<Self> {
208        let class_id = class_id.into();
209        let name = name.into();
210        if class_id.trim().is_empty() {
211            return Err(Error::Eval("VST3 class id cannot be empty".to_owned()));
212        }
213        if name.trim().is_empty() {
214            return Err(Error::Eval("VST3 plugin name cannot be empty".to_owned()));
215        }
216        Ok(Self {
217            class_id,
218            name,
219            vendor: "sim".to_owned(),
220            version: env!("CARGO_PKG_VERSION").to_owned(),
221            buses: Vec::new(),
222            parameters: Vec::new(),
223        })
224    }
225
226    /// Appends `bus` to the descriptor and returns it, for builder chaining.
227    pub fn with_bus(mut self, bus: Vst3Bus) -> Self {
228        self.buses.push(bus);
229        self
230    }
231
232    /// Appends `parameter` to the descriptor and returns it, for builder
233    /// chaining.
234    pub fn with_parameter(mut self, parameter: Vst3ParamInfo) -> Self {
235        self.parameters.push(parameter);
236        self
237    }
238
239    /// Lowers every declared bus into a graph `PortDecl`.
240    pub fn port_decls(&self) -> Vec<PortDecl> {
241        self.buses.iter().map(Vst3Bus::to_port_decl).collect()
242    }
243
244    /// Lowers this descriptor into a plugin-core `PluginDescriptor`.
245    ///
246    /// Builds a `Vst3`-format `PluginId` from the class id and populates the
247    /// resulting descriptor's ports and parameters from this descriptor's buses
248    /// and parameter set. Returns an error if any conversion fails.
249    pub fn to_plugin_descriptor(&self) -> Result<PluginDescriptor> {
250        let mut descriptor = PluginDescriptor::new(
251            PluginId::new(PluginFormat::Vst3, self.class_id.clone())?,
252            self.name.clone(),
253            self.vendor.clone(),
254            self.version.clone(),
255        )?;
256        descriptor.ports = self.port_decls();
257        descriptor.parameters = self
258            .parameters
259            .iter()
260            .map(Vst3ParamInfo::to_parameter_descriptor)
261            .collect::<Result<Vec<_>>>()?;
262        Ok(descriptor)
263    }
264}
265
266/// Builds the built-in VST3 gain fixture descriptor.
267///
268/// Declares stereo audio input and output buses, an event input bus, and a
269/// single `Gain` parameter ranging `0.0..=2.0` with a default of `1.0`.
270pub fn vst3_gain_vst3_descriptor() -> Result<Vst3PluginDescriptor> {
271    Ok(
272        Vst3PluginDescriptor::new("53494d2d4741494e2d56535433000001", "SIM VST3 Gain")?
273            .with_bus(Vst3Bus::audio_input("audio-in", 2)?)
274            .with_bus(Vst3Bus::audio_output("audio-out", 2)?)
275            .with_bus(Vst3Bus::event_input("events-in")?)
276            .with_parameter(Vst3ParamInfo::new(0, "gain", "Gain", 0.0, 2.0, 1.0)?),
277    )
278}
279
280/// Builds the gain fixture as a plugin-core `PluginDescriptor`.
281///
282/// Convenience over [`vst3_gain_vst3_descriptor`] that immediately lowers the
283/// fixture with [`Vst3PluginDescriptor::to_plugin_descriptor`].
284pub fn vst3_gain_descriptor() -> Result<PluginDescriptor> {
285    vst3_gain_vst3_descriptor()?.to_plugin_descriptor()
286}