Skip to main content

sim_lib_plugin_lv2/
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 LV2 port class of an [`Lv2Port`], mapped onto a graph [`PortMedia`].
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Lv2PortKind {
10    /// An LV2 audio port, mapped to [`PortMedia::Audio`].
11    Audio,
12    /// An LV2 control port, mapped to [`PortMedia::Control`].
13    Control,
14    /// An LV2 atom-sequence (event) port, mapped to [`PortMedia::Event`].
15    AtomSequence,
16}
17
18impl Lv2PortKind {
19    fn media(self) -> PortMedia {
20        match self {
21            Self::Audio => PortMedia::Audio,
22            Self::Control => PortMedia::Control,
23            Self::AtomSequence => PortMedia::Event,
24        }
25    }
26}
27
28/// One declared port of an [`Lv2PluginDescriptor`].
29///
30/// Carries the LV2 port metadata and lowers to a graph [`PortDecl`] via
31/// [`Lv2Port::to_port_decl`].
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct Lv2Port {
34    /// The LV2 port index within its plugin.
35    pub index: u32,
36    /// The LV2 port symbol, used as the graph port name.
37    pub symbol: String,
38    /// The human-readable port name.
39    pub name: String,
40    /// The LV2 port class.
41    pub kind: Lv2PortKind,
42    /// The signal direction (input or output).
43    pub dir: PortDir,
44    /// The number of graph lanes (channels) this port exposes.
45    pub channels: u16,
46}
47
48impl Lv2Port {
49    /// Builds a port from its fields, validating symbol, name, and channels.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`Error::Eval`] when `symbol` or `name` is blank, or when
54    /// `channels` is zero.
55    pub fn new(
56        index: u32,
57        symbol: impl Into<String>,
58        name: impl Into<String>,
59        kind: Lv2PortKind,
60        dir: PortDir,
61        channels: u16,
62    ) -> Result<Self> {
63        let symbol = symbol.into();
64        let name = name.into();
65        if symbol.trim().is_empty() {
66            return Err(Error::Eval("LV2 port symbol cannot be empty".to_owned()));
67        }
68        if name.trim().is_empty() {
69            return Err(Error::Eval("LV2 port name cannot be empty".to_owned()));
70        }
71        if channels == 0 {
72            return Err(Error::Eval(format!(
73                "LV2 port {symbol} must expose at least one graph lane"
74            )));
75        }
76        Ok(Self {
77            index,
78            symbol,
79            name,
80            kind,
81            dir,
82            channels,
83        })
84    }
85
86    /// Builds an audio input port named "Audio Input" with `channels` lanes.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`Error::Eval`] when `symbol` is blank or `channels` is zero.
91    pub fn audio_input(index: u32, symbol: impl Into<String>, channels: u16) -> Result<Self> {
92        Self::new(
93            index,
94            symbol,
95            "Audio Input",
96            Lv2PortKind::Audio,
97            PortDir::In,
98            channels,
99        )
100    }
101
102    /// Builds an audio output port named "Audio Output" with `channels` lanes.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`Error::Eval`] when `symbol` is blank or `channels` is zero.
107    pub fn audio_output(index: u32, symbol: impl Into<String>, channels: u16) -> Result<Self> {
108        Self::new(
109            index,
110            symbol,
111            "Audio Output",
112            Lv2PortKind::Audio,
113            PortDir::Out,
114            channels,
115        )
116    }
117
118    /// Builds a single-lane control input port with the given `name`.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`Error::Eval`] when `symbol` or `name` is blank.
123    pub fn control_input(
124        index: u32,
125        symbol: impl Into<String>,
126        name: impl Into<String>,
127    ) -> Result<Self> {
128        Self::new(index, symbol, name, Lv2PortKind::Control, PortDir::In, 1)
129    }
130
131    /// Builds a single-lane atom-sequence input port named "Events In".
132    ///
133    /// # Errors
134    ///
135    /// Returns [`Error::Eval`] when `symbol` is blank.
136    pub fn atom_input(index: u32, symbol: impl Into<String>) -> Result<Self> {
137        Self::new(
138            index,
139            symbol,
140            "Events In",
141            Lv2PortKind::AtomSequence,
142            PortDir::In,
143            1,
144        )
145    }
146
147    /// Lowers this port to a graph [`PortDecl`].
148    pub fn to_port_decl(&self) -> PortDecl {
149        PortDecl::new(
150            self.symbol.clone(),
151            self.kind.media(),
152            self.dir,
153            self.channels,
154        )
155    }
156}
157
158/// An LV2-shaped plugin description that lowers to a core [`PluginDescriptor`].
159///
160/// Collects the LV2 plugin identity, its [`Lv2Port`] declarations, and its
161/// [`ParameterDescriptor`] set, then converts to the format-neutral descriptor
162/// via [`Lv2PluginDescriptor::to_plugin_descriptor`].
163#[derive(Clone, Debug, PartialEq)]
164pub struct Lv2PluginDescriptor {
165    /// The plugin's LV2 URI, used as its stable id.
166    pub uri: String,
167    /// The human-readable plugin name.
168    pub name: String,
169    /// The vendor string (defaults to `"sim"`).
170    pub vendor: String,
171    /// The plugin version (defaults to this crate's package version).
172    pub version: String,
173    /// The declared ports, in index order.
174    pub ports: Vec<Lv2Port>,
175    /// The declared automatable parameters.
176    pub parameters: Vec<ParameterDescriptor>,
177}
178
179impl Lv2PluginDescriptor {
180    /// Builds a descriptor from a `uri` and `name`, with empty ports/parameters.
181    ///
182    /// The vendor defaults to `"sim"` and the version to this crate's package
183    /// version.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`Error::Eval`] when `uri` or `name` is blank.
188    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Result<Self> {
189        let uri = uri.into();
190        let name = name.into();
191        if uri.trim().is_empty() {
192            return Err(Error::Eval("LV2 plugin URI cannot be empty".to_owned()));
193        }
194        if name.trim().is_empty() {
195            return Err(Error::Eval("LV2 plugin name cannot be empty".to_owned()));
196        }
197        Ok(Self {
198            uri,
199            name,
200            vendor: "sim".to_owned(),
201            version: env!("CARGO_PKG_VERSION").to_owned(),
202            ports: Vec::new(),
203            parameters: Vec::new(),
204        })
205    }
206
207    /// Appends `port` and returns the updated descriptor (builder style).
208    pub fn with_port(mut self, port: Lv2Port) -> Self {
209        self.ports.push(port);
210        self
211    }
212
213    /// Appends `parameter` and returns the updated descriptor (builder style).
214    pub fn with_parameter(mut self, parameter: ParameterDescriptor) -> Self {
215        self.parameters.push(parameter);
216        self
217    }
218
219    /// Lowers every declared port to a graph [`PortDecl`], in index order.
220    pub fn port_decls(&self) -> Vec<PortDecl> {
221        self.ports.iter().map(Lv2Port::to_port_decl).collect()
222    }
223
224    /// Converts this LV2 description to a format-neutral [`PluginDescriptor`].
225    ///
226    /// # Errors
227    ///
228    /// Returns an error when the core [`PluginId`] or [`PluginDescriptor`]
229    /// rejects the URI, name, vendor, or version.
230    pub fn to_plugin_descriptor(&self) -> Result<PluginDescriptor> {
231        let mut descriptor = PluginDescriptor::new(
232            PluginId::new(PluginFormat::Lv2, self.uri.clone())?,
233            self.name.clone(),
234            self.vendor.clone(),
235            self.version.clone(),
236        )?;
237        descriptor.ports = self.port_decls();
238        descriptor.parameters = self.parameters.clone();
239        Ok(descriptor)
240    }
241}
242
243/// Builds the built-in stereo gain plugin's LV2 description.
244///
245/// Declares a stereo audio input, a stereo audio output, and a single `gain`
246/// control port backed by a float parameter ranging from 0.0 to 2.0.
247///
248/// # Errors
249///
250/// Returns an error when any port or parameter fails validation.
251pub fn lv2_gain_lv2_descriptor() -> Result<Lv2PluginDescriptor> {
252    Ok(
253        Lv2PluginDescriptor::new("https://sim.dev/lv2/gain", "SIM LV2 Gain")?
254            .with_port(Lv2Port::audio_input(0, "audio-in", 2)?)
255            .with_port(Lv2Port::audio_output(1, "audio-out", 2)?)
256            .with_port(Lv2Port::control_input(2, "gain", "Gain")?)
257            .with_parameter(
258                ParameterDescriptor::new(2, "gain", "Gain", 0.0, 2.0, 1.0)?
259                    .with_kind(ParameterKind::Float),
260            ),
261    )
262}
263
264/// Builds the built-in gain plugin's format-neutral [`PluginDescriptor`].
265///
266/// Convenience over [`lv2_gain_lv2_descriptor`] followed by
267/// [`Lv2PluginDescriptor::to_plugin_descriptor`].
268///
269/// # Errors
270///
271/// Returns an error when the LV2 description or its conversion fails.
272pub fn lv2_gain_descriptor() -> Result<PluginDescriptor> {
273    lv2_gain_lv2_descriptor()?.to_plugin_descriptor()
274}