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}