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}