conformal_vst_wrapper/lib.rs
1#![doc = include_str!("../docs_boilerplate.md")]
2#![doc = include_str!("../README.md")]
3
4use conformal_component::parameters::UNIQUE_ID_INTERNAL_PREFIX;
5pub use conformal_ui::Size as UiSize;
6use core::slice;
7
8/// Contains information about the host.
9///
10/// You can use this to customize the comonent based on the host.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct HostInfo {
13 /// The name of the host.
14 pub name: String,
15}
16
17/// A class ID for a VST3 component.
18///
19/// This must be _globally_ unique for each class
20pub type ClassID = [u8; 16];
21
22/// A component factory that can create a component.
23///
24/// This can return a specialized component based on information
25/// about the current host
26#[allow(clippy::module_name_repetitions)]
27pub trait ComponentFactory: Clone {
28 /// The type of component that this factory creates
29 type Component;
30
31 /// Create a component
32 fn create(&self, host: &HostInfo) -> Self::Component;
33}
34
35impl<C, F: Fn(&HostInfo) -> C + Clone> ComponentFactory for F {
36 type Component = C;
37 fn create(&self, host_info: &HostInfo) -> C {
38 (self)(host_info)
39 }
40}
41
42/// Information about a VST3 component
43#[derive(Debug, Clone, Copy)]
44pub struct ClassInfo<'a> {
45 /// User-visibile name of the component.
46 pub name: &'a str,
47
48 /// Class ID for the processor component. This is used by the host to identify the VST.
49 pub cid: ClassID,
50
51 /// Class ID for the so-called "edit controller" component. This is arbitrary
52 /// but must be unique.
53 pub edit_controller_cid: ClassID,
54
55 /// Initial size of the UI in logical pixels
56 pub ui_initial_size: UiSize,
57}
58
59#[doc(hidden)]
60pub struct ParameterModel {
61 pub parameter_infos: Box<dyn Fn(&HostInfo) -> Vec<conformal_component::parameters::Info>>,
62}
63
64#[doc(hidden)]
65pub trait ClassCategory {
66 fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase>;
67
68 fn info(&self) -> &ClassInfo<'static>;
69
70 fn category_str(&self) -> &'static str;
71
72 fn create_parameter_model(&self) -> ParameterModel;
73
74 fn get_kind(&self) -> edit_controller::Kind;
75}
76
77/// Information about a synth component
78pub struct SynthClass<CF> {
79 /// The actual factory.
80 pub factory: CF,
81
82 /// Information about the component
83 pub info: ClassInfo<'static>,
84}
85
86fn create_parameter_model_internal<CF: ComponentFactory + 'static>(factory: CF) -> ParameterModel
87where
88 CF::Component: Component,
89{
90 ParameterModel {
91 parameter_infos: Box::new(move |host_info| {
92 let component = factory.create(host_info);
93 component.parameter_infos()
94 }),
95 }
96}
97
98impl<CF: ComponentFactory + 'static> ClassCategory for SynthClass<CF>
99where
100 CF::Component: Component<Processor: Synth> + 'static,
101{
102 fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase> {
103 vst3::ComWrapper::new(processor::create_synth(
104 self.factory.clone(),
105 controller_cid,
106 ))
107 .to_com_ptr::<IPluginBase>()
108 .unwrap()
109 }
110
111 fn create_parameter_model(&self) -> ParameterModel {
112 create_parameter_model_internal(self.factory.clone())
113 }
114
115 fn category_str(&self) -> &'static str {
116 "Instrument|Synth"
117 }
118
119 fn info(&self) -> &ClassInfo<'static> {
120 &self.info
121 }
122
123 fn get_kind(&self) -> edit_controller::Kind {
124 edit_controller::Kind::Synth()
125 }
126}
127
128/// Information about an effect component
129pub struct EffectClass<CF> {
130 /// The actual factory.
131 pub factory: CF,
132
133 /// Information about the component
134 pub info: ClassInfo<'static>,
135
136 /// The VST3 category for this effect
137 /// See [here](https://steinbergmedia.github.io/vst3_doc/vstinterfaces/group__plugType.html)
138 /// for a list of possible categories.
139 pub category: &'static str,
140
141 /// All effects must have a bypass parameter. This is the unique ID for that parameter.
142 pub bypass_id: &'static str,
143}
144
145impl<CF: ComponentFactory<Component: Component<Processor: Effect> + 'static> + 'static>
146 ClassCategory for EffectClass<CF>
147{
148 fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase> {
149 vst3::ComWrapper::new(processor::create_effect(
150 self.factory.clone(),
151 controller_cid,
152 ))
153 .to_com_ptr::<IPluginBase>()
154 .unwrap()
155 }
156
157 fn category_str(&self) -> &'static str {
158 self.category
159 }
160
161 fn info(&self) -> &ClassInfo<'static> {
162 &self.info
163 }
164
165 fn create_parameter_model(&self) -> ParameterModel {
166 create_parameter_model_internal(self.factory.clone())
167 }
168
169 fn get_kind(&self) -> edit_controller::Kind {
170 edit_controller::Kind::Effect {
171 bypass_id: self.bypass_id,
172 }
173 }
174}
175
176/// General global infor about a vst plug-in
177#[derive(Debug, Clone, Copy)]
178pub struct Info<'a> {
179 /// The "vendor" of the plug-in.
180 ///
181 /// Hosts often present plug-ins grouped by vendor.
182 pub vendor: &'a str,
183
184 /// The vendor's URL
185 pub url: &'a str,
186
187 /// The vendor's email
188 pub email: &'a str,
189
190 /// User-visibile version of components in this factory
191 pub version: &'a str,
192}
193
194use conformal_component::Component;
195use conformal_component::effect::Effect;
196use conformal_component::synth::Synth;
197
198use vst3::Steinberg::{IPluginBase, IPluginFactory2, IPluginFactory2Trait};
199use vst3::{Class, Steinberg::IPluginFactory};
200
201mod edit_controller;
202mod factory;
203mod host_info;
204mod io;
205mod mpe_quirks;
206mod parameters;
207mod processor;
208mod view;
209
210#[cfg(test)]
211mod dummy_host;
212
213#[cfg(test)]
214mod fake_ibstream;
215
216#[doc(hidden)]
217pub fn _wrap_factory(
218 classes: &'static [&'static dyn ClassCategory],
219 info: Info<'static>,
220) -> impl Class<Interfaces = (IPluginFactory, IPluginFactory2)> + 'static + IPluginFactory2Trait {
221 factory::Factory::new(classes, info)
222}
223
224fn to_utf16(s: &str, buffer: &mut [u16]) {
225 for (i, c) in s.encode_utf16().chain([0]).enumerate() {
226 buffer[i] = c;
227 }
228}
229
230fn from_utf16_ptr(buffer: *const u16, max_size: usize) -> Option<String> {
231 let mut len = 0;
232 unsafe {
233 while *buffer.add(len) != 0 {
234 if len >= max_size {
235 return None;
236 }
237 len += 1;
238 }
239 }
240 let utf16_slice = unsafe { slice::from_raw_parts(buffer.cast(), len) };
241 String::from_utf16(utf16_slice).ok()
242}
243
244fn from_utf16_buffer(buffer: &[u16]) -> Option<String> {
245 let mut len = 0;
246 for c in buffer {
247 if *c == 0 {
248 break;
249 }
250 len += 1;
251 }
252 let utf16_slice = unsafe { slice::from_raw_parts(buffer.as_ptr().cast(), len) };
253 String::from_utf16(utf16_slice).ok()
254}
255
256fn should_include_parameter_in_snapshot(id: &str) -> bool {
257 !id.starts_with(UNIQUE_ID_INTERNAL_PREFIX)
258 && !conformal_component::synth::CONTROLLER_PARAMETERS
259 .iter()
260 .any(|p| id == p.unique_id)
261}
262
263/// Create a VST3-compatible plug-in entry point.
264///
265/// This is the main entry point for the VST3 Conformal Wrapper, and must
266/// be invoked exactly once in each VST3 plug-in binary.
267///
268/// Note that each VST3 plug-in binary can contain _multiple_ components,
269/// so this takes a slice of `EffectClass` and `SynthClass` instances.
270///
271/// Note that to create a loadable plug-in, you must add this to your
272/// `Cargo.toml`:
273///
274/// ```toml
275/// [lib]
276/// crate-type = ["cdylib"]
277/// ```
278///
279/// Conformal provides a template project that you can use to get started,
280/// using `bun create conformal` script. This will provide a working example
281/// of using the VST3 wrapper.
282///
283/// # Example
284///
285/// ```
286/// use conformal_vst_wrapper::{ClassID, ClassInfo, EffectClass, HostInfo, Info};
287/// use conformal_component::audio::{channels, channels_mut, Buffer, BufferMut};
288/// use conformal_component::effect::Effect as EffectTrait;
289/// use conformal_component::parameters::{self, BufferStates, Flags, InfoRef, TypeSpecificInfoRef};
290/// use conformal_component::pzip;
291/// use conformal_component::{Component as ComponentTrait, ProcessingEnvironment, Processor};
292///
293/// const PARAMETERS: [InfoRef<'static, &'static str>; 2] = [
294/// InfoRef {
295/// title: "Bypass",
296/// short_title: "Bypass",
297/// unique_id: "bypass",
298/// flags: Flags { automatable: true },
299/// type_specific: TypeSpecificInfoRef::Switch { default: false },
300/// },
301/// InfoRef {
302/// title: "Gain",
303/// short_title: "Gain",
304/// unique_id: "gain",
305/// flags: Flags { automatable: true },
306/// type_specific: TypeSpecificInfoRef::Numeric {
307/// default: 100.,
308/// valid_range: 0f32..=100.,
309/// units: Some("%"),
310/// },
311/// },
312/// ];
313///
314/// #[derive(Clone, Debug, Default)]
315/// pub struct Component {}
316///
317/// #[derive(Clone, Debug, Default)]
318/// pub struct Effect {}
319///
320/// impl Processor for Effect {
321/// fn set_processing(&mut self, _processing: bool) {}
322/// }
323///
324/// impl EffectTrait for Effect {
325/// fn handle_parameters<P: parameters::States>(&mut self, _: P) {}
326/// fn process<P: BufferStates, I: Buffer, O: BufferMut>(
327/// &mut self,
328/// parameters: P,
329/// input: &I,
330/// output: &mut O,
331/// ) {
332/// for (input_channel, output_channel) in channels(input).zip(channels_mut(output)) {
333/// for ((input_sample, output_sample), (gain, bypass)) in input_channel
334/// .iter()
335/// .zip(output_channel.iter_mut())
336/// .zip(pzip!(parameters[numeric "gain", switch "bypass"]))
337/// {
338/// *output_sample = *input_sample * (if bypass { 1.0 } else { gain / 100.0 });
339/// }
340/// }
341/// }
342/// }
343///
344/// impl ComponentTrait for Component {
345/// type Processor = Effect;
346///
347/// fn parameter_infos(&self) -> Vec<parameters::Info> {
348/// parameters::to_infos(&PARAMETERS)
349/// }
350///
351/// fn create_processor(&self, _env: &ProcessingEnvironment) -> Self::Processor {
352/// Default::default()
353/// }
354/// }
355///
356/// // DO NOT USE this class ID, rather generate your own globally unique one.
357/// const CID: ClassID = [
358/// 0x1d, 0x33, 0x78, 0xb8, 0xbd, 0xc9, 0x40, 0x8d, 0x86, 0x1f, 0xaf, 0xa4, 0xb5, 0x42, 0x5b, 0x74
359/// ];
360///
361/// // DO NOT USE this class ID, rather generate your own globally unique one.
362/// const EDIT_CONTROLLER_CID: ClassID = [
363/// 0x96, 0xa6, 0xd4, 0x7d, 0xb2, 0x73, 0x46, 0x7c, 0xb0, 0xd6, 0xea, 0x6a, 0xd0, 0x27, 0xb2, 0x6f
364/// ];
365///
366/// conformal_vst_wrapper::wrap_factory!(
367/// &const {
368/// [&EffectClass {
369/// info: ClassInfo {
370/// name: "My effect",
371/// cid: CID,
372/// edit_controller_cid: EDIT_CONTROLLER_CID,
373/// ui_initial_size: conformal_vst_wrapper::UiSize {
374/// width: 400,
375/// height: 400,
376/// },
377/// },
378/// factory: |_: &HostInfo| -> Component { Default::default() },
379/// category: "Fx",
380/// bypass_id: "bypass",
381/// }]
382/// },
383/// Info {
384/// vendor: "My vendor name",
385/// url: "www.example.com",
386/// email: "test@example.com",
387/// version: "1.0.0",
388/// }
389/// );
390/// ```
391#[macro_export]
392macro_rules! wrap_factory {
393 ($CLASSES:expr, $INFO:expr) => {
394 #[unsafe(no_mangle)]
395 #[allow(non_snake_case, clippy::missing_safety_doc, clippy::missing_panics_doc)]
396 pub unsafe extern "system" fn GetPluginFactory() -> *mut core::ffi::c_void {
397 let factory = conformal_vst_wrapper::_wrap_factory($CLASSES, $INFO);
398 vst3::ComWrapper::new(factory)
399 .to_com_ptr::<vst3::Steinberg::IPluginFactory>()
400 .unwrap()
401 .into_raw()
402 .cast()
403 }
404
405 /// This is required by the API [see here](https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/VST+Module+Architecture/Index.html?highlight=GetPluginFactory#module-factory)
406 #[cfg(target_os = "macos")]
407 #[unsafe(no_mangle)]
408 #[allow(non_snake_case)]
409 pub extern "system" fn bundleEntry(_: *mut core::ffi::c_void) -> bool {
410 true
411 }
412
413 /// This is required by the API [see here](https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/VST+Module+Architecture/Index.html?highlight=GetPluginFactory#module-factory)
414 #[cfg(target_os = "macos")]
415 #[unsafe(no_mangle)]
416 #[allow(non_snake_case)]
417 pub extern "system" fn bundleExit() -> bool {
418 true
419 }
420 };
421}