1use truce_rack_core::buffer::AudioBuffer;
27use truce_rack_core::bus::BusLayout;
28use truce_rack_core::error::{Error, Result};
29use truce_rack_core::events::EventList;
30use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
31use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
32use truce_rack_core::scanner::PluginScanner;
33use truce_rack_core::transport::TransportInfo;
34use truce_rack_core::wrapper::run_audio_block_with;
35
36use std::path::{Path, PathBuf};
37use std::ptr;
38
39use vst3::Steinberg::Vst::{
40 AudioBusBuffers, AudioBusBuffers__type0, Event, Event_::EventTypes_, Event__type0,
41 IAudioProcessor, IAudioProcessorTrait, IComponent, IComponentTrait, IConnectionPoint,
42 IConnectionPointTrait, IEditController, IEditControllerTrait, IEventList, IEventListTrait,
43 IParameterChanges, NoteOffEvent, NoteOnEvent, ParameterInfo as Vst3ParameterInfo,
44 ParameterInfo_::ParameterFlags_, PolyPressureEvent, ProcessContext as Vst3ProcessContext,
45 ProcessContext_::StatesAndFlags_, ProcessData, ProcessModes_, ProcessSetup,
46 SymbolicSampleSizes_, ViewType,
47};
48use vst3::Steinberg::{
49 IBStream, IBStreamTrait, IPlugView, IPlugViewTrait, IPluginBaseTrait, IPluginFactory,
50 IPluginFactoryTrait, PClassInfo, PClassInfo_, TUID, ViewRect, kPlatformTypeHWND,
51 kPlatformTypeNSView, kPlatformTypeX11EmbedWindowID, kResultOk, kResultTrue,
52};
53use vst3::{Class, ComPtr, ComWrapper};
54
55pub const FORMAT: &str = "vst3";
57
58pub const VST3_EXTENSION: &str = ".vst3";
60
61const GET_FACTORY_SYMBOL: &[u8] = b"GetPluginFactory\0";
63
64#[cfg(target_os = "macos")]
66const BUNDLE_ENTRY_SYMBOL: &[u8] = b"bundleEntry\0";
67
68#[cfg(target_os = "macos")]
70const BUNDLE_EXIT_SYMBOL: &[u8] = b"bundleExit\0";
71
72const STEREO_ARRANGEMENT: u64 = 0x03;
75
76#[derive(Debug, Default)]
78pub struct Vst3Scanner;
79
80impl Vst3Scanner {
81 #[must_use]
83 pub fn new() -> Self {
84 Self
85 }
86}
87
88impl PluginScanner for Vst3Scanner {
89 type Plugin = Vst3Plugin;
90
91 fn scan(&self) -> Result<Vec<PluginInfo>> {
92 let mut out = Vec::new();
93 for dir in default_vst3_paths() {
94 if dir.exists() {
95 scan_dir_into(&dir, &mut out);
96 }
97 }
98 Ok(out)
99 }
100
101 fn scan_path(&self, path: &Path) -> Result<Vec<PluginInfo>> {
102 let mut out = Vec::new();
103 if path.exists() {
104 scan_dir_into(path, &mut out);
105 }
106 Ok(out)
107 }
108
109 fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
110 Vst3Plugin::load_from(info)
111 }
112}
113
114#[must_use]
116pub fn default_vst3_paths() -> Vec<PathBuf> {
117 let mut out = Vec::new();
118 if let Some(home) = std::env::var_os("HOME") {
119 let mut user = PathBuf::from(home);
120 #[cfg(target_os = "macos")]
121 user.push("Library/Audio/Plug-Ins/VST3");
122 #[cfg(target_os = "linux")]
123 user.push(".vst3");
124 #[cfg(target_os = "windows")]
125 user.push("AppData/Local/Programs/Common/VST3");
126 out.push(user);
127 }
128 #[cfg(target_os = "macos")]
129 out.push(PathBuf::from("/Library/Audio/Plug-Ins/VST3"));
130 #[cfg(target_os = "linux")]
131 out.push(PathBuf::from("/usr/lib/vst3"));
132 #[cfg(target_os = "windows")]
133 {
134 if let Some(pf) = std::env::var_os("CommonProgramFiles") {
135 let mut p = PathBuf::from(pf);
136 p.push("VST3");
137 out.push(p);
138 }
139 }
140 out
141}
142
143fn scan_dir_into(dir: &Path, out: &mut Vec<PluginInfo>) {
144 let Ok(entries) = std::fs::read_dir(dir) else {
145 return;
146 };
147 for entry in entries.flatten() {
148 let path = entry.path();
149 let name = match path.file_name().and_then(|n| n.to_str()) {
150 Some(n) => n.to_string(),
151 None => continue,
152 };
153 if !name.ends_with(VST3_EXTENSION) {
154 continue;
155 }
156 if let Err(err) = scan_bundle_into(&path, out) {
157 eprintln!("[truce-rack-vst3] skipping {}: {err}", path.display());
158 }
159 }
160}
161
162fn scan_bundle_into(bundle: &Path, out: &mut Vec<PluginInfo>) -> Result<()> {
163 let module = unsafe { LoadedModule::open(bundle) }?;
164 let factory = module.factory()?;
165 let count = unsafe { factory.countClasses() };
166 let mut info = empty_pclass_info();
167 for idx in 0..count {
168 if unsafe { factory.getClassInfo(idx, &raw mut info) } != kResultOk {
169 continue;
170 }
171 let category = char8_array_to_string(&info.category);
172 if category != "Audio Module Class" {
173 continue;
174 }
175 let name = char8_array_to_string(&info.name);
176 out.push(PluginInfo {
177 name,
178 vendor: String::new(),
179 version: 0,
180 category: PluginCategory::Effect,
181 path: bundle.to_path_buf(),
182 unique_id: tuid_to_hex(&info.cid),
183 format: FORMAT,
184 has_editor: false,
185 accepts_midi: false,
186 });
187 }
188 Ok(())
189}
190
191fn empty_pclass_info() -> PClassInfo {
192 PClassInfo {
193 cid: [0; 16],
194 cardinality: 0,
195 category: [0; PClassInfo_::kCategorySize as usize],
196 name: [0; PClassInfo_::kNameSize as usize],
197 }
198}
199
200#[allow(clippy::cast_sign_loss)]
201fn char8_array_to_string(array: &[i8]) -> String {
202 let bytes: Vec<u8> = array
205 .iter()
206 .take_while(|&&b| b != 0)
207 .map(|&b| b as u8)
208 .collect();
209 String::from_utf8_lossy(&bytes).into_owned()
210}
211
212#[allow(clippy::cast_sign_loss)]
213fn tuid_to_hex(cid: &[i8; 16]) -> String {
214 use std::fmt::Write;
215 let mut s = String::with_capacity(32);
216 for &b in cid {
217 let _ = write!(s, "{:02x}", b as u8);
218 }
219 s
220}
221
222#[allow(clippy::cast_possible_wrap)]
223fn hex_to_tuid(hex: &str) -> Option<TUID> {
224 if hex.len() != 32 {
225 return None;
226 }
227 let mut out: TUID = [0; 16];
228 for i in 0..16 {
229 out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()? as i8;
230 }
231 Some(out)
232}
233
234#[cfg(target_os = "macos")]
235mod mac {
236 use std::path::{Path, PathBuf};
248
249 use core_foundation::base::TCFType;
250 use core_foundation::bundle::CFBundle;
251 use core_foundation::string::CFString;
252 use core_foundation::url::{CFURL, kCFURLPOSIXPathStyle};
253
254 use truce_rack_core::error::{Error, Result};
255
256 pub(super) struct MacBundle {
257 bundle: CFBundle,
258 path: PathBuf,
259 }
260
261 impl MacBundle {
262 pub(super) fn open(bundle_path: &Path) -> Result<Self> {
263 let path_str = bundle_path.to_str().ok_or_else(|| Error::LoadFailed {
264 path: bundle_path.to_path_buf(),
265 reason: "bundle path is not valid UTF-8".into(),
266 })?;
267 let cf_path = CFString::new(path_str);
268 let url = unsafe {
273 use core_foundation_sys::base::kCFAllocatorDefault;
274 use core_foundation_sys::url::CFURLCreateWithFileSystemPath;
275 let raw = CFURLCreateWithFileSystemPath(
276 kCFAllocatorDefault,
277 cf_path.as_concrete_TypeRef(),
278 kCFURLPOSIXPathStyle,
279 1,
280 );
281 if raw.is_null() {
282 return Err(Error::LoadFailed {
283 path: bundle_path.to_path_buf(),
284 reason: "CFURLCreateWithFileSystemPath returned NULL".into(),
285 });
286 }
287 CFURL::wrap_under_create_rule(raw)
288 };
289
290 let bundle = CFBundle::new(url).ok_or_else(|| Error::LoadFailed {
291 path: bundle_path.to_path_buf(),
292 reason: "CFBundleCreate returned NULL".into(),
293 })?;
294
295 let loaded = unsafe {
298 use core_foundation_sys::bundle::CFBundleLoadExecutable;
299 CFBundleLoadExecutable(bundle.as_concrete_TypeRef()) != 0
300 };
301 if !loaded {
302 return Err(Error::LoadFailed {
303 path: bundle_path.to_path_buf(),
304 reason: "CFBundleLoadExecutable failed".into(),
305 });
306 }
307
308 Ok(Self {
309 bundle,
310 path: bundle_path.to_path_buf(),
311 })
312 }
313
314 pub(super) fn path(&self) -> &Path {
315 &self.path
316 }
317
318 pub(super) fn raw(&self) -> *mut std::ffi::c_void {
323 self.bundle.as_concrete_TypeRef().cast::<std::ffi::c_void>()
324 }
325
326 pub(super) unsafe fn function_ptr(&self, name: &[u8]) -> Option<*mut std::ffi::c_void> {
329 let name = match name.split_last() {
330 Some((0, rest)) => rest,
331 _ => name,
332 };
333 let name_str = std::str::from_utf8(name).ok()?;
334 let cf_name = CFString::new(name_str);
335 let ptr = unsafe {
336 use core_foundation_sys::bundle::CFBundleGetFunctionPointerForName;
337 CFBundleGetFunctionPointerForName(
338 self.bundle.as_concrete_TypeRef(),
339 cf_name.as_concrete_TypeRef(),
340 )
341 };
342 if ptr.is_null() {
343 None
344 } else {
345 Some(ptr.cast_mut().cast::<std::ffi::c_void>())
346 }
347 }
348 }
349
350 unsafe impl Send for MacBundle {}
359}
360
361#[cfg(not(target_os = "macos"))]
367fn bundle_binary_path(bundle: &Path) -> PathBuf {
368 let stem = bundle
369 .file_stem()
370 .map(std::ffi::OsStr::to_os_string)
371 .unwrap_or_default();
372 #[cfg(target_os = "linux")]
373 {
374 if bundle.is_dir() {
375 let arch_dir = format!("{}-linux", std::env::consts::ARCH);
376 let mut binary = stem.clone();
377 binary.push(".so");
378 return bundle.join("Contents").join(arch_dir).join(binary);
379 }
380 }
381 #[cfg(target_os = "windows")]
382 {
383 if bundle.is_dir() {
384 let arch_dir = format!("{}-win", std::env::consts::ARCH);
385 let mut binary = stem.clone();
386 binary.push(".vst3");
387 return bundle.join("Contents").join(arch_dir).join(binary);
388 }
389 }
390 let _ = stem;
391 bundle.to_path_buf()
392}
393
394#[cfg(not(target_os = "macos"))]
402struct LoadedModule {
403 library: libloading::Library,
404}
405
406#[cfg(target_os = "macos")]
407struct LoadedModule {
408 bundle: mac::MacBundle,
409 entered: bool,
410}
411
412#[cfg(not(target_os = "macos"))]
413impl LoadedModule {
414 unsafe fn open(bundle: &Path) -> Result<Self> {
415 let binary = bundle_binary_path(bundle);
416 let library =
417 unsafe { libloading::Library::new(&binary) }.map_err(|e| Error::LoadFailed {
418 path: bundle.to_path_buf(),
419 reason: format!("dlopen: {e}"),
420 })?;
421 Ok(Self { library })
422 }
423
424 fn factory(&self) -> Result<ComPtr<IPluginFactory>> {
425 let get_factory: libloading::Symbol<'_, unsafe extern "C" fn() -> *mut IPluginFactory> =
426 unsafe { self.library.get(GET_FACTORY_SYMBOL) }.map_err(|e| Error::LoadFailed {
427 path: PathBuf::new(),
428 reason: format!("missing GetPluginFactory: {e}"),
429 })?;
430 let ptr = unsafe { get_factory() };
431 let factory = unsafe { ComPtr::<IPluginFactory>::from_raw(ptr) }.ok_or_else(|| {
432 Error::LoadFailed {
433 path: PathBuf::new(),
434 reason: "GetPluginFactory returned NULL".into(),
435 }
436 })?;
437 Ok(factory)
438 }
439}
440
441#[cfg(target_os = "macos")]
442impl LoadedModule {
443 unsafe fn open(bundle: &Path) -> Result<Self> {
444 let mac_bundle = mac::MacBundle::open(bundle)?;
445
446 let entered = unsafe {
453 match mac_bundle.function_ptr(BUNDLE_ENTRY_SYMBOL) {
454 Some(ptr) => {
455 let entry: unsafe extern "C" fn(*mut std::ffi::c_void) -> bool =
456 std::mem::transmute(ptr);
457 entry(mac_bundle.raw())
458 }
459 None => false,
460 }
461 };
462
463 Ok(Self {
464 bundle: mac_bundle,
465 entered,
466 })
467 }
468
469 fn factory(&self) -> Result<ComPtr<IPluginFactory>> {
470 let raw = unsafe {
471 self.bundle
472 .function_ptr(GET_FACTORY_SYMBOL)
473 .ok_or_else(|| Error::LoadFailed {
474 path: self.bundle.path().to_path_buf(),
475 reason: "missing GetPluginFactory".into(),
476 })?
477 };
478 let get_factory: unsafe extern "C" fn() -> *mut IPluginFactory =
479 unsafe { std::mem::transmute(raw) };
480 let ptr = unsafe { get_factory() };
481 let factory = unsafe { ComPtr::<IPluginFactory>::from_raw(ptr) }.ok_or_else(|| {
482 Error::LoadFailed {
483 path: self.bundle.path().to_path_buf(),
484 reason: "GetPluginFactory returned NULL".into(),
485 }
486 })?;
487 Ok(factory)
488 }
489}
490
491#[cfg(target_os = "macos")]
492impl Drop for LoadedModule {
493 fn drop(&mut self) {
494 if self.entered
499 && let Some(ptr) = unsafe { self.bundle.function_ptr(BUNDLE_EXIT_SYMBOL) }
500 {
501 let exit: unsafe extern "C" fn() -> bool = unsafe { std::mem::transmute(ptr) };
502 unsafe {
503 exit();
504 }
505 }
506 }
507}
508
509pub struct Vst3Plugin {
517 info: PluginInfo,
518 layouts: Vec<BusLayout>,
519 active_layout: Option<BusLayout>,
520
521 _module: LoadedModule,
523 component: ComPtr<IComponent>,
524 processor: ComPtr<IAudioProcessor>,
525 controller: ComPtr<IEditController>,
526 separate_controller: bool,
530 component_cp: Option<ComPtr<IConnectionPoint>>,
531 controller_cp: Option<ComPtr<IConnectionPoint>>,
532
533 param_count: usize,
534 processing: bool,
535
536 view: Option<ComPtr<IPlugView>>,
539 editor_open: bool,
540}
541
542impl Vst3Plugin {
543 fn load_from(info: &PluginInfo) -> Result<Self> {
544 let module = unsafe { LoadedModule::open(&info.path) }?;
545 let factory = module.factory()?;
546
547 let cid = hex_to_tuid(&info.unique_id).ok_or_else(|| Error::LoadFailed {
548 path: info.path.clone(),
549 reason: format!("could not parse VST3 unique_id {:?}", info.unique_id),
550 })?;
551
552 let component_ptr =
554 unsafe { create_instance::<IComponent>(&factory, &cid) }.ok_or_else(|| {
555 Error::LoadFailed {
556 path: info.path.clone(),
557 reason: "factory.createInstance(IComponent) returned NULL".into(),
558 }
559 })?;
560 let component = component_ptr;
561
562 if unsafe { component.initialize(ptr::null_mut()) } != kResultOk {
566 return Err(Error::LoadFailed {
567 path: info.path.clone(),
568 reason: "IComponent::initialize returned non-OK".into(),
569 });
570 }
571
572 let processor = component
574 .as_com_ref()
575 .cast::<IAudioProcessor>()
576 .ok_or_else(|| Error::LoadFailed {
577 path: info.path.clone(),
578 reason: "component does not implement IAudioProcessor".into(),
579 })?;
580
581 let mut controller_cid: TUID = [0; 16];
585 let cls_id_status = unsafe { component.getControllerClassId(&raw mut controller_cid) };
586 let (controller, separate_controller) = if cls_id_status == kResultTrue {
587 let ctrl_ptr = unsafe { create_instance::<IEditController>(&factory, &controller_cid) }
588 .ok_or_else(|| Error::LoadFailed {
589 path: info.path.clone(),
590 reason: "factory.createInstance(IEditController) returned NULL".into(),
591 })?;
592 if unsafe { ctrl_ptr.initialize(ptr::null_mut()) } != kResultOk {
593 return Err(Error::LoadFailed {
594 path: info.path.clone(),
595 reason: "IEditController::initialize returned non-OK".into(),
596 });
597 }
598 (ctrl_ptr, true)
599 } else {
600 let ctrl = component
604 .as_com_ref()
605 .cast::<IEditController>()
606 .ok_or_else(|| Error::LoadFailed {
607 path: info.path.clone(),
608 reason:
609 "component is not its own controller and didn't report a controller cid"
610 .into(),
611 })?;
612 (ctrl, false)
613 };
614
615 let (component_cp, controller_cp) = if separate_controller {
621 let cp_a = component.as_com_ref().cast::<IConnectionPoint>();
622 let cp_b = controller.as_com_ref().cast::<IConnectionPoint>();
623 if let (Some(a), Some(b)) = (&cp_a, &cp_b) {
624 unsafe {
625 a.connect(b.as_com_ref().as_ptr().cast());
626 b.connect(a.as_com_ref().as_ptr().cast());
627 }
628 }
629 (cp_a, cp_b)
630 } else {
631 (None, None)
632 };
633
634 let param_count_raw = unsafe { controller.getParameterCount() }.max(0);
635 #[allow(clippy::cast_sign_loss)]
636 let param_count = param_count_raw as usize;
637
638 let mut info = info.clone();
639 info.has_editor = unsafe { create_editor_view(&controller) }.is_some();
644
645 Ok(Self {
646 info,
647 layouts: vec![BusLayout::stereo()],
648 active_layout: None,
649 _module: module,
650 component,
651 processor,
652 controller,
653 separate_controller,
654 component_cp,
655 controller_cp,
656 param_count,
657 processing: false,
658 view: None,
659 editor_open: false,
660 })
661 }
662}
663
664unsafe fn create_editor_view(controller: &ComPtr<IEditController>) -> Option<ComPtr<IPlugView>> {
667 let raw = unsafe { controller.createView(ViewType::kEditor) };
668 if raw.is_null() {
669 return None;
670 }
671 unsafe { ComPtr::<IPlugView>::from_raw(raw) }
672}
673
674fn platform_type_for_handle(
675 handle: truce_rack_core::editor::WindowHandle,
676) -> (*const i8, *mut std::ffi::c_void) {
677 use truce_rack_core::editor::WindowHandle;
678 match handle {
679 WindowHandle::NSView(p) => (kPlatformTypeNSView, p),
680 WindowHandle::HWND(p) => (kPlatformTypeHWND, p),
681 WindowHandle::X11(id) => (kPlatformTypeX11EmbedWindowID, id as *mut std::ffi::c_void),
682 }
683}
684
685unsafe fn create_instance<I>(factory: &ComPtr<IPluginFactory>, cid: &TUID) -> Option<ComPtr<I>>
689where
690 I: vst3::Interface,
691{
692 let mut obj: *mut std::ffi::c_void = ptr::null_mut();
693 let iid_bytes: &TUID =
696 unsafe { &*(std::ptr::from_ref::<vst3::com_scrape_types::Guid>(&I::IID).cast::<TUID>()) };
697 let cid_ptr = cid.as_ptr();
698 let iid_ptr = iid_bytes.as_ptr();
699 if unsafe { factory.createInstance(cid_ptr, iid_ptr, &raw mut obj) } != kResultOk
700 || obj.is_null()
701 {
702 return None;
703 }
704 unsafe { ComPtr::<I>::from_raw(obj.cast()) }
705}
706
707impl Drop for Vst3Plugin {
708 fn drop(&mut self) {
709 if self.processing {
710 unsafe { self.processor.setProcessing(0) };
711 }
712 if self.active_layout.is_some() {
713 unsafe { self.component.setActive(0) };
714 }
715 if let (Some(a), Some(b)) = (&self.component_cp, &self.controller_cp) {
716 unsafe {
717 a.disconnect(b.as_com_ref().as_ptr().cast());
718 b.disconnect(a.as_com_ref().as_ptr().cast());
719 }
720 }
721 if self.separate_controller {
722 unsafe { self.controller.terminate() };
723 }
724 unsafe { self.component.terminate() };
725 }
726}
727
728impl PluginCore for Vst3Plugin {
729 fn info(&self) -> &PluginInfo {
730 &self.info
731 }
732 fn active_layout(&self) -> Option<&BusLayout> {
733 self.active_layout.as_ref()
734 }
735 fn supported_layouts(&self) -> &[BusLayout] {
736 &self.layouts
737 }
738
739 fn parameter_count(&self) -> usize {
740 self.param_count
741 }
742
743 fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
744 if index >= self.param_count {
745 return Err(Error::InvalidParameter(index));
746 }
747 let mut info = empty_parameter_info();
748 let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
749 if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
750 return Err(Error::InvalidParameter(index));
751 }
752 Ok(vst3_param_info_to_rack(&info))
753 }
754
755 fn parameter_value(&self, index: usize) -> Result<f64> {
756 if index >= self.param_count {
757 return Err(Error::InvalidParameter(index));
758 }
759 let mut info = empty_parameter_info();
760 let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
761 if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
762 return Err(Error::InvalidParameter(index));
763 }
764 Ok(unsafe { self.controller.getParamNormalized(info.id) })
765 }
766
767 fn parameter_value_string(&self, index: usize, _value: f64) -> Result<String> {
768 let _ = index;
773 Err(Error::Other("vst3 parameter_value_string TODO".into()))
774 }
775
776 fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
777 if index >= self.param_count {
778 return Err(Error::InvalidParameter(index));
779 }
780 let mut info = empty_parameter_info();
781 let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
782 if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
783 return Err(Error::InvalidParameter(index));
784 }
785 let clamped = value.clamp(0.0, 1.0);
786 if unsafe { self.controller.setParamNormalized(info.id, clamped) } != kResultOk {
787 return Err(Error::Other(
788 "IEditController::setParamNormalized failed".into(),
789 ));
790 }
791 Ok(())
792 }
793
794 fn preset_count(&self) -> usize {
795 0
798 }
799 fn preset_info(&self, index: usize) -> Result<PresetInfo> {
800 Err(Error::InvalidParameter(index))
801 }
802 fn load_preset(&mut self, _preset_number: i32) -> Result<()> {
803 Err(Error::Other("vst3 preset loading not yet wired".into()))
804 }
805
806 fn save_state(&self) -> Result<Vec<u8>> {
807 let stream = ComWrapper::new(MemoryStream::default());
808 let stream_ptr = stream
809 .to_com_ptr::<IBStream>()
810 .ok_or_else(|| Error::Other("MemoryStream missing IBStream IID".into()))?;
811 let status = unsafe { self.component.getState(stream_ptr.as_ptr()) };
812 if status != kResultOk {
813 return Err(Error::Other(format!(
814 "IComponent::getState returned {status}"
815 )));
816 }
817 Ok(stream.data.borrow().clone())
818 }
819
820 fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
821 let stream = ComWrapper::new(MemoryStream {
822 data: std::cell::RefCell::new(bytes.to_vec()),
823 position: std::cell::Cell::new(0),
824 });
825 let stream_ptr = stream
826 .to_com_ptr::<IBStream>()
827 .ok_or_else(|| Error::Other("MemoryStream missing IBStream IID".into()))?;
828 let status = unsafe { self.component.setState(stream_ptr.as_ptr()) };
829 if status != kResultOk {
830 return Err(Error::Other(format!(
831 "IComponent::setState returned {status}"
832 )));
833 }
834 Ok(())
835 }
836
837 fn activate(
838 &mut self,
839 layout: BusLayout,
840 sample_rate: f64,
841 max_block_size: usize,
842 ) -> Result<()> {
843 let mut input_arr = STEREO_ARRANGEMENT;
844 let mut output_arr = STEREO_ARRANGEMENT;
845 let _ = unsafe {
846 self.processor
847 .setBusArrangements(&raw mut input_arr, 1, &raw mut output_arr, 1)
848 };
849
850 let mut setup = ProcessSetup {
851 #[allow(clippy::cast_possible_wrap)]
852 processMode: ProcessModes_::kRealtime as i32,
853 #[allow(clippy::cast_possible_wrap)]
854 symbolicSampleSize: SymbolicSampleSizes_::kSample32 as i32,
855 maxSamplesPerBlock: i32::try_from(max_block_size).unwrap_or(i32::MAX),
856 sampleRate: sample_rate,
857 };
858 if unsafe { self.processor.setupProcessing(&raw mut setup) } != kResultOk {
859 return Err(Error::Other(
860 "IAudioProcessor::setupProcessing failed".into(),
861 ));
862 }
863 if unsafe { self.component.setActive(1) } != kResultOk {
864 return Err(Error::Other("IComponent::setActive(true) failed".into()));
865 }
866 if unsafe { self.processor.setProcessing(1) } != kResultOk {
867 return Err(Error::Other(
868 "IAudioProcessor::setProcessing(true) failed".into(),
869 ));
870 }
871 self.processing = true;
872 self.active_layout = Some(layout);
873 Ok(())
874 }
875
876 fn deactivate(&mut self) {
877 if self.processing {
878 unsafe { self.processor.setProcessing(0) };
879 self.processing = false;
880 }
881 if self.active_layout.is_some() {
882 unsafe { self.component.setActive(0) };
883 }
884 self.active_layout = None;
885 }
886 fn is_active(&self) -> bool {
887 self.active_layout.is_some()
888 }
889
890 fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
891 if !self.info.has_editor {
892 return None;
893 }
894 Some(self)
895 }
896}
897
898impl truce_rack_core::editor::PluginEditor for Vst3Plugin {
899 fn open(
900 &mut self,
901 parent: truce_rack_core::editor::WindowHandle,
902 _scale: f64,
903 ) -> truce_rack_core::error::Result<()> {
904 if self.editor_open {
905 return Ok(());
906 }
907 let view = unsafe { create_editor_view(&self.controller) }
908 .ok_or_else(|| Error::Other("IEditController::createView returned NULL".into()))?;
909 let (type_str, parent_ptr) = platform_type_for_handle(parent);
910 if unsafe { view.isPlatformTypeSupported(type_str) } != kResultOk {
911 return Err(Error::Other(
912 "IPlugView::isPlatformTypeSupported returned false".into(),
913 ));
914 }
915 if unsafe { view.attached(parent_ptr, type_str) } != kResultOk {
916 return Err(Error::Other("IPlugView::attached returned non-OK".into()));
917 }
918 self.view = Some(view);
919 self.editor_open = true;
920 Ok(())
921 }
922
923 fn close(&mut self) {
924 if let Some(view) = self.view.take() {
925 unsafe { view.removed() };
926 }
927 self.editor_open = false;
928 }
929
930 fn is_open(&self) -> bool {
931 self.editor_open
932 }
933
934 fn size(&self) -> Option<(u32, u32)> {
935 let view = self.view.as_ref()?;
936 let mut rect = ViewRect {
937 left: 0,
938 top: 0,
939 right: 0,
940 bottom: 0,
941 };
942 if unsafe { view.getSize(&raw mut rect) } != kResultOk {
943 return None;
944 }
945 let w = u32::try_from(rect.right - rect.left).ok()?;
946 let h = u32::try_from(rect.bottom - rect.top).ok()?;
947 Some((w, h))
948 }
949
950 fn is_resizable(&self) -> bool {
951 let Some(view) = self.view.as_ref() else {
952 return false;
953 };
954 unsafe { view.canResize() == kResultOk }
955 }
956
957 fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
958 let view = self.view.as_ref()?;
959 let mut rect = ViewRect {
960 left: 0,
961 top: 0,
962 right: i32::try_from(width).ok()?,
963 bottom: i32::try_from(height).ok()?,
964 };
965 let _ = unsafe { view.checkSizeConstraint(&raw mut rect) };
967 if unsafe { view.onSize(&raw mut rect) } != kResultOk {
968 return None;
969 }
970 let w = u32::try_from(rect.right - rect.left).ok()?;
971 let h = u32::try_from(rect.bottom - rect.top).ok()?;
972 Some((w, h))
973 }
974
975 fn show(&mut self) {
976 }
979
980 fn hide(&mut self) {
981 }
983}
984
985fn empty_parameter_info() -> Vst3ParameterInfo {
986 Vst3ParameterInfo {
987 id: 0,
988 title: [0; 128],
989 shortTitle: [0; 128],
990 units: [0; 128],
991 stepCount: 0,
992 defaultNormalizedValue: 0.0,
993 unitId: 0,
994 flags: 0,
995 }
996}
997
998fn vst3_param_info_to_rack(info: &Vst3ParameterInfo) -> ParameterInfo {
999 let name = string128_to_string(&info.title);
1000 let short_name = string128_to_string(&info.shortTitle);
1001 let unit = string128_to_string(&info.units);
1002 let mut flags = truce_rack_core::info::ParameterFlags::empty();
1003 if info.flags & ParameterFlags_::kIsBypass != 0 {
1004 flags |= truce_rack_core::info::ParameterFlags::BYPASS;
1005 }
1006 if info.flags & ParameterFlags_::kCanAutomate != 0 {
1007 flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
1008 }
1009 if info.flags & ParameterFlags_::kIsHidden != 0 {
1010 flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
1011 }
1012 if info.flags & ParameterFlags_::kIsReadOnly != 0 {
1013 flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
1014 }
1015 if info.flags & ParameterFlags_::kIsList != 0 {
1016 flags |= truce_rack_core::info::ParameterFlags::ENUMERATED;
1017 }
1018 ParameterInfo {
1019 id: info.id,
1020 name,
1021 short_name,
1022 unit,
1023 min: 0.0,
1024 max: 1.0,
1025 default: info.defaultNormalizedValue,
1026 step_count: u32::try_from(info.stepCount).unwrap_or(0),
1027 flags,
1028 }
1029}
1030
1031fn string128_to_string(buf: &[u16; 128]) -> String {
1034 let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
1035 String::from_utf16_lossy(&buf[..len])
1036}
1037
1038#[allow(clippy::unnecessary_cast)]
1045fn build_vst3_context(t: &TransportInfo, sample_rate: f64) -> Vst3ProcessContext {
1046 let mut ctx: Vst3ProcessContext = unsafe { std::mem::zeroed() };
1051 ctx.sampleRate = sample_rate;
1052
1053 let mut state: u32 = 0;
1054 if let Some(tempo) = t.tempo_bpm {
1055 ctx.tempo = tempo;
1056 state |= StatesAndFlags_::kTempoValid as u32;
1057 }
1058 if let Some((num, den)) = t.time_signature {
1059 ctx.timeSigNumerator = i32::try_from(num).unwrap_or(4);
1060 ctx.timeSigDenominator = i32::try_from(den).unwrap_or(4);
1061 state |= StatesAndFlags_::kTimeSigValid as u32;
1062 }
1063 if let Some(beats) = t.song_position_beats {
1064 ctx.projectTimeMusic = beats;
1066 state |= StatesAndFlags_::kProjectTimeMusicValid as u32;
1067 }
1068 if let Some(samples) = t.song_position_samples {
1069 ctx.projectTimeSamples = samples;
1070 }
1071 if let Some(bar) = t.bar_start_beats {
1072 ctx.barPositionMusic = bar;
1073 state |= StatesAndFlags_::kBarPositionValid as u32;
1074 }
1075 if t.playing {
1076 state |= StatesAndFlags_::kPlaying as u32;
1077 }
1078 if t.recording {
1079 state |= StatesAndFlags_::kRecording as u32;
1080 }
1081 if t.loop_active {
1082 state |= StatesAndFlags_::kCycleActive as u32;
1083 }
1084 ctx.state = state;
1085 ctx
1086}
1087
1088impl Plugin<f32> for Vst3Plugin {
1089 fn process(
1090 &mut self,
1091 buffer: &mut AudioBuffer<'_, f32>,
1092 events: &EventList,
1093 context: &mut ProcessContext<'_>,
1094 ) -> Result<ProcessStatus> {
1095 if !self.is_active() {
1096 return Err(Error::NotActivated);
1097 }
1098 let frames = buffer.num_frames();
1099
1100 let mut translated = EventBuffer::default();
1103 for event in events {
1104 translated.push_rack(event);
1105 }
1106 let input_events_wrapper = ComWrapper::new(EventList3 {
1107 events: std::cell::RefCell::new(translated.events),
1108 });
1109 let input_events_ptr = input_events_wrapper
1110 .to_com_ptr::<IEventList>()
1111 .ok_or_else(|| Error::Other("EventList3 missing IEventList IID".into()))?;
1112 let output_events_wrapper = ComWrapper::new(EventList3::default());
1113 let output_events_ptr = output_events_wrapper
1114 .to_com_ptr::<IEventList>()
1115 .ok_or_else(|| Error::Other("EventList3 missing IEventList IID".into()))?;
1116
1117 let main_inputs = buffer.main_inputs();
1118 let mut input_ptrs: Vec<*mut f32> =
1119 main_inputs.iter().map(|c| c.as_ptr().cast_mut()).collect();
1120 let mut input_bus = AudioBusBuffers {
1121 numChannels: i32::try_from(input_ptrs.len()).unwrap_or(0),
1122 silenceFlags: 0,
1123 __field0: AudioBusBuffers__type0 {
1124 channelBuffers32: input_ptrs.as_mut_ptr(),
1125 },
1126 };
1127
1128 let main_outputs = buffer.main_outputs();
1129 let mut output_ptrs: Vec<*mut f32> =
1130 main_outputs.iter_mut().map(|c| c.as_mut_ptr()).collect();
1131 let mut output_bus = AudioBusBuffers {
1132 numChannels: i32::try_from(output_ptrs.len()).unwrap_or(0),
1133 silenceFlags: 0,
1134 __field0: AudioBusBuffers__type0 {
1135 channelBuffers32: output_ptrs.as_mut_ptr(),
1136 },
1137 };
1138
1139 let mut process_context = context
1142 .transport
1143 .map(|t| build_vst3_context(&t, context.sample_rate));
1144
1145 let mut data = ProcessData {
1146 #[allow(clippy::cast_possible_wrap)]
1147 processMode: ProcessModes_::kRealtime as i32,
1148 #[allow(clippy::cast_possible_wrap)]
1149 symbolicSampleSize: SymbolicSampleSizes_::kSample32 as i32,
1150 numSamples: i32::try_from(frames).unwrap_or(i32::MAX),
1151 numInputs: 1,
1152 numOutputs: 1,
1153 inputs: &raw mut input_bus,
1154 outputs: &raw mut output_bus,
1155 inputParameterChanges: ptr::null_mut::<IParameterChanges>(),
1156 outputParameterChanges: ptr::null_mut::<IParameterChanges>(),
1157 inputEvents: input_events_ptr.as_ptr(),
1158 outputEvents: output_events_ptr.as_ptr(),
1159 processContext: process_context
1160 .as_mut()
1161 .map_or(ptr::null_mut(), std::ptr::from_mut),
1162 };
1163
1164 let processor_ptr = self.processor.as_ptr();
1165 let status = run_audio_block_with::<Vst3Plugin, i32>(FORMAT, -1, || unsafe {
1166 ((*(*processor_ptr).vtbl).process)(processor_ptr, &raw mut data)
1167 });
1168 if status == kResultOk {
1169 Ok(ProcessStatus::Continue)
1170 } else {
1171 Ok(ProcessStatus::Error)
1172 }
1173 }
1174}
1175
1176#[derive(Default)]
1182struct EventBuffer {
1183 events: Vec<Event>,
1184}
1185
1186impl EventBuffer {
1187 fn push_rack(&mut self, event: &truce_rack_core::events::Event) {
1188 use truce_rack_core::events::{EventBody, MidiData};
1189 let offset = i32::try_from(event.sample_offset).unwrap_or(i32::MAX);
1190 let EventBody::Midi(body) = event.body else {
1191 return;
1192 };
1193 let header = |type_: u16| Event {
1194 busIndex: 0,
1195 sampleOffset: offset,
1196 ppqPosition: 0.0,
1197 flags: 0,
1198 r#type: type_,
1199 __field0: Event__type0 {
1200 noteOn: NoteOnEvent {
1201 channel: 0,
1202 pitch: 0,
1203 tuning: 0.0,
1204 velocity: 0.0,
1205 length: 0,
1206 noteId: -1,
1207 },
1208 },
1209 };
1210 match body {
1211 MidiData::NoteOn {
1212 channel,
1213 note,
1214 velocity,
1215 } => {
1216 let mut ev = header(u16::try_from(EventTypes_::kNoteOnEvent).unwrap_or(0));
1217 ev.__field0 = Event__type0 {
1218 noteOn: NoteOnEvent {
1219 channel: i16::from(channel),
1220 pitch: i16::from(note),
1221 tuning: 0.0,
1222 velocity: f32::from(velocity) / 127.0,
1223 length: 0,
1224 noteId: -1,
1225 },
1226 };
1227 self.events.push(ev);
1228 }
1229 MidiData::NoteOff {
1230 channel,
1231 note,
1232 velocity,
1233 } => {
1234 let mut ev = header(u16::try_from(EventTypes_::kNoteOffEvent).unwrap_or(0));
1235 ev.__field0 = Event__type0 {
1236 noteOff: NoteOffEvent {
1237 channel: i16::from(channel),
1238 pitch: i16::from(note),
1239 velocity: f32::from(velocity) / 127.0,
1240 noteId: -1,
1241 tuning: 0.0,
1242 },
1243 };
1244 self.events.push(ev);
1245 }
1246 MidiData::PolyAftertouch {
1247 channel,
1248 note,
1249 pressure,
1250 } => {
1251 let mut ev = header(u16::try_from(EventTypes_::kPolyPressureEvent).unwrap_or(0));
1252 ev.__field0 = Event__type0 {
1253 polyPressure: PolyPressureEvent {
1254 channel: i16::from(channel),
1255 pitch: i16::from(note),
1256 pressure: f32::from(pressure) / 127.0,
1257 noteId: -1,
1258 },
1259 };
1260 self.events.push(ev);
1261 }
1262 _ => {}
1268 }
1269 }
1270}
1271
1272#[derive(Default)]
1273struct EventList3 {
1274 events: std::cell::RefCell<Vec<Event>>,
1275}
1276
1277impl Class for EventList3 {
1278 type Interfaces = (IEventList,);
1279}
1280
1281#[allow(clippy::cast_sign_loss)]
1282impl IEventListTrait for EventList3 {
1283 unsafe fn getEventCount(&self) -> i32 {
1284 i32::try_from(self.events.borrow().len()).unwrap_or(i32::MAX)
1285 }
1286
1287 unsafe fn getEvent(&self, index: i32, out: *mut Event) -> i32 {
1288 if out.is_null() || index < 0 {
1289 return -1;
1290 }
1291 let events = self.events.borrow();
1292 let Some(event) = events.get(index as usize) else {
1293 return -1;
1294 };
1295 unsafe { *out = *event };
1296 kResultOk
1297 }
1298
1299 unsafe fn addEvent(&self, event: *mut Event) -> i32 {
1300 if event.is_null() {
1301 return -1;
1302 }
1303 self.events.borrow_mut().push(unsafe { *event });
1304 kResultOk
1305 }
1306}
1307
1308#[derive(Default)]
1317struct MemoryStream {
1318 data: std::cell::RefCell<Vec<u8>>,
1319 position: std::cell::Cell<usize>,
1320}
1321
1322impl Class for MemoryStream {
1323 type Interfaces = (IBStream,);
1324}
1325
1326impl IBStreamTrait for MemoryStream {
1327 unsafe fn read(
1328 &self,
1329 buffer: *mut std::ffi::c_void,
1330 num_bytes: i32,
1331 num_bytes_read: *mut i32,
1332 ) -> i32 {
1333 if buffer.is_null() || num_bytes < 0 {
1334 return -1;
1335 }
1336 let pos = self.position.get();
1337 #[allow(clippy::cast_sign_loss)]
1338 let want = num_bytes as usize;
1339 let data = self.data.borrow();
1340 let available = data.len().saturating_sub(pos);
1341 let take = want.min(available);
1342 if take > 0 {
1343 unsafe {
1344 std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), buffer.cast::<u8>(), take);
1345 }
1346 }
1347 self.position.set(pos + take);
1348 if !num_bytes_read.is_null() {
1349 unsafe {
1350 *num_bytes_read = i32::try_from(take).unwrap_or(i32::MAX);
1351 }
1352 }
1353 kResultOk
1354 }
1355
1356 unsafe fn write(
1357 &self,
1358 buffer: *mut std::ffi::c_void,
1359 num_bytes: i32,
1360 num_bytes_written: *mut i32,
1361 ) -> i32 {
1362 if buffer.is_null() || num_bytes < 0 {
1363 return -1;
1364 }
1365 let pos = self.position.get();
1366 #[allow(clippy::cast_sign_loss)]
1367 let want = num_bytes as usize;
1368 let mut data = self.data.borrow_mut();
1369 if data.len() < pos + want {
1370 data.resize(pos + want, 0);
1371 }
1372 unsafe {
1373 std::ptr::copy_nonoverlapping(buffer.cast::<u8>(), data.as_mut_ptr().add(pos), want);
1374 }
1375 self.position.set(pos + want);
1376 if !num_bytes_written.is_null() {
1377 unsafe {
1378 *num_bytes_written = i32::try_from(want).unwrap_or(i32::MAX);
1379 }
1380 }
1381 kResultOk
1382 }
1383
1384 #[allow(clippy::cast_possible_wrap)]
1385 unsafe fn seek(&self, pos: i64, mode: i32, result: *mut i64) -> i32 {
1386 let data_len = self.data.borrow().len() as i64;
1388 let current = self.position.get() as i64;
1389 let new_pos = match mode {
1390 0 => pos,
1391 1 => current + pos,
1392 2 => data_len + pos,
1393 _ => return -1,
1394 };
1395 if new_pos < 0 {
1396 return -1;
1397 }
1398 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1399 self.position.set(new_pos as usize);
1400 if !result.is_null() {
1401 unsafe {
1402 *result = new_pos;
1403 }
1404 }
1405 kResultOk
1406 }
1407
1408 #[allow(clippy::cast_possible_wrap)]
1409 unsafe fn tell(&self, pos: *mut i64) -> i32 {
1410 if pos.is_null() {
1411 return -1;
1412 }
1413 unsafe {
1414 *pos = self.position.get() as i64;
1415 }
1416 kResultOk
1417 }
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422 use super::*;
1423
1424 #[test]
1425 fn tuid_hex_roundtrip() {
1426 let cid: TUID = [
1427 0x01, 0x23, 0x45, 0x67, -0x10, -0x10, -0x10, -0x10, 0, 0, 0, 0, 0, 0, 0, 0,
1428 ];
1429 let hex = tuid_to_hex(&cid);
1430 let parsed = hex_to_tuid(&hex).expect("parse");
1431 assert_eq!(parsed, cid);
1432 }
1433
1434 #[test]
1435 fn char8_skips_trailing_nul() {
1436 let mut arr = [0i8; 16];
1437 for (i, &b) in b"Hello".iter().enumerate() {
1438 arr[i] = i8::try_from(b).expect("ASCII byte fits in i8");
1441 }
1442 assert_eq!(char8_array_to_string(&arr), "Hello");
1443 }
1444}