Skip to main content

sim_lib_plugin_core/
descriptor.rs

1use sim_kernel::{Error, Result};
2use sim_lib_audio_graph_core::{PortDecl, PortDir, PortMedia};
3
4/// The host backend format a plugin is loaded through.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub enum PluginFormat {
7    /// The CLAP plugin format.
8    Clap,
9    /// The LV2 plugin format.
10    Lv2,
11    /// The VST3 plugin format.
12    Vst3,
13    /// A WebAssembly-hosted plugin.
14    Wasm,
15    /// The native SIM plugin format.
16    Sim,
17}
18
19impl PluginFormat {
20    /// Returns the lowercase wire name for this format.
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Self::Clap => "clap",
24            Self::Lv2 => "lv2",
25            Self::Vst3 => "vst3",
26            Self::Wasm => "wasm",
27            Self::Sim => "sim",
28        }
29    }
30}
31
32/// A plugin's stable identity: its format paired with a backend-stable id.
33#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct PluginId {
35    /// The host backend format this id refers to.
36    pub format: PluginFormat,
37    /// The format-stable identifier string for the plugin.
38    pub stable_id: String,
39}
40
41/// A request to load a plugin through a specific backend format.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct PluginLoadSpec {
44    format: PluginFormat,
45    location: String,
46}
47
48impl PluginLoadSpec {
49    /// Builds a plugin load request, rejecting an empty or whitespace-only
50    /// location.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error when `location` is empty after trimming.
55    pub fn new(format: PluginFormat, location: impl Into<String>) -> Result<Self> {
56        let location = location.into();
57        if location.trim().is_empty() {
58            return Err(Error::Eval(
59                "plugin load location cannot be empty".to_owned(),
60            ));
61        }
62        Ok(Self { format, location })
63    }
64
65    /// Returns the backend format requested by this load.
66    pub fn format(&self) -> PluginFormat {
67        self.format
68    }
69
70    /// Returns the backend-specific load location.
71    pub fn location(&self) -> &str {
72        &self.location
73    }
74
75    /// Requires a specific backend format for this load request.
76    ///
77    /// # Errors
78    ///
79    /// Returns a type mismatch when the requested format does not match
80    /// `expected`.
81    pub fn require_format(&self, expected: PluginFormat) -> Result<()> {
82        if self.format == expected {
83            Ok(())
84        } else {
85            Err(Error::TypeMismatch {
86                expected: expected.as_str(),
87                found: self.format.as_str(),
88            })
89        }
90    }
91}
92
93impl PluginId {
94    /// Builds a plugin id, rejecting an empty or whitespace-only stable id.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error when `stable_id` is empty after trimming.
99    pub fn new(format: PluginFormat, stable_id: impl Into<String>) -> Result<Self> {
100        let stable_id = stable_id.into();
101        if stable_id.trim().is_empty() {
102            return Err(Error::Eval("plugin stable id cannot be empty".to_owned()));
103        }
104        Ok(Self { format, stable_id })
105    }
106}
107
108/// The value domain a parameter exposes.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub enum ParameterKind {
111    /// A continuous floating-point parameter.
112    Float,
113    /// A discrete integer-valued parameter.
114    Integer,
115    /// A two-state on/off parameter.
116    Boolean,
117}
118
119/// A single automatable plugin parameter and its value range.
120#[derive(Clone, Debug, PartialEq)]
121pub struct ParameterDescriptor {
122    /// The parameter's numeric id, unique within a plugin.
123    pub id: u32,
124    /// The backend-stable identifier string.
125    pub stable_id: String,
126    /// The human-readable display name.
127    pub name: String,
128    /// The value domain ([`ParameterKind`]).
129    pub kind: ParameterKind,
130    /// The inclusive minimum plain value.
131    pub min: f64,
132    /// The inclusive maximum plain value.
133    pub max: f64,
134    /// The default plain value, clamped into `min..=max`.
135    pub default: f64,
136    /// Whether a host may automate this parameter.
137    pub automatable: bool,
138}
139
140impl ParameterDescriptor {
141    /// Builds a [`ParameterKind::Float`], automatable parameter.
142    ///
143    /// `default` is clamped into `min..=max`.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error when `stable_id` or `name` is empty after trimming, or
148    /// when `min` exceeds `max`.
149    pub fn new(
150        id: u32,
151        stable_id: impl Into<String>,
152        name: impl Into<String>,
153        min: f64,
154        max: f64,
155        default: f64,
156    ) -> Result<Self> {
157        let stable_id = stable_id.into();
158        let name = name.into();
159        if stable_id.trim().is_empty() {
160            return Err(Error::Eval(
161                "parameter stable id cannot be empty".to_owned(),
162            ));
163        }
164        if name.trim().is_empty() {
165            return Err(Error::Eval("parameter name cannot be empty".to_owned()));
166        }
167        if min > max {
168            return Err(Error::Eval(format!(
169                "parameter {stable_id} min {min} exceeds max {max}"
170            )));
171        }
172        Ok(Self {
173            id,
174            stable_id,
175            name,
176            kind: ParameterKind::Float,
177            min,
178            max,
179            default: default.clamp(min, max),
180            automatable: true,
181        })
182    }
183
184    /// Returns the descriptor with its [`ParameterKind`] replaced.
185    pub fn with_kind(mut self, kind: ParameterKind) -> Self {
186        self.kind = kind;
187        self
188    }
189
190    /// Maps a plain value to the normalized `0.0..=1.0` range.
191    ///
192    /// The input is clamped into `min..=max` first; a zero-width range maps to
193    /// `0.0`.
194    pub fn plain_to_normalized(&self, value: f64) -> f64 {
195        if (self.max - self.min).abs() <= f64::EPSILON {
196            return 0.0;
197        }
198        ((value.clamp(self.min, self.max) - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
199    }
200
201    /// Maps a normalized `0.0..=1.0` value back to a plain value in
202    /// `min..=max`.
203    ///
204    /// The input is clamped into `0.0..=1.0` first.
205    pub fn normalized_to_plain(&self, normalized: f64) -> f64 {
206        self.min + normalized.clamp(0.0, 1.0) * (self.max - self.min)
207    }
208}
209
210/// A plugin's full static description: identity, metadata, ports, parameters,
211/// and reported latency.
212#[derive(Clone, Debug, PartialEq)]
213pub struct PluginDescriptor {
214    /// The plugin's stable identity.
215    pub id: PluginId,
216    /// The human-readable plugin name.
217    pub name: String,
218    /// The vendor string.
219    pub vendor: String,
220    /// The version string.
221    pub version: String,
222    /// The plugin's declared audio and event ports.
223    pub ports: Vec<PortDecl>,
224    /// The plugin's automatable parameters.
225    pub parameters: Vec<ParameterDescriptor>,
226    /// The reported processing latency in frames.
227    pub latency_frames: u32,
228}
229
230impl PluginDescriptor {
231    /// Builds a descriptor with no ports, no parameters, and zero latency.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error when `name` is empty after trimming.
236    pub fn new(
237        id: PluginId,
238        name: impl Into<String>,
239        vendor: impl Into<String>,
240        version: impl Into<String>,
241    ) -> Result<Self> {
242        let name = name.into();
243        if name.trim().is_empty() {
244            return Err(Error::Eval("plugin name cannot be empty".to_owned()));
245        }
246        Ok(Self {
247            id,
248            name,
249            vendor: vendor.into(),
250            version: version.into(),
251            ports: Vec::new(),
252            parameters: Vec::new(),
253            latency_frames: 0,
254        })
255    }
256
257    /// Builds a stereo-capable audio-effect descriptor with the standard port
258    /// layout.
259    ///
260    /// Adds `channels`-wide `audio-in`/`audio-out` ports plus single-channel
261    /// `events-in`/`events-out` ports, and sets the vendor to `"sim"` and the
262    /// version to this crate's package version.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error when `stable_id` or `name` is empty after trimming.
267    pub fn audio_effect(
268        format: PluginFormat,
269        stable_id: impl Into<String>,
270        name: impl Into<String>,
271        channels: u16,
272    ) -> Result<Self> {
273        let mut descriptor = Self::new(
274            PluginId::new(format, stable_id)?,
275            name,
276            "sim",
277            env!("CARGO_PKG_VERSION"),
278        )?;
279        descriptor.ports.push(PortDecl::new(
280            "audio-in",
281            PortMedia::Audio,
282            PortDir::In,
283            channels,
284        ));
285        descriptor.ports.push(PortDecl::new(
286            "audio-out",
287            PortMedia::Audio,
288            PortDir::Out,
289            channels,
290        ));
291        descriptor
292            .ports
293            .push(PortDecl::new("events-in", PortMedia::Event, PortDir::In, 1));
294        descriptor.ports.push(PortDecl::new(
295            "events-out",
296            PortMedia::Event,
297            PortDir::Out,
298            1,
299        ));
300        Ok(descriptor)
301    }
302
303    /// Returns the descriptor with `parameter` appended to its parameter list.
304    pub fn with_parameter(mut self, parameter: ParameterDescriptor) -> Self {
305        self.parameters.push(parameter);
306        self
307    }
308
309    /// Returns the parameter whose [`ParameterDescriptor::id`] matches `id`, if
310    /// any.
311    pub fn parameter(&self, id: u32) -> Option<&ParameterDescriptor> {
312        self.parameters.iter().find(|parameter| parameter.id == id)
313    }
314}