use truce_rack_core::buffer::AudioBuffer;
use truce_rack_core::bus::BusLayout;
use truce_rack_core::error::{Error, Result};
use truce_rack_core::events::EventList;
use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
use truce_rack_core::scanner::PluginScanner;
use truce_rack_core::transport::TransportInfo;
use truce_rack_core::wrapper::run_audio_block_with;
use std::path::{Path, PathBuf};
use std::ptr;
use vst3::Steinberg::Vst::{
AudioBusBuffers, AudioBusBuffers__type0, Event, Event_::EventTypes_, Event__type0,
IAudioProcessor, IAudioProcessorTrait, IComponent, IComponentTrait, IConnectionPoint,
IConnectionPointTrait, IEditController, IEditControllerTrait, IEventList, IEventListTrait,
IParameterChanges, NoteOffEvent, NoteOnEvent, ParameterInfo as Vst3ParameterInfo,
ParameterInfo_::ParameterFlags_, PolyPressureEvent, ProcessContext as Vst3ProcessContext,
ProcessContext_::StatesAndFlags_, ProcessData, ProcessModes_, ProcessSetup,
SymbolicSampleSizes_, ViewType,
};
use vst3::Steinberg::{
IBStream, IBStreamTrait, IPlugView, IPlugViewTrait, IPluginBaseTrait, IPluginFactory,
IPluginFactoryTrait, PClassInfo, PClassInfo_, TUID, ViewRect, kPlatformTypeHWND,
kPlatformTypeNSView, kPlatformTypeX11EmbedWindowID, kResultOk, kResultTrue,
};
use vst3::{Class, ComPtr, ComWrapper};
pub const FORMAT: &str = "vst3";
pub const VST3_EXTENSION: &str = ".vst3";
const GET_FACTORY_SYMBOL: &[u8] = b"GetPluginFactory\0";
#[cfg(target_os = "macos")]
const BUNDLE_ENTRY_SYMBOL: &[u8] = b"bundleEntry\0";
#[cfg(target_os = "macos")]
const BUNDLE_EXIT_SYMBOL: &[u8] = b"bundleExit\0";
const STEREO_ARRANGEMENT: u64 = 0x03;
#[derive(Debug, Default)]
pub struct Vst3Scanner;
impl Vst3Scanner {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl PluginScanner for Vst3Scanner {
type Plugin = Vst3Plugin;
fn scan(&self) -> Result<Vec<PluginInfo>> {
let mut out = Vec::new();
for dir in default_vst3_paths() {
if dir.exists() {
scan_dir_into(&dir, &mut out);
}
}
Ok(out)
}
fn scan_path(&self, path: &Path) -> Result<Vec<PluginInfo>> {
let mut out = Vec::new();
if path.exists() {
scan_dir_into(path, &mut out);
}
Ok(out)
}
fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
Vst3Plugin::load_from(info)
}
}
#[must_use]
pub fn default_vst3_paths() -> Vec<PathBuf> {
let mut out = Vec::new();
if let Some(home) = std::env::var_os("HOME") {
let mut user = PathBuf::from(home);
#[cfg(target_os = "macos")]
user.push("Library/Audio/Plug-Ins/VST3");
#[cfg(target_os = "linux")]
user.push(".vst3");
#[cfg(target_os = "windows")]
user.push("AppData/Local/Programs/Common/VST3");
out.push(user);
}
#[cfg(target_os = "macos")]
out.push(PathBuf::from("/Library/Audio/Plug-Ins/VST3"));
#[cfg(target_os = "linux")]
out.push(PathBuf::from("/usr/lib/vst3"));
#[cfg(target_os = "windows")]
{
if let Some(pf) = std::env::var_os("CommonProgramFiles") {
let mut p = PathBuf::from(pf);
p.push("VST3");
out.push(p);
}
}
out
}
fn scan_dir_into(dir: &Path, out: &mut Vec<PluginInfo>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if !name.ends_with(VST3_EXTENSION) {
continue;
}
if let Err(err) = scan_bundle_into(&path, out) {
eprintln!("[truce-rack-vst3] skipping {}: {err}", path.display());
}
}
}
fn scan_bundle_into(bundle: &Path, out: &mut Vec<PluginInfo>) -> Result<()> {
let module = unsafe { LoadedModule::open(bundle) }?;
let factory = module.factory()?;
let count = unsafe { factory.countClasses() };
let mut info = empty_pclass_info();
for idx in 0..count {
if unsafe { factory.getClassInfo(idx, &raw mut info) } != kResultOk {
continue;
}
let category = char8_array_to_string(&info.category);
if category != "Audio Module Class" {
continue;
}
let name = char8_array_to_string(&info.name);
out.push(PluginInfo {
name,
vendor: String::new(),
version: 0,
category: PluginCategory::Effect,
path: bundle.to_path_buf(),
unique_id: tuid_to_hex(&info.cid),
format: FORMAT,
has_editor: false,
accepts_midi: false,
});
}
Ok(())
}
fn empty_pclass_info() -> PClassInfo {
PClassInfo {
cid: [0; 16],
cardinality: 0,
category: [0; PClassInfo_::kCategorySize as usize],
name: [0; PClassInfo_::kNameSize as usize],
}
}
#[allow(clippy::cast_sign_loss)]
fn char8_array_to_string(array: &[i8]) -> String {
let bytes: Vec<u8> = array
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
#[allow(clippy::cast_sign_loss)]
fn tuid_to_hex(cid: &[i8; 16]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(32);
for &b in cid {
let _ = write!(s, "{:02x}", b as u8);
}
s
}
#[allow(clippy::cast_possible_wrap)]
fn hex_to_tuid(hex: &str) -> Option<TUID> {
if hex.len() != 32 {
return None;
}
let mut out: TUID = [0; 16];
for i in 0..16 {
out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()? as i8;
}
Some(out)
}
#[cfg(target_os = "macos")]
mod mac {
use std::path::{Path, PathBuf};
use core_foundation::base::TCFType;
use core_foundation::bundle::CFBundle;
use core_foundation::string::CFString;
use core_foundation::url::{CFURL, kCFURLPOSIXPathStyle};
use truce_rack_core::error::{Error, Result};
pub(super) struct MacBundle {
bundle: CFBundle,
path: PathBuf,
}
impl MacBundle {
pub(super) fn open(bundle_path: &Path) -> Result<Self> {
let path_str = bundle_path.to_str().ok_or_else(|| Error::LoadFailed {
path: bundle_path.to_path_buf(),
reason: "bundle path is not valid UTF-8".into(),
})?;
let cf_path = CFString::new(path_str);
let url = unsafe {
use core_foundation_sys::base::kCFAllocatorDefault;
use core_foundation_sys::url::CFURLCreateWithFileSystemPath;
let raw = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
cf_path.as_concrete_TypeRef(),
kCFURLPOSIXPathStyle,
1,
);
if raw.is_null() {
return Err(Error::LoadFailed {
path: bundle_path.to_path_buf(),
reason: "CFURLCreateWithFileSystemPath returned NULL".into(),
});
}
CFURL::wrap_under_create_rule(raw)
};
let bundle = CFBundle::new(url).ok_or_else(|| Error::LoadFailed {
path: bundle_path.to_path_buf(),
reason: "CFBundleCreate returned NULL".into(),
})?;
let loaded = unsafe {
use core_foundation_sys::bundle::CFBundleLoadExecutable;
CFBundleLoadExecutable(bundle.as_concrete_TypeRef()) != 0
};
if !loaded {
return Err(Error::LoadFailed {
path: bundle_path.to_path_buf(),
reason: "CFBundleLoadExecutable failed".into(),
});
}
Ok(Self {
bundle,
path: bundle_path.to_path_buf(),
})
}
pub(super) fn path(&self) -> &Path {
&self.path
}
pub(super) fn raw(&self) -> *mut std::ffi::c_void {
self.bundle.as_concrete_TypeRef().cast::<std::ffi::c_void>()
}
pub(super) unsafe fn function_ptr(&self, name: &[u8]) -> Option<*mut std::ffi::c_void> {
let name = match name.split_last() {
Some((0, rest)) => rest,
_ => name,
};
let name_str = std::str::from_utf8(name).ok()?;
let cf_name = CFString::new(name_str);
let ptr = unsafe {
use core_foundation_sys::bundle::CFBundleGetFunctionPointerForName;
CFBundleGetFunctionPointerForName(
self.bundle.as_concrete_TypeRef(),
cf_name.as_concrete_TypeRef(),
)
};
if ptr.is_null() {
None
} else {
Some(ptr.cast_mut().cast::<std::ffi::c_void>())
}
}
}
unsafe impl Send for MacBundle {}
}
#[cfg(not(target_os = "macos"))]
fn bundle_binary_path(bundle: &Path) -> PathBuf {
let stem = bundle
.file_stem()
.map(std::ffi::OsStr::to_os_string)
.unwrap_or_default();
#[cfg(target_os = "linux")]
{
if bundle.is_dir() {
let arch_dir = format!("{}-linux", std::env::consts::ARCH);
let mut binary = stem.clone();
binary.push(".so");
return bundle.join("Contents").join(arch_dir).join(binary);
}
}
#[cfg(target_os = "windows")]
{
if bundle.is_dir() {
let arch_dir = format!("{}-win", std::env::consts::ARCH);
let mut binary = stem.clone();
binary.push(".vst3");
return bundle.join("Contents").join(arch_dir).join(binary);
}
}
let _ = stem;
bundle.to_path_buf()
}
#[cfg(not(target_os = "macos"))]
struct LoadedModule {
library: libloading::Library,
}
#[cfg(target_os = "macos")]
struct LoadedModule {
bundle: mac::MacBundle,
entered: bool,
}
#[cfg(not(target_os = "macos"))]
impl LoadedModule {
unsafe fn open(bundle: &Path) -> Result<Self> {
let binary = bundle_binary_path(bundle);
let library =
unsafe { libloading::Library::new(&binary) }.map_err(|e| Error::LoadFailed {
path: bundle.to_path_buf(),
reason: format!("dlopen: {e}"),
})?;
Ok(Self { library })
}
fn factory(&self) -> Result<ComPtr<IPluginFactory>> {
let get_factory: libloading::Symbol<'_, unsafe extern "C" fn() -> *mut IPluginFactory> =
unsafe { self.library.get(GET_FACTORY_SYMBOL) }.map_err(|e| Error::LoadFailed {
path: PathBuf::new(),
reason: format!("missing GetPluginFactory: {e}"),
})?;
let ptr = unsafe { get_factory() };
let factory = unsafe { ComPtr::<IPluginFactory>::from_raw(ptr) }.ok_or_else(|| {
Error::LoadFailed {
path: PathBuf::new(),
reason: "GetPluginFactory returned NULL".into(),
}
})?;
Ok(factory)
}
}
#[cfg(target_os = "macos")]
impl LoadedModule {
unsafe fn open(bundle: &Path) -> Result<Self> {
let mac_bundle = mac::MacBundle::open(bundle)?;
let entered = unsafe {
match mac_bundle.function_ptr(BUNDLE_ENTRY_SYMBOL) {
Some(ptr) => {
let entry: unsafe extern "C" fn(*mut std::ffi::c_void) -> bool =
std::mem::transmute(ptr);
entry(mac_bundle.raw())
}
None => false,
}
};
Ok(Self {
bundle: mac_bundle,
entered,
})
}
fn factory(&self) -> Result<ComPtr<IPluginFactory>> {
let raw = unsafe {
self.bundle
.function_ptr(GET_FACTORY_SYMBOL)
.ok_or_else(|| Error::LoadFailed {
path: self.bundle.path().to_path_buf(),
reason: "missing GetPluginFactory".into(),
})?
};
let get_factory: unsafe extern "C" fn() -> *mut IPluginFactory =
unsafe { std::mem::transmute(raw) };
let ptr = unsafe { get_factory() };
let factory = unsafe { ComPtr::<IPluginFactory>::from_raw(ptr) }.ok_or_else(|| {
Error::LoadFailed {
path: self.bundle.path().to_path_buf(),
reason: "GetPluginFactory returned NULL".into(),
}
})?;
Ok(factory)
}
}
#[cfg(target_os = "macos")]
impl Drop for LoadedModule {
fn drop(&mut self) {
if self.entered
&& let Some(ptr) = unsafe { self.bundle.function_ptr(BUNDLE_EXIT_SYMBOL) }
{
let exit: unsafe extern "C" fn() -> bool = unsafe { std::mem::transmute(ptr) };
unsafe {
exit();
}
}
}
}
pub struct Vst3Plugin {
info: PluginInfo,
layouts: Vec<BusLayout>,
active_layout: Option<BusLayout>,
_module: LoadedModule,
component: ComPtr<IComponent>,
processor: ComPtr<IAudioProcessor>,
controller: ComPtr<IEditController>,
separate_controller: bool,
component_cp: Option<ComPtr<IConnectionPoint>>,
controller_cp: Option<ComPtr<IConnectionPoint>>,
param_count: usize,
processing: bool,
view: Option<ComPtr<IPlugView>>,
editor_open: bool,
}
impl Vst3Plugin {
fn load_from(info: &PluginInfo) -> Result<Self> {
let module = unsafe { LoadedModule::open(&info.path) }?;
let factory = module.factory()?;
let cid = hex_to_tuid(&info.unique_id).ok_or_else(|| Error::LoadFailed {
path: info.path.clone(),
reason: format!("could not parse VST3 unique_id {:?}", info.unique_id),
})?;
let component_ptr =
unsafe { create_instance::<IComponent>(&factory, &cid) }.ok_or_else(|| {
Error::LoadFailed {
path: info.path.clone(),
reason: "factory.createInstance(IComponent) returned NULL".into(),
}
})?;
let component = component_ptr;
if unsafe { component.initialize(ptr::null_mut()) } != kResultOk {
return Err(Error::LoadFailed {
path: info.path.clone(),
reason: "IComponent::initialize returned non-OK".into(),
});
}
let processor = component
.as_com_ref()
.cast::<IAudioProcessor>()
.ok_or_else(|| Error::LoadFailed {
path: info.path.clone(),
reason: "component does not implement IAudioProcessor".into(),
})?;
let mut controller_cid: TUID = [0; 16];
let cls_id_status = unsafe { component.getControllerClassId(&raw mut controller_cid) };
let (controller, separate_controller) = if cls_id_status == kResultTrue {
let ctrl_ptr = unsafe { create_instance::<IEditController>(&factory, &controller_cid) }
.ok_or_else(|| Error::LoadFailed {
path: info.path.clone(),
reason: "factory.createInstance(IEditController) returned NULL".into(),
})?;
if unsafe { ctrl_ptr.initialize(ptr::null_mut()) } != kResultOk {
return Err(Error::LoadFailed {
path: info.path.clone(),
reason: "IEditController::initialize returned non-OK".into(),
});
}
(ctrl_ptr, true)
} else {
let ctrl = component
.as_com_ref()
.cast::<IEditController>()
.ok_or_else(|| Error::LoadFailed {
path: info.path.clone(),
reason:
"component is not its own controller and didn't report a controller cid"
.into(),
})?;
(ctrl, false)
};
let (component_cp, controller_cp) = if separate_controller {
let cp_a = component.as_com_ref().cast::<IConnectionPoint>();
let cp_b = controller.as_com_ref().cast::<IConnectionPoint>();
if let (Some(a), Some(b)) = (&cp_a, &cp_b) {
unsafe {
a.connect(b.as_com_ref().as_ptr().cast());
b.connect(a.as_com_ref().as_ptr().cast());
}
}
(cp_a, cp_b)
} else {
(None, None)
};
let param_count_raw = unsafe { controller.getParameterCount() }.max(0);
#[allow(clippy::cast_sign_loss)]
let param_count = param_count_raw as usize;
let mut info = info.clone();
info.has_editor = unsafe { create_editor_view(&controller) }.is_some();
Ok(Self {
info,
layouts: vec![BusLayout::stereo()],
active_layout: None,
_module: module,
component,
processor,
controller,
separate_controller,
component_cp,
controller_cp,
param_count,
processing: false,
view: None,
editor_open: false,
})
}
}
unsafe fn create_editor_view(controller: &ComPtr<IEditController>) -> Option<ComPtr<IPlugView>> {
let raw = unsafe { controller.createView(ViewType::kEditor) };
if raw.is_null() {
return None;
}
unsafe { ComPtr::<IPlugView>::from_raw(raw) }
}
fn platform_type_for_handle(
handle: truce_rack_core::editor::WindowHandle,
) -> (*const i8, *mut std::ffi::c_void) {
use truce_rack_core::editor::WindowHandle;
match handle {
WindowHandle::NSView(p) => (kPlatformTypeNSView, p),
WindowHandle::HWND(p) => (kPlatformTypeHWND, p),
WindowHandle::X11(id) => (kPlatformTypeX11EmbedWindowID, id as *mut std::ffi::c_void),
}
}
unsafe fn create_instance<I>(factory: &ComPtr<IPluginFactory>, cid: &TUID) -> Option<ComPtr<I>>
where
I: vst3::Interface,
{
let mut obj: *mut std::ffi::c_void = ptr::null_mut();
let iid_bytes: &TUID =
unsafe { &*(std::ptr::from_ref::<vst3::com_scrape_types::Guid>(&I::IID).cast::<TUID>()) };
let cid_ptr = cid.as_ptr();
let iid_ptr = iid_bytes.as_ptr();
if unsafe { factory.createInstance(cid_ptr, iid_ptr, &raw mut obj) } != kResultOk
|| obj.is_null()
{
return None;
}
unsafe { ComPtr::<I>::from_raw(obj.cast()) }
}
impl Drop for Vst3Plugin {
fn drop(&mut self) {
if self.processing {
unsafe { self.processor.setProcessing(0) };
}
if self.active_layout.is_some() {
unsafe { self.component.setActive(0) };
}
if let (Some(a), Some(b)) = (&self.component_cp, &self.controller_cp) {
unsafe {
a.disconnect(b.as_com_ref().as_ptr().cast());
b.disconnect(a.as_com_ref().as_ptr().cast());
}
}
if self.separate_controller {
unsafe { self.controller.terminate() };
}
unsafe { self.component.terminate() };
}
}
impl PluginCore for Vst3Plugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn active_layout(&self) -> Option<&BusLayout> {
self.active_layout.as_ref()
}
fn supported_layouts(&self) -> &[BusLayout] {
&self.layouts
}
fn parameter_count(&self) -> usize {
self.param_count
}
fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
if index >= self.param_count {
return Err(Error::InvalidParameter(index));
}
let mut info = empty_parameter_info();
let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
return Err(Error::InvalidParameter(index));
}
Ok(vst3_param_info_to_rack(&info))
}
fn parameter_value(&self, index: usize) -> Result<f64> {
if index >= self.param_count {
return Err(Error::InvalidParameter(index));
}
let mut info = empty_parameter_info();
let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
return Err(Error::InvalidParameter(index));
}
Ok(unsafe { self.controller.getParamNormalized(info.id) })
}
fn parameter_value_string(&self, index: usize, _value: f64) -> Result<String> {
let _ = index;
Err(Error::Other("vst3 parameter_value_string TODO".into()))
}
fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
if index >= self.param_count {
return Err(Error::InvalidParameter(index));
}
let mut info = empty_parameter_info();
let i32_index = i32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
if unsafe { self.controller.getParameterInfo(i32_index, &raw mut info) } != kResultOk {
return Err(Error::InvalidParameter(index));
}
let clamped = value.clamp(0.0, 1.0);
if unsafe { self.controller.setParamNormalized(info.id, clamped) } != kResultOk {
return Err(Error::Other(
"IEditController::setParamNormalized failed".into(),
));
}
Ok(())
}
fn preset_count(&self) -> usize {
0
}
fn preset_info(&self, index: usize) -> Result<PresetInfo> {
Err(Error::InvalidParameter(index))
}
fn load_preset(&mut self, _preset_number: i32) -> Result<()> {
Err(Error::Other("vst3 preset loading not yet wired".into()))
}
fn save_state(&self) -> Result<Vec<u8>> {
let stream = ComWrapper::new(MemoryStream::default());
let stream_ptr = stream
.to_com_ptr::<IBStream>()
.ok_or_else(|| Error::Other("MemoryStream missing IBStream IID".into()))?;
let status = unsafe { self.component.getState(stream_ptr.as_ptr()) };
if status != kResultOk {
return Err(Error::Other(format!(
"IComponent::getState returned {status}"
)));
}
Ok(stream.data.borrow().clone())
}
fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
let stream = ComWrapper::new(MemoryStream {
data: std::cell::RefCell::new(bytes.to_vec()),
position: std::cell::Cell::new(0),
});
let stream_ptr = stream
.to_com_ptr::<IBStream>()
.ok_or_else(|| Error::Other("MemoryStream missing IBStream IID".into()))?;
let status = unsafe { self.component.setState(stream_ptr.as_ptr()) };
if status != kResultOk {
return Err(Error::Other(format!(
"IComponent::setState returned {status}"
)));
}
Ok(())
}
fn activate(
&mut self,
layout: BusLayout,
sample_rate: f64,
max_block_size: usize,
) -> Result<()> {
let mut input_arr = STEREO_ARRANGEMENT;
let mut output_arr = STEREO_ARRANGEMENT;
let _ = unsafe {
self.processor
.setBusArrangements(&raw mut input_arr, 1, &raw mut output_arr, 1)
};
let mut setup = ProcessSetup {
#[allow(clippy::cast_possible_wrap)]
processMode: ProcessModes_::kRealtime as i32,
#[allow(clippy::cast_possible_wrap)]
symbolicSampleSize: SymbolicSampleSizes_::kSample32 as i32,
maxSamplesPerBlock: i32::try_from(max_block_size).unwrap_or(i32::MAX),
sampleRate: sample_rate,
};
if unsafe { self.processor.setupProcessing(&raw mut setup) } != kResultOk {
return Err(Error::Other(
"IAudioProcessor::setupProcessing failed".into(),
));
}
if unsafe { self.component.setActive(1) } != kResultOk {
return Err(Error::Other("IComponent::setActive(true) failed".into()));
}
if unsafe { self.processor.setProcessing(1) } != kResultOk {
return Err(Error::Other(
"IAudioProcessor::setProcessing(true) failed".into(),
));
}
self.processing = true;
self.active_layout = Some(layout);
Ok(())
}
fn deactivate(&mut self) {
if self.processing {
unsafe { self.processor.setProcessing(0) };
self.processing = false;
}
if self.active_layout.is_some() {
unsafe { self.component.setActive(0) };
}
self.active_layout = None;
}
fn is_active(&self) -> bool {
self.active_layout.is_some()
}
fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
if !self.info.has_editor {
return None;
}
Some(self)
}
}
impl truce_rack_core::editor::PluginEditor for Vst3Plugin {
fn open(
&mut self,
parent: truce_rack_core::editor::WindowHandle,
_scale: f64,
) -> truce_rack_core::error::Result<()> {
if self.editor_open {
return Ok(());
}
let view = unsafe { create_editor_view(&self.controller) }
.ok_or_else(|| Error::Other("IEditController::createView returned NULL".into()))?;
let (type_str, parent_ptr) = platform_type_for_handle(parent);
if unsafe { view.isPlatformTypeSupported(type_str) } != kResultOk {
return Err(Error::Other(
"IPlugView::isPlatformTypeSupported returned false".into(),
));
}
if unsafe { view.attached(parent_ptr, type_str) } != kResultOk {
return Err(Error::Other("IPlugView::attached returned non-OK".into()));
}
self.view = Some(view);
self.editor_open = true;
Ok(())
}
fn close(&mut self) {
if let Some(view) = self.view.take() {
unsafe { view.removed() };
}
self.editor_open = false;
}
fn is_open(&self) -> bool {
self.editor_open
}
fn size(&self) -> Option<(u32, u32)> {
let view = self.view.as_ref()?;
let mut rect = ViewRect {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
if unsafe { view.getSize(&raw mut rect) } != kResultOk {
return None;
}
let w = u32::try_from(rect.right - rect.left).ok()?;
let h = u32::try_from(rect.bottom - rect.top).ok()?;
Some((w, h))
}
fn is_resizable(&self) -> bool {
let Some(view) = self.view.as_ref() else {
return false;
};
unsafe { view.canResize() == kResultOk }
}
fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
let view = self.view.as_ref()?;
let mut rect = ViewRect {
left: 0,
top: 0,
right: i32::try_from(width).ok()?,
bottom: i32::try_from(height).ok()?,
};
let _ = unsafe { view.checkSizeConstraint(&raw mut rect) };
if unsafe { view.onSize(&raw mut rect) } != kResultOk {
return None;
}
let w = u32::try_from(rect.right - rect.left).ok()?;
let h = u32::try_from(rect.bottom - rect.top).ok()?;
Some((w, h))
}
fn show(&mut self) {
}
fn hide(&mut self) {
}
}
fn empty_parameter_info() -> Vst3ParameterInfo {
Vst3ParameterInfo {
id: 0,
title: [0; 128],
shortTitle: [0; 128],
units: [0; 128],
stepCount: 0,
defaultNormalizedValue: 0.0,
unitId: 0,
flags: 0,
}
}
fn vst3_param_info_to_rack(info: &Vst3ParameterInfo) -> ParameterInfo {
let name = string128_to_string(&info.title);
let short_name = string128_to_string(&info.shortTitle);
let unit = string128_to_string(&info.units);
let mut flags = truce_rack_core::info::ParameterFlags::empty();
if info.flags & ParameterFlags_::kIsBypass != 0 {
flags |= truce_rack_core::info::ParameterFlags::BYPASS;
}
if info.flags & ParameterFlags_::kCanAutomate != 0 {
flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
}
if info.flags & ParameterFlags_::kIsHidden != 0 {
flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
}
if info.flags & ParameterFlags_::kIsReadOnly != 0 {
flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
}
if info.flags & ParameterFlags_::kIsList != 0 {
flags |= truce_rack_core::info::ParameterFlags::ENUMERATED;
}
ParameterInfo {
id: info.id,
name,
short_name,
unit,
min: 0.0,
max: 1.0,
default: info.defaultNormalizedValue,
step_count: u32::try_from(info.stepCount).unwrap_or(0),
flags,
}
}
fn string128_to_string(buf: &[u16; 128]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..len])
}
#[allow(clippy::unnecessary_cast)]
fn build_vst3_context(t: &TransportInfo, sample_rate: f64) -> Vst3ProcessContext {
let mut ctx: Vst3ProcessContext = unsafe { std::mem::zeroed() };
ctx.sampleRate = sample_rate;
let mut state: u32 = 0;
if let Some(tempo) = t.tempo_bpm {
ctx.tempo = tempo;
state |= StatesAndFlags_::kTempoValid as u32;
}
if let Some((num, den)) = t.time_signature {
ctx.timeSigNumerator = i32::try_from(num).unwrap_or(4);
ctx.timeSigDenominator = i32::try_from(den).unwrap_or(4);
state |= StatesAndFlags_::kTimeSigValid as u32;
}
if let Some(beats) = t.song_position_beats {
ctx.projectTimeMusic = beats;
state |= StatesAndFlags_::kProjectTimeMusicValid as u32;
}
if let Some(samples) = t.song_position_samples {
ctx.projectTimeSamples = samples;
}
if let Some(bar) = t.bar_start_beats {
ctx.barPositionMusic = bar;
state |= StatesAndFlags_::kBarPositionValid as u32;
}
if t.playing {
state |= StatesAndFlags_::kPlaying as u32;
}
if t.recording {
state |= StatesAndFlags_::kRecording as u32;
}
if t.loop_active {
state |= StatesAndFlags_::kCycleActive as u32;
}
ctx.state = state;
ctx
}
impl Plugin<f32> for Vst3Plugin {
fn process(
&mut self,
buffer: &mut AudioBuffer<'_, f32>,
events: &EventList,
context: &mut ProcessContext<'_>,
) -> Result<ProcessStatus> {
if !self.is_active() {
return Err(Error::NotActivated);
}
let frames = buffer.num_frames();
let mut translated = EventBuffer::default();
for event in events {
translated.push_rack(event);
}
let input_events_wrapper = ComWrapper::new(EventList3 {
events: std::cell::RefCell::new(translated.events),
});
let input_events_ptr = input_events_wrapper
.to_com_ptr::<IEventList>()
.ok_or_else(|| Error::Other("EventList3 missing IEventList IID".into()))?;
let output_events_wrapper = ComWrapper::new(EventList3::default());
let output_events_ptr = output_events_wrapper
.to_com_ptr::<IEventList>()
.ok_or_else(|| Error::Other("EventList3 missing IEventList IID".into()))?;
let main_inputs = buffer.main_inputs();
let mut input_ptrs: Vec<*mut f32> =
main_inputs.iter().map(|c| c.as_ptr().cast_mut()).collect();
let mut input_bus = AudioBusBuffers {
numChannels: i32::try_from(input_ptrs.len()).unwrap_or(0),
silenceFlags: 0,
__field0: AudioBusBuffers__type0 {
channelBuffers32: input_ptrs.as_mut_ptr(),
},
};
let main_outputs = buffer.main_outputs();
let mut output_ptrs: Vec<*mut f32> =
main_outputs.iter_mut().map(|c| c.as_mut_ptr()).collect();
let mut output_bus = AudioBusBuffers {
numChannels: i32::try_from(output_ptrs.len()).unwrap_or(0),
silenceFlags: 0,
__field0: AudioBusBuffers__type0 {
channelBuffers32: output_ptrs.as_mut_ptr(),
},
};
let mut process_context = context
.transport
.map(|t| build_vst3_context(&t, context.sample_rate));
let mut data = ProcessData {
#[allow(clippy::cast_possible_wrap)]
processMode: ProcessModes_::kRealtime as i32,
#[allow(clippy::cast_possible_wrap)]
symbolicSampleSize: SymbolicSampleSizes_::kSample32 as i32,
numSamples: i32::try_from(frames).unwrap_or(i32::MAX),
numInputs: 1,
numOutputs: 1,
inputs: &raw mut input_bus,
outputs: &raw mut output_bus,
inputParameterChanges: ptr::null_mut::<IParameterChanges>(),
outputParameterChanges: ptr::null_mut::<IParameterChanges>(),
inputEvents: input_events_ptr.as_ptr(),
outputEvents: output_events_ptr.as_ptr(),
processContext: process_context
.as_mut()
.map_or(ptr::null_mut(), std::ptr::from_mut),
};
let processor_ptr = self.processor.as_ptr();
let status = run_audio_block_with::<Vst3Plugin, i32>(FORMAT, -1, || unsafe {
((*(*processor_ptr).vtbl).process)(processor_ptr, &raw mut data)
});
if status == kResultOk {
Ok(ProcessStatus::Continue)
} else {
Ok(ProcessStatus::Error)
}
}
}
#[derive(Default)]
struct EventBuffer {
events: Vec<Event>,
}
impl EventBuffer {
fn push_rack(&mut self, event: &truce_rack_core::events::Event) {
use truce_rack_core::events::{EventBody, MidiData};
let offset = i32::try_from(event.sample_offset).unwrap_or(i32::MAX);
let EventBody::Midi(body) = event.body else {
return;
};
let header = |type_: u16| Event {
busIndex: 0,
sampleOffset: offset,
ppqPosition: 0.0,
flags: 0,
r#type: type_,
__field0: Event__type0 {
noteOn: NoteOnEvent {
channel: 0,
pitch: 0,
tuning: 0.0,
velocity: 0.0,
length: 0,
noteId: -1,
},
},
};
match body {
MidiData::NoteOn {
channel,
note,
velocity,
} => {
let mut ev = header(u16::try_from(EventTypes_::kNoteOnEvent).unwrap_or(0));
ev.__field0 = Event__type0 {
noteOn: NoteOnEvent {
channel: i16::from(channel),
pitch: i16::from(note),
tuning: 0.0,
velocity: f32::from(velocity) / 127.0,
length: 0,
noteId: -1,
},
};
self.events.push(ev);
}
MidiData::NoteOff {
channel,
note,
velocity,
} => {
let mut ev = header(u16::try_from(EventTypes_::kNoteOffEvent).unwrap_or(0));
ev.__field0 = Event__type0 {
noteOff: NoteOffEvent {
channel: i16::from(channel),
pitch: i16::from(note),
velocity: f32::from(velocity) / 127.0,
noteId: -1,
tuning: 0.0,
},
};
self.events.push(ev);
}
MidiData::PolyAftertouch {
channel,
note,
pressure,
} => {
let mut ev = header(u16::try_from(EventTypes_::kPolyPressureEvent).unwrap_or(0));
ev.__field0 = Event__type0 {
polyPressure: PolyPressureEvent {
channel: i16::from(channel),
pitch: i16::from(note),
pressure: f32::from(pressure) / 127.0,
noteId: -1,
},
};
self.events.push(ev);
}
_ => {}
}
}
}
#[derive(Default)]
struct EventList3 {
events: std::cell::RefCell<Vec<Event>>,
}
impl Class for EventList3 {
type Interfaces = (IEventList,);
}
#[allow(clippy::cast_sign_loss)]
impl IEventListTrait for EventList3 {
unsafe fn getEventCount(&self) -> i32 {
i32::try_from(self.events.borrow().len()).unwrap_or(i32::MAX)
}
unsafe fn getEvent(&self, index: i32, out: *mut Event) -> i32 {
if out.is_null() || index < 0 {
return -1;
}
let events = self.events.borrow();
let Some(event) = events.get(index as usize) else {
return -1;
};
unsafe { *out = *event };
kResultOk
}
unsafe fn addEvent(&self, event: *mut Event) -> i32 {
if event.is_null() {
return -1;
}
self.events.borrow_mut().push(unsafe { *event });
kResultOk
}
}
#[derive(Default)]
struct MemoryStream {
data: std::cell::RefCell<Vec<u8>>,
position: std::cell::Cell<usize>,
}
impl Class for MemoryStream {
type Interfaces = (IBStream,);
}
impl IBStreamTrait for MemoryStream {
unsafe fn read(
&self,
buffer: *mut std::ffi::c_void,
num_bytes: i32,
num_bytes_read: *mut i32,
) -> i32 {
if buffer.is_null() || num_bytes < 0 {
return -1;
}
let pos = self.position.get();
#[allow(clippy::cast_sign_loss)]
let want = num_bytes as usize;
let data = self.data.borrow();
let available = data.len().saturating_sub(pos);
let take = want.min(available);
if take > 0 {
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), buffer.cast::<u8>(), take);
}
}
self.position.set(pos + take);
if !num_bytes_read.is_null() {
unsafe {
*num_bytes_read = i32::try_from(take).unwrap_or(i32::MAX);
}
}
kResultOk
}
unsafe fn write(
&self,
buffer: *mut std::ffi::c_void,
num_bytes: i32,
num_bytes_written: *mut i32,
) -> i32 {
if buffer.is_null() || num_bytes < 0 {
return -1;
}
let pos = self.position.get();
#[allow(clippy::cast_sign_loss)]
let want = num_bytes as usize;
let mut data = self.data.borrow_mut();
if data.len() < pos + want {
data.resize(pos + want, 0);
}
unsafe {
std::ptr::copy_nonoverlapping(buffer.cast::<u8>(), data.as_mut_ptr().add(pos), want);
}
self.position.set(pos + want);
if !num_bytes_written.is_null() {
unsafe {
*num_bytes_written = i32::try_from(want).unwrap_or(i32::MAX);
}
}
kResultOk
}
#[allow(clippy::cast_possible_wrap)]
unsafe fn seek(&self, pos: i64, mode: i32, result: *mut i64) -> i32 {
let data_len = self.data.borrow().len() as i64;
let current = self.position.get() as i64;
let new_pos = match mode {
0 => pos,
1 => current + pos,
2 => data_len + pos,
_ => return -1,
};
if new_pos < 0 {
return -1;
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
self.position.set(new_pos as usize);
if !result.is_null() {
unsafe {
*result = new_pos;
}
}
kResultOk
}
#[allow(clippy::cast_possible_wrap)]
unsafe fn tell(&self, pos: *mut i64) -> i32 {
if pos.is_null() {
return -1;
}
unsafe {
*pos = self.position.get() as i64;
}
kResultOk
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tuid_hex_roundtrip() {
let cid: TUID = [
0x01, 0x23, 0x45, 0x67, -0x10, -0x10, -0x10, -0x10, 0, 0, 0, 0, 0, 0, 0, 0,
];
let hex = tuid_to_hex(&cid);
let parsed = hex_to_tuid(&hex).expect("parse");
assert_eq!(parsed, cid);
}
#[test]
fn char8_skips_trailing_nul() {
let mut arr = [0i8; 16];
for (i, &b) in b"Hello".iter().enumerate() {
arr[i] = i8::try_from(b).expect("ASCII byte fits in i8");
}
assert_eq!(char8_array_to_string(&arr), "Hello");
}
}