conformal_vst_wrapper/
lib.rs

1#![doc = include_str!("../docs_boilerplate.md")]
2#![doc = include_str!("../README.md")]
3
4pub use conformal_ui::Size as UiSize;
5
6use core::slice;
7#[doc(hidden)]
8pub use vst3 as _vst3;
9
10/// Contains information about the host.
11///
12/// You can use this to customize the comonent based on the host.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct HostInfo {
15    /// The name of the host.
16    pub name: String,
17}
18
19/// A class ID for a VST3 component.
20///
21/// This must be _globally_ unique for each class
22pub type ClassID = [u8; 16];
23
24/// A component factory that can create a component.
25///
26/// This can return a specialized component based on information
27/// about the current host
28#[allow(clippy::module_name_repetitions)]
29pub trait ComponentFactory: Clone {
30    /// The type of component that this factory creates
31    type Component;
32
33    /// Create a component
34    fn create(&self, host: &HostInfo) -> Self::Component;
35}
36
37impl<C, F: Fn(&HostInfo) -> C + Clone> ComponentFactory for F {
38    type Component = C;
39    fn create(&self, host_info: &HostInfo) -> C {
40        (self)(host_info)
41    }
42}
43
44#[derive(Debug, Clone, Copy)]
45enum Resizability {
46    FixedSize,
47    Resizable {
48        ui_min_size: Option<UiSize>,
49        ui_max_size: Option<UiSize>,
50    },
51}
52
53/// Information about a VST3 component
54#[derive(Debug, Clone, Copy)]
55pub struct ClassInfo<'a> {
56    /// User-visible name of the component.
57    name: &'a str,
58
59    /// Class ID for the processor component.  This is used by the host to identify the VST.
60    cid: ClassID,
61
62    /// Class ID for the so-called "edit controller" component.  This is arbitrary
63    /// but must be unique.
64    edit_controller_cid: ClassID,
65
66    /// Initial size of the UI in logical pixels
67    ui_initial_size: UiSize,
68
69    /// Whether the component is resizable
70    resizability: Resizability,
71}
72
73/// A builder for `ClassInfo`
74///
75/// See [general documentation about this pattern](https://rust-unofficial.github.io/patterns/patterns/creational/builder.html).
76///
77/// We use a builder here to allow future additional options to not break the API.
78#[derive(Debug, Clone, Copy)]
79pub struct ClassInfoBuilder<'a> {
80    info: ClassInfo<'a>,
81}
82
83/// Options for components with resizable UI
84#[derive(Debug, Clone, Copy, Default)]
85pub struct ResizingOptions {
86    /// Minimum size of UI, if any
87    pub ui_min_size: Option<UiSize>,
88    /// Maximum size of UI, if any
89    pub ui_max_size: Option<UiSize>,
90}
91
92impl<'a> ClassInfoBuilder<'a> {
93    /// Create a new `ClassInfoBuilder`
94    #[must_use]
95    pub const fn new(
96        name: &'a str,
97        cid: ClassID,
98        edit_controller_cid: ClassID,
99        ui_initial_size: UiSize,
100    ) -> Self {
101        Self {
102            info: ClassInfo {
103                name,
104                cid,
105                edit_controller_cid,
106                ui_initial_size,
107                resizability: Resizability::FixedSize,
108            },
109        }
110    }
111
112    /// Make the component resizeable with optional min and max sizes.
113    ///
114    /// If you don't provide min or max sizes, there will be no size constraints.
115    #[must_use]
116    pub const fn resizable(self, options: ResizingOptions) -> Self {
117        Self {
118            info: ClassInfo {
119                resizability: Resizability::Resizable {
120                    ui_min_size: options.ui_min_size,
121                    ui_max_size: options.ui_max_size,
122                },
123                ..self.info
124            },
125        }
126    }
127
128    /// Build the `ClassInfo`
129    ///
130    /// # Panics
131    ///
132    /// If the initial size is not within the min and max size bounds or the min size is greater than the max size in either dimension.
133    #[must_use]
134    pub const fn build(self) -> ClassInfo<'a> {
135        self.info
136    }
137}
138
139#[doc(hidden)]
140pub struct ParameterModel {
141    pub parameter_infos: Box<dyn Fn(&HostInfo) -> Vec<conformal_component::parameters::Info>>,
142}
143
144#[doc(hidden)]
145pub trait ClassCategory {
146    fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase>;
147
148    fn info(&self) -> &ClassInfo<'static>;
149
150    fn category_str(&self) -> &'static str;
151
152    fn create_parameter_model(&self) -> ParameterModel;
153
154    fn get_kind(&self) -> edit_controller::Kind;
155}
156
157/// Information about a synth component
158pub struct SynthClass<CF> {
159    /// The actual factory.
160    pub factory: CF,
161
162    /// Information about the component
163    pub info: ClassInfo<'static>,
164}
165
166fn create_parameter_model_internal<CF: ComponentFactory + 'static>(factory: CF) -> ParameterModel
167where
168    CF::Component: Component,
169{
170    ParameterModel {
171        parameter_infos: Box::new(move |host_info| {
172            let component = factory.create(host_info);
173            component.parameter_infos()
174        }),
175    }
176}
177
178impl<CF: ComponentFactory + 'static> ClassCategory for SynthClass<CF>
179where
180    CF::Component: Component<Processor: Synth> + 'static,
181{
182    fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase> {
183        vst3::ComWrapper::new(processor::create_synth(
184            self.factory.clone(),
185            controller_cid,
186        ))
187        .to_com_ptr::<IPluginBase>()
188        .unwrap()
189    }
190
191    fn create_parameter_model(&self) -> ParameterModel {
192        create_parameter_model_internal(self.factory.clone())
193    }
194
195    fn category_str(&self) -> &'static str {
196        "Instrument|Synth"
197    }
198
199    fn info(&self) -> &ClassInfo<'static> {
200        &self.info
201    }
202
203    fn get_kind(&self) -> edit_controller::Kind {
204        edit_controller::Kind::Synth()
205    }
206}
207
208/// Information about an effect component
209pub struct EffectClass<CF> {
210    /// The actual factory.
211    pub factory: CF,
212
213    /// Information about the component
214    pub info: ClassInfo<'static>,
215
216    /// The VST3 category for this effect
217    /// See [here](https://steinbergmedia.github.io/vst3_doc/vstinterfaces/group__plugType.html)
218    /// for a list of possible categories.
219    pub category: &'static str,
220
221    /// All effects must have a bypass parameter. This is the unique ID for that parameter.
222    pub bypass_id: &'static str,
223}
224
225impl<CF: ComponentFactory<Component: Component<Processor: Effect> + 'static> + 'static>
226    ClassCategory for EffectClass<CF>
227{
228    fn create_processor(&self, controller_cid: ClassID) -> vst3::ComPtr<IPluginBase> {
229        vst3::ComWrapper::new(processor::create_effect(
230            self.factory.clone(),
231            controller_cid,
232        ))
233        .to_com_ptr::<IPluginBase>()
234        .unwrap()
235    }
236
237    fn category_str(&self) -> &'static str {
238        self.category
239    }
240
241    fn info(&self) -> &ClassInfo<'static> {
242        &self.info
243    }
244
245    fn create_parameter_model(&self) -> ParameterModel {
246        create_parameter_model_internal(self.factory.clone())
247    }
248
249    fn get_kind(&self) -> edit_controller::Kind {
250        edit_controller::Kind::Effect {
251            bypass_id: self.bypass_id,
252        }
253    }
254}
255
256/// General global infor about a vst plug-in
257#[derive(Debug, Clone, Copy)]
258pub struct Info<'a> {
259    /// The "vendor" of the plug-in.
260    ///
261    /// Hosts often present plug-ins grouped by vendor.
262    pub vendor: &'a str,
263
264    /// The vendor's URL
265    pub url: &'a str,
266
267    /// The vendor's email
268    pub email: &'a str,
269
270    /// User-visible version of components in this factory
271    pub version: &'a str,
272}
273
274use conformal_component::Component;
275use conformal_component::effect::Effect;
276use conformal_component::synth::Synth;
277
278use vst3::Steinberg::{IPluginBase, IPluginFactory2, IPluginFactory2Trait};
279use vst3::{Class, Steinberg::IPluginFactory};
280
281mod edit_controller;
282mod factory;
283mod host_info;
284mod io;
285mod mpe;
286mod parameters;
287mod processor;
288mod view;
289
290#[cfg(test)]
291mod dummy_host;
292
293#[cfg(test)]
294mod fake_ibstream;
295
296#[doc(hidden)]
297pub fn _wrap_factory(
298    classes: &'static [&'static dyn ClassCategory],
299    info: Info<'static>,
300) -> impl Class<Interfaces = (IPluginFactory, IPluginFactory2)> + 'static + IPluginFactory2Trait {
301    factory::Factory::new(classes, info)
302}
303
304fn to_utf16(s: &str, buffer: &mut [u16]) {
305    for (i, c) in s.encode_utf16().chain([0]).enumerate() {
306        buffer[i] = c;
307    }
308}
309
310fn from_utf16_ptr(buffer: *const u16, max_size: usize) -> Option<String> {
311    let mut len = 0;
312    unsafe {
313        while *buffer.add(len) != 0 {
314            if len >= max_size {
315                return None;
316            }
317            len += 1;
318        }
319    }
320    let utf16_slice = unsafe { slice::from_raw_parts(buffer.cast(), len) };
321    String::from_utf16(utf16_slice).ok()
322}
323
324fn from_utf16_buffer(buffer: &[u16]) -> Option<String> {
325    let mut len = 0;
326    for c in buffer {
327        if *c == 0 {
328            break;
329        }
330        len += 1;
331    }
332    let utf16_slice = unsafe { slice::from_raw_parts(buffer.as_ptr().cast(), len) };
333    String::from_utf16(utf16_slice).ok()
334}
335
336/// Create a VST3-compatible plug-in entry point.
337///
338/// This is the main entry point for the VST3 Conformal Wrapper, and must
339/// be invoked exactly once in each VST3 plug-in binary.
340///
341/// Note that each VST3 plug-in binary can contain _multiple_ components,
342/// so this takes a slice of `EffectClass` and `SynthClass` instances.
343///
344/// Note that to create a loadable plug-in, you must add this to your
345/// `Cargo.toml`:
346///
347/// ```toml
348/// [lib]
349/// crate-type = ["cdylib"]
350/// ```
351///
352/// Conformal provides a template project that you can use to get started,
353/// using `bun create conformal` script. This will provide a working example
354/// of using the VST3 wrapper.
355///
356/// # Example
357///
358/// ```
359/// use conformal_vst_wrapper::{ClassID, ClassInfoBuilder, EffectClass, HostInfo, Info};
360/// use conformal_component::audio::{channels, channels_mut, Buffer, BufferMut};
361/// use conformal_component::effect::Effect as EffectTrait;
362/// use conformal_component::parameters::{self, BufferStates, Flags, InfoRef, TypeSpecificInfoRef};
363/// use conformal_component::pzip;
364/// use conformal_component::{Component as ComponentTrait, ProcessingEnvironment, Processor};
365///
366/// const PARAMETERS: [InfoRef<'static, &'static str>; 2] = [
367///     InfoRef {
368///         title: "Bypass",
369///         short_title: "Bypass",
370///         unique_id: "bypass",
371///         flags: Flags { automatable: true },
372///         type_specific: TypeSpecificInfoRef::Switch { default: false },
373///     },
374///     InfoRef {
375///         title: "Gain",
376///         short_title: "Gain",
377///         unique_id: "gain",
378///         flags: Flags { automatable: true },
379///         type_specific: TypeSpecificInfoRef::Numeric {
380///             default: 100.,
381///             valid_range: 0f32..=100.,
382///             units: Some("%"),
383///         },
384///     },
385/// ];
386///
387/// #[derive(Clone, Debug, Default)]
388/// pub struct Component {}
389///
390/// #[derive(Clone, Debug, Default)]
391/// pub struct Effect {}
392///
393/// impl Processor for Effect {
394///     fn set_processing(&mut self, _processing: bool) {}
395/// }
396///
397/// impl EffectTrait for Effect {
398///     fn handle_parameters(&mut self, _context: &impl conformal_component::effect::HandleParametersContext) {}
399///     fn process(
400///         &mut self,
401///         context: &impl conformal_component::effect::ProcessContext,
402///         input: &impl Buffer,
403///         output: &mut impl BufferMut,
404///     ) {
405///         let parameters = context.parameters();
406///         for (input_channel, output_channel) in channels(input).zip(channels_mut(output)) {
407///             for ((input_sample, output_sample), (gain, bypass)) in input_channel
408///                 .iter()
409///                 .zip(output_channel.iter_mut())
410///                 .zip(pzip!(parameters[numeric "gain", switch "bypass"]))
411///             {
412///                 *output_sample = *input_sample * (if bypass { 1.0 } else { gain / 100.0 });
413///             }
414///         }
415///     }
416/// }
417///
418/// impl ComponentTrait for Component {
419///     type Processor = Effect;
420///
421///     fn parameter_infos(&self) -> Vec<parameters::Info> {
422///         parameters::to_infos(&PARAMETERS)
423///     }
424///
425///     fn create_processor(&self, _env: &ProcessingEnvironment) -> Self::Processor {
426///         Default::default()
427///     }
428/// }
429///
430/// // DO NOT USE this class ID, rather generate your own globally unique one.
431/// const CID: ClassID = [
432///   0x1d, 0x33, 0x78, 0xb8, 0xbd, 0xc9, 0x40, 0x8d, 0x86, 0x1f, 0xaf, 0xa4, 0xb5, 0x42, 0x5b, 0x74
433/// ];
434///
435/// // DO NOT USE this class ID, rather generate your own globally unique one.
436/// const EDIT_CONTROLLER_CID: ClassID = [
437///   0x96, 0xa6, 0xd4, 0x7d, 0xb2, 0x73, 0x46, 0x7c, 0xb0, 0xd6, 0xea, 0x6a, 0xd0, 0x27, 0xb2, 0x6f
438/// ];
439///
440/// conformal_vst_wrapper::wrap_factory!(
441///     &const {
442///         [&EffectClass {
443///             info: ClassInfoBuilder::new(
444///                 "My effect",
445///                 CID,
446///                 EDIT_CONTROLLER_CID,
447///                 conformal_vst_wrapper::UiSize {
448///                     width: 400,
449///                     height: 400,
450///                 },
451///             ).build(),
452///             factory: |_: &HostInfo| -> Component { Default::default() },
453///             category: "Fx",
454///             bypass_id: "bypass",
455///         }]
456///     },
457///     Info {
458///         vendor: "My vendor name",
459///         url: "www.example.com",
460///         email: "test@example.com",
461///         version: "1.0.0",
462///     }
463/// );
464/// ```
465#[macro_export]
466macro_rules! wrap_factory {
467    ($CLASSES:expr, $INFO:expr) => {
468        #[unsafe(no_mangle)]
469        #[allow(non_snake_case, clippy::missing_safety_doc, clippy::missing_panics_doc)]
470        pub unsafe extern "system" fn GetPluginFactory() -> *mut core::ffi::c_void {
471            let factory = $crate::_wrap_factory($CLASSES, $INFO);
472            $crate::_vst3::ComWrapper::new(factory)
473                .to_com_ptr::<$crate::_vst3::Steinberg::IPluginFactory>()
474                .unwrap()
475                .into_raw()
476                .cast()
477        }
478
479        /// 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)
480        #[cfg(target_os = "macos")]
481        #[unsafe(no_mangle)]
482        #[allow(non_snake_case)]
483        pub extern "system" fn bundleEntry(_: *mut core::ffi::c_void) -> bool {
484            true
485        }
486
487        /// 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)
488        #[cfg(target_os = "macos")]
489        #[unsafe(no_mangle)]
490        #[allow(non_snake_case)]
491        pub extern "system" fn bundleExit() -> bool {
492            true
493        }
494    };
495}
496
497#[cfg(target_os = "windows")]
498type DefaultEnumType = std::ffi::c_int;
499
500#[cfg(target_os = "windows")]
501type FromU32ConversionError = std::num::TryFromIntError;
502
503#[cfg(target_os = "windows")]
504type ToU32ConversionError = std::num::TryFromIntError;
505
506#[cfg(target_os = "windows")]
507type FromI32ConversionError = std::convert::Infallible;
508
509#[cfg(target_os = "windows")]
510type ToI32ConversionError = std::convert::Infallible;
511
512#[cfg(not(target_os = "windows"))]
513type DefaultEnumType = std::ffi::c_uint;
514
515#[cfg(not(target_os = "windows"))]
516type FromU32ConversionError = std::convert::Infallible;
517
518#[cfg(not(target_os = "windows"))]
519type ToU32ConversionError = std::convert::Infallible;
520
521#[cfg(not(target_os = "windows"))]
522type FromI32ConversionError = std::num::TryFromIntError;
523
524#[cfg(not(target_os = "windows"))]
525type ToI32ConversionError = std::num::TryFromIntError;
526
527#[cfg(target_os = "windows")]
528fn enum_to_u32(value: DefaultEnumType) -> Result<u32, ToU32ConversionError> {
529    value.try_into()
530}
531
532#[cfg(target_os = "windows")]
533fn u32_to_enum(value: u32) -> Result<DefaultEnumType, FromU32ConversionError> {
534    value.try_into()
535}
536
537#[cfg(target_os = "windows")]
538#[allow(clippy::unnecessary_wraps)] // We need to match mac behavior where this is fallible.
539fn i32_to_enum(value: i32) -> Result<DefaultEnumType, FromI32ConversionError> {
540    Ok(value)
541}
542
543#[cfg(target_os = "windows")]
544#[allow(clippy::unnecessary_wraps)] // We need to match mac behavior where this is fallible.
545fn enum_to_i32(value: DefaultEnumType) -> Result<i32, ToI32ConversionError> {
546    Ok(value)
547}
548
549#[cfg(not(target_os = "windows"))]
550#[allow(clippy::unnecessary_wraps)] // We need to match windows behavior where this is fallible.
551fn enum_to_u32(value: DefaultEnumType) -> Result<u32, ToU32ConversionError> {
552    Ok(value)
553}
554
555#[cfg(not(target_os = "windows"))]
556#[allow(clippy::unnecessary_wraps)] // We need to match windows behavior where this is fallible.
557fn u32_to_enum(value: u32) -> Result<DefaultEnumType, FromU32ConversionError> {
558    Ok(value)
559}
560
561#[cfg(not(target_os = "windows"))]
562fn i32_to_enum(value: i32) -> Result<DefaultEnumType, FromI32ConversionError> {
563    value.try_into()
564}
565
566#[cfg(not(target_os = "windows"))]
567fn enum_to_i32(value: DefaultEnumType) -> Result<i32, ToI32ConversionError> {
568    value.try_into()
569}