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#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct HostInfo {
15 pub name: String,
17}
18
19pub type ClassID = [u8; 16];
23
24#[allow(clippy::module_name_repetitions)]
29pub trait ComponentFactory: Clone {
30 type Component;
32
33 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#[derive(Debug, Clone, Copy)]
55pub struct ClassInfo<'a> {
56 name: &'a str,
58
59 cid: ClassID,
61
62 edit_controller_cid: ClassID,
65
66 ui_initial_size: UiSize,
68
69 resizability: Resizability,
71}
72
73#[derive(Debug, Clone, Copy)]
79pub struct ClassInfoBuilder<'a> {
80 info: ClassInfo<'a>,
81}
82
83#[derive(Debug, Clone, Copy, Default)]
85pub struct ResizingOptions {
86 pub ui_min_size: Option<UiSize>,
88 pub ui_max_size: Option<UiSize>,
90}
91
92impl<'a> ClassInfoBuilder<'a> {
93 #[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 #[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 #[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
157pub struct SynthClass<CF> {
159 pub factory: CF,
161
162 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
208pub struct EffectClass<CF> {
210 pub factory: CF,
212
213 pub info: ClassInfo<'static>,
215
216 pub category: &'static str,
220
221 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#[derive(Debug, Clone, Copy)]
258pub struct Info<'a> {
259 pub vendor: &'a str,
263
264 pub url: &'a str,
266
267 pub email: &'a str,
269
270 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#[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 #[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 #[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)] fn i32_to_enum(value: i32) -> Result<DefaultEnumType, FromI32ConversionError> {
540 Ok(value)
541}
542
543#[cfg(target_os = "windows")]
544#[allow(clippy::unnecessary_wraps)] fn enum_to_i32(value: DefaultEnumType) -> Result<i32, ToI32ConversionError> {
546 Ok(value)
547}
548
549#[cfg(not(target_os = "windows"))]
550#[allow(clippy::unnecessary_wraps)] fn enum_to_u32(value: DefaultEnumType) -> Result<u32, ToU32ConversionError> {
552 Ok(value)
553}
554
555#[cfg(not(target_os = "windows"))]
556#[allow(clippy::unnecessary_wraps)] fn 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}