diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index ee3aa03f..3cf0aae5 100644 --- a/src/wrapper/vst3.rs +++ b/src/wrapper/vst3.rs @@ -14,12 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -// The VST3 macro generates an `allocate()` function for initializing the struct, so Clippy will -// complain as soon as a struct has more than 8 fields -#![allow(clippy::too_many_arguments)] - -use crossbeam::atomic::AtomicCell; -use lazy_static::lazy_static; use parking_lot::{RwLock, RwLockWriteGuard}; use raw_window_handle::RawWindowHandle; use std::any::Any; @@ -29,24 +23,26 @@ use std::ffi::{c_void, CStr}; use std::marker::PhantomData; use std::mem::{self, MaybeUninit}; use std::ptr; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::atomic::Ordering; use std::sync::Arc; use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tresult, TBool}; use vst3_sys::base::{IBStream, IPluginBase, IPluginFactory, IPluginFactory2, IPluginFactory3}; use vst3_sys::gui::IPlugView; use vst3_sys::utils::SharedVstPtr; use vst3_sys::vst::{ - IAudioProcessor, IComponent, IComponentHandler, IEditController, IEventList, IParamValueQueue, - IParameterChanges, TChar, + IAudioProcessor, IComponent, IEditController, IEventList, IParamValueQueue, IParameterChanges, + TChar, }; use vst3_sys::VST3; use widestring::U16CStr; +mod inner; #[macro_use] mod util; -use crate::buffer::Buffer; -use crate::context::{EventLoop, GuiContext, MainThreadExecutor, OsEventLoop, ProcessContext}; +use self::inner::WrapperInner; +use self::util::{VstPtr, BYPASS_PARAM_HASH}; +use crate::context::{EventLoop, ProcessContext}; use crate::param::internals::ParamPtr; use crate::param::range::Range; use crate::param::Param; @@ -54,7 +50,7 @@ use crate::plugin::{ BufferConfig, BusConfig, Editor, NoteEvent, Plugin, ProcessStatus, Vst3Plugin, }; use crate::wrapper::state::{ParamValue, State}; -use crate::wrapper::util::{hash_param_id, process_wrapper, strlcpy, u16strlcpy}; +use crate::wrapper::util::{process_wrapper, strlcpy, u16strlcpy}; use crate::ParentWindowHandle; // Alias needed for the VST3 attribute macro @@ -78,84 +74,8 @@ const VST3_PLATFORM_UIVIEW: &str = "UIView"; #[allow(unused)] const VST3_PLATFORM_X11_WINDOW: &str = "X11EmbedWindowID"; -/// Right now the wrapper adds its own bypass parameter. -/// -/// TODO: Actually use this parameter. -const BYPASS_PARAM_ID: &str = "bypass"; -lazy_static! { - static ref BYPASS_PARAM_HASH: u32 = hash_param_id(BYPASS_PARAM_ID); -} - -/// The actual wrapper bits. We need this as an `Arc` so we can safely use our event loop API. -/// Since we can't combine that with VST3's interior reference counting this just has to be moved to -/// its own struct. -struct WrapperInner { - /// The wrapped plugin instance. - plugin: RwLock

, - /// The plugin's editor, if it has one. This object does not do anything on its own, but we need - /// to instantiate this in advance so we don't need to lock the entire [Plugin] object when - /// creating an editor. - editor: Option>, - - /// The host's `IComponentHandler` instance, if passed through - /// `IEditController::set_component_handler`. - component_handler: RwLock>>, - - /// A realtime-safe task queue so the plugin can schedule tasks that need to be run later on the - /// GUI thread. - /// - /// This RwLock is only needed because it has to be initialized late. There is no reason to - /// mutably borrow the event loop, so reads will never be contested. - /// - /// TODO: Is there a better type for Send+Sync late initializaiton? - event_loop: RwLock>>, - - /// Whether the plugin is currently processing audio. In other words, the last state - /// `IAudioProcessor::setActive()` has been called with. - is_processing: AtomicBool, - /// The current bus configuration, modified through `IAudioProcessor::setBusArrangements()`. - current_bus_config: AtomicCell, - /// The current buffer configuration, containing the sample rate and the maximum block size. - /// Will be set in `IAudioProcessor::setupProcessing()`. - current_buffer_config: AtomicCell>, - /// Whether the plugin is currently bypassed. This is not yet integrated with the `Plugin` - /// trait. - bypass_state: AtomicBool, - /// The last process status returned by the plugin. This is used for tail handling. - last_process_status: AtomicCell, - /// The current latency in samples, as set by the plugin through the [ProcessContext]. - current_latency: AtomicU32, - /// Contains slices for the plugin's outputs. You can't directly create a nested slice form - /// apointer to pointers, so this needs to be preallocated in the setup call and kept around - /// between process calls. This buffer owns the vector, because otherwise it would need to store - /// a mutable reference to the data contained in this mutex. - output_buffer: RwLock>, - /// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. - /// - /// TODO: Maybe load these lazily at some point instead of needing to spool them all to this - /// queue first - input_events: RwLock>, - - /// The keys from `param_map` in a stable order. - param_hashes: Vec, - /// A mapping from parameter ID hashes (obtained from the string parameter IDs) to pointers to - /// parameters belonging to the plugin. As long as `plugin` does not get recreated, these - /// addresses will remain stable, as they are obtained from a pinned object. - param_by_hash: HashMap, - /// The default normalized parameter value for every parameter in `param_ids`. We need to store - /// this in case the host requeries the parmaeter later. - param_defaults_normalized: Vec, - /// Mappings from string parameter indentifiers to parameter hashes. Useful for debug logging - /// and when storing and restorign plugin state. - param_id_to_hash: HashMap<&'static str, u32>, - /// The inverse mapping from [Self::param_by_hash]. This is needed to be able to have an - /// ergonomic parameter setting API that uses references to the parameters instead of having to - /// add a setter function to the parameter (or even worse, have it be completely untyped). - param_ptr_to_hash: HashMap, -} - #[VST3(implements(IComponent, IEditController, IAudioProcessor))] -pub(crate) struct Wrapper { +struct Wrapper { inner: Arc>, } @@ -171,7 +91,7 @@ struct WrapperView { /// A [ProcessContext] implementation for the wrapper. This is a separate object so it can hold on /// to lock guards for event queues. Otherwise reading these events would require constant /// unnecessary atomic operations to lock the uncontested RwLocks. -struct WrapperProcessContext<'a, P: Plugin> { +pub(crate) struct WrapperProcessContext<'a, P: Plugin> { inner: &'a WrapperInner

, input_events_guard: RwLockWriteGuard<'a, VecDeque>, } @@ -182,7 +102,7 @@ impl ProcessContext for WrapperProcessContext<'_, P> { let old_latency = self.inner.current_latency.swap(samples, Ordering::SeqCst); if old_latency != samples { let task_posted = unsafe { self.inner.event_loop.read().assume_init_ref() } - .do_maybe_async(Task::TriggerRestart( + .do_maybe_async(inner::Task::TriggerRestart( vst3_sys::vst::RestartFlags::kLatencyChanged as i32, )); nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); @@ -194,226 +114,6 @@ impl ProcessContext for WrapperProcessContext<'_, P> { } } -// We can't use a nice standalone context object for this as we don't have any control over the -// lifetime since we currently try to stay GUI framework agnostic. Because of that, the only -// alternative is to pass an `Arc` to the plugin and hope it doesn't do anything -// weird with it. -impl GuiContext for WrapperInner

{ - // All of these functions are supposed to be called from the main thread, so we'll put some - // trust in the caller and assume that this is indeed the case - unsafe fn raw_begin_set_parameter(&self, param: ParamPtr) { - match &*self.component_handler.read() { - Some(handler) => match self.param_ptr_to_hash.get(¶m) { - Some(hash) => { - handler.begin_edit(*hash); - } - None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), - }, - None => nih_debug_assert_failure!("Component handler not yet set"), - } - } - - unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32) { - match &*self.component_handler.read() { - Some(handler) => match self.param_ptr_to_hash.get(¶m) { - Some(hash) => { - // Only update the parameters manually if the host is not processing audio. If - // the plugin is currently processing audio, the host will pass this change back - // to the plugin in the audio callback. This also prevents the values from - // changing in the middle of the process callback, which would be unsound. - if !self.is_processing.load(Ordering::SeqCst) { - self.set_normalized_value_by_hash( - *hash, - normalized, - self.current_buffer_config.load().map(|c| c.sample_rate), - ); - } - - handler.perform_edit(*hash, normalized as f64); - } - None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), - }, - None => nih_debug_assert_failure!("Component handler not yet set"), - } - } - - unsafe fn raw_end_set_parameter(&self, param: ParamPtr) { - match &*self.component_handler.read() { - Some(handler) => match self.param_ptr_to_hash.get(¶m) { - Some(hash) => { - handler.end_edit(*hash); - } - None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), - }, - None => nih_debug_assert_failure!("Component handler not yet set"), - } - } -} - -/// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking -/// realtime safe way (either a random thread or `IRunLoop` on Linux, the OS' message loop on -/// Windows and macOS). -#[derive(Debug, Clone)] -enum Task { - /// Trigger a restart with the given restart flags. This is a bit set of the flags from - /// [vst3_sys::vst::RestartFlags]. - TriggerRestart(i32), -} - -/// Send+Sync wrapper for these interface pointers. -#[repr(transparent)] -struct VstPtr { - ptr: vst3_sys::VstPtr, -} - -impl std::ops::Deref for VstPtr { - type Target = vst3_sys::VstPtr; - - fn deref(&self) -> &Self::Target { - &self.ptr - } -} - -impl From> for VstPtr { - fn from(ptr: vst3_sys::VstPtr) -> Self { - Self { ptr } - } -} - -/// SAFETY: Sharing these pointers across thread is s safe as they have internal atomic reference -/// counting, so as long as a `VstPtr` handle exists the object will stay alive. -unsafe impl Send for VstPtr {} -unsafe impl Sync for VstPtr {} - -impl WrapperInner

{ - // XXX: The unsafe blocks in this function are unnecessary. but rust-analyzer gets a bit - // confused by all of these vtables - #[allow(unused_unsafe)] - pub fn new() -> Arc { - let plugin = RwLock::new(P::default()); - let editor = plugin.read().editor().map(Arc::from); - - let mut wrapper = Self { - plugin, - editor, - - component_handler: RwLock::new(None), - - event_loop: RwLock::new(MaybeUninit::uninit()), - - is_processing: AtomicBool::new(false), - // Some hosts, like the current version of Bitwig and Ardour at the time of writing, - // will try using the plugin's default not yet initialized bus arrangement. Because of - // that, we'll always initialize this configuration even before the host requests a - // channel layout. - current_bus_config: AtomicCell::new(BusConfig { - num_input_channels: P::DEFAULT_NUM_INPUTS, - num_output_channels: P::DEFAULT_NUM_OUTPUTS, - }), - current_buffer_config: AtomicCell::new(None), - bypass_state: AtomicBool::new(false), - last_process_status: AtomicCell::new(ProcessStatus::Normal), - current_latency: AtomicU32::new(0), - output_buffer: RwLock::new(Buffer::default()), - input_events: RwLock::new(VecDeque::with_capacity(512)), - - param_hashes: Vec::new(), - param_by_hash: HashMap::new(), - param_defaults_normalized: Vec::new(), - param_id_to_hash: HashMap::new(), - param_ptr_to_hash: HashMap::new(), - }; - - // This is a mapping from the parameter IDs specified by the plugin to pointers to thsoe - // parameters. Since the object returned by `params()` is pinned, these pointers are safe to - // dereference as long as `wrapper.plugin` is alive - // XXX: This unsafe block is unnecessary. rust-analyzer gets a bit confused and this this - // `read()` function is from `IBStream` which it definitely is not. - let param_map = unsafe { wrapper.plugin.read() }.params().param_map(); - let param_ids = unsafe { wrapper.plugin.read() }.params().param_ids(); - nih_debug_assert!( - !param_map.contains_key(BYPASS_PARAM_ID), - "The wrapper alread yadds its own bypass parameter" - ); - - // Only calculate these hashes once, and in the stable order defined by the plugin - let param_id_hashes_ptrs: Vec<_> = param_ids - .iter() - .filter_map(|id| { - let param_ptr = param_map.get(id)?; - Some((id, hash_param_id(id), param_ptr)) - }) - .collect(); - wrapper.param_hashes = param_id_hashes_ptrs - .iter() - .map(|&(_, hash, _)| hash) - .collect(); - wrapper.param_by_hash = param_id_hashes_ptrs - .iter() - .map(|&(_, hash, ptr)| (hash, *ptr)) - .collect(); - wrapper.param_defaults_normalized = param_id_hashes_ptrs - .iter() - .map(|&(_, _, ptr)| unsafe { ptr.normalized_value() }) - .collect(); - wrapper.param_id_to_hash = param_id_hashes_ptrs - .iter() - .map(|&(id, hash, _)| (*id, hash)) - .collect(); - wrapper.param_ptr_to_hash = param_id_hashes_ptrs - .into_iter() - .map(|(_, hash, ptr)| (*ptr, hash)) - .collect(); - - // FIXME: Right now this is safe, but if we are going to have a singleton main thread queue - // serving multiple plugin instances, Arc can't be used because its reference count - // is separate from the internal COM-style reference count. - let wrapper: Arc> = wrapper.into(); - // XXX: This unsafe block is unnecessary. rust-analyzer gets a bit confused and this this - // `write()` function is from `IBStream` which it definitely is not. - *unsafe { wrapper.event_loop.write() } = - MaybeUninit::new(OsEventLoop::new_and_spawn(Arc::downgrade(&wrapper))); - - wrapper - } - - pub fn make_process_context(&self) -> WrapperProcessContext<'_, P> { - WrapperProcessContext { - inner: self, - input_events_guard: self.input_events.write(), - } - } - - /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter - /// update. The same rate is for updating parameter smoothing. - unsafe fn set_normalized_value_by_hash( - &self, - hash: u32, - normalized_value: f32, - sample_rate: Option, - ) -> tresult { - if hash == *BYPASS_PARAM_HASH { - self.bypass_state - .store(normalized_value >= 0.5, Ordering::SeqCst); - - kResultOk - } else if let Some(param_ptr) = self.param_by_hash.get(&hash) { - // Also update the parameter's smoothing if applicable - match (param_ptr, sample_rate) { - (_, Some(sample_rate)) => { - param_ptr.set_normalized_value(normalized_value); - param_ptr.update_smoother(sample_rate, false); - } - _ => param_ptr.set_normalized_value(normalized_value), - } - - kResultOk - } else { - kInvalidArgument - } - } -} - impl Wrapper

{ pub fn new() -> Box { Self::allocate(WrapperInner::new()) @@ -426,25 +126,6 @@ impl WrapperView

{ } } -impl MainThreadExecutor for WrapperInner

{ - unsafe fn execute(&self, task: Task) { - // This function is always called from the main thread - // TODO: When we add GUI resizing and context menus, this should propagate those events to - // `IRunLoop` on Linux to keep REAPER happy. That does mean a double spool, but we can - // come up with a nicer solution to handle that later (can always add a separate - // function for checking if a to be scheduled task can be handled right ther and - // then). - match task { - Task::TriggerRestart(flags) => match &*self.component_handler.read() { - Some(handler) => { - handler.restart_component(flags); - } - None => nih_debug_assert_failure!("Component handler not yet set"), - }, - } - } -} - impl IPluginBase for Wrapper

{ unsafe fn initialize(&self, _context: *mut c_void) -> tresult { // We currently don't need or allow any initialization logic @@ -642,7 +323,7 @@ impl IComponent for Wrapper

{ .map(|c| c.sample_rate); for (param_id_str, param_value) in state.params { // Handle the bypass parameter separately - if param_id_str == BYPASS_PARAM_ID { + if param_id_str == util::BYPASS_PARAM_ID { match param_value { ParamValue::Bool(b) => self.inner.bypass_state.store(b, Ordering::SeqCst), _ => nih_debug_assert_failure!( @@ -740,7 +421,7 @@ impl IComponent for Wrapper

{ // Don't forget about the bypass parameter params.insert( - BYPASS_PARAM_ID.to_string(), + util::BYPASS_PARAM_ID.to_string(), ParamValue::Bool(self.inner.bypass_state.load(Ordering::SeqCst)), ); diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs new file mode 100644 index 00000000..38e715b6 --- /dev/null +++ b/src/wrapper/vst3/inner.rs @@ -0,0 +1,314 @@ +// nih-plug: plugins, but rewritten in Rust +// Copyright (C) 2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crossbeam::atomic::AtomicCell; +use parking_lot::RwLock; +use std::collections::{HashMap, VecDeque}; +use std::mem::MaybeUninit; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Arc; +use vst3_sys::base::{kInvalidArgument, kResultOk, tresult}; +use vst3_sys::vst::IComponentHandler; + +use super::util::{VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID}; +use super::WrapperProcessContext; +use crate::buffer::Buffer; +use crate::context::{EventLoop, GuiContext, MainThreadExecutor, OsEventLoop}; +use crate::param::internals::ParamPtr; +use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, Plugin, ProcessStatus}; +use crate::wrapper::util::hash_param_id; + +/// The actual wrapper bits. We need this as an `Arc` so we can safely use our event loop API. +/// Since we can't combine that with VST3's interior reference counting this just has to be moved to +/// its own struct. +pub(crate) struct WrapperInner { + /// The wrapped plugin instance. + pub plugin: RwLock

, + /// The plugin's editor, if it has one. This object does not do anything on its own, but we need + /// to instantiate this in advance so we don't need to lock the entire [Plugin] object when + /// creating an editor. + pub editor: Option>, + + /// The host's `IComponentHandler` instance, if passed through + /// `IEditController::set_component_handler`. + pub component_handler: RwLock>>, + + /// A realtime-safe task queue so the plugin can schedule tasks that need to be run later on the + /// GUI thread. + /// + /// This RwLock is only needed because it has to be initialized late. There is no reason to + /// mutably borrow the event loop, so reads will never be contested. + /// + /// TODO: Is there a better type for Send+Sync late initializaiton? + pub event_loop: RwLock>>, + + /// Whether the plugin is currently processing audio. In other words, the last state + /// `IAudioProcessor::setActive()` has been called with. + pub is_processing: AtomicBool, + /// The current bus configuration, modified through `IAudioProcessor::setBusArrangements()`. + pub current_bus_config: AtomicCell, + /// The current buffer configuration, containing the sample rate and the maximum block size. + /// Will be set in `IAudioProcessor::setupProcessing()`. + pub current_buffer_config: AtomicCell>, + /// Whether the plugin is currently bypassed. This is not yet integrated with the `Plugin` + /// trait. + pub bypass_state: AtomicBool, + /// The last process status returned by the plugin. This is used for tail handling. + pub last_process_status: AtomicCell, + /// The current latency in samples, as set by the plugin through the [ProcessContext]. + pub current_latency: AtomicU32, + /// Contains slices for the plugin's outputs. You can't directly create a nested slice form + /// apointer to pointers, so this needs to be preallocated in the setup call and kept around + /// between process calls. This buffer owns the vector, because otherwise it would need to store + /// a mutable reference to the data contained in this mutex. + pub output_buffer: RwLock>, + /// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. + /// + /// TODO: Maybe load these lazily at some point instead of needing to spool them all to this + /// queue first + pub input_events: RwLock>, + + /// The keys from `param_map` in a stable order. + pub param_hashes: Vec, + /// A mapping from parameter ID hashes (obtained from the string parameter IDs) to pointers to + /// parameters belonging to the plugin. As long as `plugin` does not get recreated, these + /// addresses will remain stable, as they are obtained from a pinned object. + pub param_by_hash: HashMap, + /// The default normalized parameter value for every parameter in `param_ids`. We need to store + /// this in case the host requeries the parmaeter later. + pub param_defaults_normalized: Vec, + /// Mappings from string parameter indentifiers to parameter hashes. Useful for debug logging + /// and when storing and restorign plugin state. + pub param_id_to_hash: HashMap<&'static str, u32>, + /// The inverse mapping from [Self::param_by_hash]. This is needed to be able to have an + /// ergonomic parameter setting API that uses references to the parameters instead of having to + /// add a setter function to the parameter (or even worse, have it be completely untyped). + pub param_ptr_to_hash: HashMap, +} + +/// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking +/// realtime safe way (either a random thread or `IRunLoop` on Linux, the OS' message loop on +/// Windows and macOS). +#[derive(Debug, Clone)] +pub enum Task { + /// Trigger a restart with the given restart flags. This is a bit set of the flags from + /// [vst3_sys::vst::RestartFlags]. + TriggerRestart(i32), +} + +impl WrapperInner

{ + // XXX: The unsafe blocks in this function are unnecessary. but rust-analyzer gets a bit + // confused by all of these vtables + #[allow(unused_unsafe)] + pub fn new() -> Arc { + let plugin = RwLock::new(P::default()); + let editor = plugin.read().editor().map(Arc::from); + + let mut wrapper = Self { + plugin, + editor, + + component_handler: RwLock::new(None), + + event_loop: RwLock::new(MaybeUninit::uninit()), + + is_processing: AtomicBool::new(false), + // Some hosts, like the current version of Bitwig and Ardour at the time of writing, + // will try using the plugin's default not yet initialized bus arrangement. Because of + // that, we'll always initialize this configuration even before the host requests a + // channel layout. + current_bus_config: AtomicCell::new(BusConfig { + num_input_channels: P::DEFAULT_NUM_INPUTS, + num_output_channels: P::DEFAULT_NUM_OUTPUTS, + }), + current_buffer_config: AtomicCell::new(None), + bypass_state: AtomicBool::new(false), + last_process_status: AtomicCell::new(ProcessStatus::Normal), + current_latency: AtomicU32::new(0), + output_buffer: RwLock::new(Buffer::default()), + input_events: RwLock::new(VecDeque::with_capacity(512)), + + param_hashes: Vec::new(), + param_by_hash: HashMap::new(), + param_defaults_normalized: Vec::new(), + param_id_to_hash: HashMap::new(), + param_ptr_to_hash: HashMap::new(), + }; + + // This is a mapping from the parameter IDs specified by the plugin to pointers to thsoe + // parameters. Since the object returned by `params()` is pinned, these pointers are safe to + // dereference as long as `wrapper.plugin` is alive + // XXX: This unsafe block is unnecessary. rust-analyzer gets a bit confused and this this + // `read()` function is from `IBStream` which it definitely is not. + let param_map = unsafe { wrapper.plugin.read() }.params().param_map(); + let param_ids = unsafe { wrapper.plugin.read() }.params().param_ids(); + nih_debug_assert!( + !param_map.contains_key(BYPASS_PARAM_ID), + "The wrapper alread yadds its own bypass parameter" + ); + + // Only calculate these hashes once, and in the stable order defined by the plugin + let param_id_hashes_ptrs: Vec<_> = param_ids + .iter() + .filter_map(|id| { + let param_ptr = param_map.get(id)?; + Some((id, hash_param_id(id), param_ptr)) + }) + .collect(); + wrapper.param_hashes = param_id_hashes_ptrs + .iter() + .map(|&(_, hash, _)| hash) + .collect(); + wrapper.param_by_hash = param_id_hashes_ptrs + .iter() + .map(|&(_, hash, ptr)| (hash, *ptr)) + .collect(); + wrapper.param_defaults_normalized = param_id_hashes_ptrs + .iter() + .map(|&(_, _, ptr)| unsafe { ptr.normalized_value() }) + .collect(); + wrapper.param_id_to_hash = param_id_hashes_ptrs + .iter() + .map(|&(id, hash, _)| (*id, hash)) + .collect(); + wrapper.param_ptr_to_hash = param_id_hashes_ptrs + .into_iter() + .map(|(_, hash, ptr)| (*ptr, hash)) + .collect(); + + // FIXME: Right now this is safe, but if we are going to have a singleton main thread queue + // serving multiple plugin instances, Arc can't be used because its reference count + // is separate from the internal COM-style reference count. + let wrapper: Arc> = wrapper.into(); + // XXX: This unsafe block is unnecessary. rust-analyzer gets a bit confused and this this + // `write()` function is from `IBStream` which it definitely is not. + *unsafe { wrapper.event_loop.write() } = + MaybeUninit::new(OsEventLoop::new_and_spawn(Arc::downgrade(&wrapper))); + + wrapper + } + + pub fn make_process_context(&self) -> WrapperProcessContext<'_, P> { + WrapperProcessContext { + inner: self, + input_events_guard: self.input_events.write(), + } + } + + /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter + /// update. The same rate is for updating parameter smoothing. + pub unsafe fn set_normalized_value_by_hash( + &self, + hash: u32, + normalized_value: f32, + sample_rate: Option, + ) -> tresult { + if hash == *BYPASS_PARAM_HASH { + self.bypass_state + .store(normalized_value >= 0.5, Ordering::SeqCst); + + kResultOk + } else if let Some(param_ptr) = self.param_by_hash.get(&hash) { + // Also update the parameter's smoothing if applicable + match (param_ptr, sample_rate) { + (_, Some(sample_rate)) => { + param_ptr.set_normalized_value(normalized_value); + param_ptr.update_smoother(sample_rate, false); + } + _ => param_ptr.set_normalized_value(normalized_value), + } + + kResultOk + } else { + kInvalidArgument + } + } +} + +// We can't use a nice standalone context object for this as we don't have any control over the +// lifetime since we currently try to stay GUI framework agnostic. Because of that, the only +// alternative is to pass an `Arc` to the plugin and hope it doesn't do anything +// weird with it. +impl GuiContext for WrapperInner

{ + // All of these functions are supposed to be called from the main thread, so we'll put some + // trust in the caller and assume that this is indeed the case + unsafe fn raw_begin_set_parameter(&self, param: ParamPtr) { + match &*self.component_handler.read() { + Some(handler) => match self.param_ptr_to_hash.get(¶m) { + Some(hash) => { + handler.begin_edit(*hash); + } + None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), + }, + None => nih_debug_assert_failure!("Component handler not yet set"), + } + } + + unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32) { + match &*self.component_handler.read() { + Some(handler) => match self.param_ptr_to_hash.get(¶m) { + Some(hash) => { + // Only update the parameters manually if the host is not processing audio. If + // the plugin is currently processing audio, the host will pass this change back + // to the plugin in the audio callback. This also prevents the values from + // changing in the middle of the process callback, which would be unsound. + if !self.is_processing.load(Ordering::SeqCst) { + self.set_normalized_value_by_hash( + *hash, + normalized, + self.current_buffer_config.load().map(|c| c.sample_rate), + ); + } + + handler.perform_edit(*hash, normalized as f64); + } + None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), + }, + None => nih_debug_assert_failure!("Component handler not yet set"), + } + } + + unsafe fn raw_end_set_parameter(&self, param: ParamPtr) { + match &*self.component_handler.read() { + Some(handler) => match self.param_ptr_to_hash.get(¶m) { + Some(hash) => { + handler.end_edit(*hash); + } + None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), + }, + None => nih_debug_assert_failure!("Component handler not yet set"), + } + } +} + +impl MainThreadExecutor for WrapperInner

{ + unsafe fn execute(&self, task: Task) { + // This function is always called from the main thread + // TODO: When we add GUI resizing and context menus, this should propagate those events to + // `IRunLoop` on Linux to keep REAPER happy. That does mean a double spool, but we can + // come up with a nicer solution to handle that later (can always add a separate + // function for checking if a to be scheduled task can be handled right ther and + // then). + match task { + Task::TriggerRestart(flags) => match &*self.component_handler.read() { + Some(handler) => { + handler.restart_component(flags); + } + None => nih_debug_assert_failure!("Component handler not yet set"), + }, + } + } +} diff --git a/src/wrapper/vst3/util.rs b/src/wrapper/vst3/util.rs index a4156e70..a828223e 100644 --- a/src/wrapper/vst3/util.rs +++ b/src/wrapper/vst3/util.rs @@ -14,6 +14,18 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use lazy_static::lazy_static; + +use crate::wrapper::util::hash_param_id; + +/// Right now the wrapper adds its own bypass parameter. +/// +/// TODO: Actually use this parameter. +pub const BYPASS_PARAM_ID: &str = "bypass"; +lazy_static! { + pub static ref BYPASS_PARAM_HASH: u32 = hash_param_id(BYPASS_PARAM_ID); +} + /// Early exit out of a VST3 function when one of the passed pointers is null macro_rules! check_null_ptr { ($ptr:expr $(, $ptrs:expr)* $(, )?) => { @@ -30,3 +42,28 @@ macro_rules! check_null_ptr_msg { } }; } + +/// Send+Sync wrapper for these interface pointers. +#[repr(transparent)] +pub struct VstPtr { + ptr: vst3_sys::VstPtr, +} + +impl std::ops::Deref for VstPtr { + type Target = vst3_sys::VstPtr; + + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl From> for VstPtr { + fn from(ptr: vst3_sys::VstPtr) -> Self { + Self { ptr } + } +} + +/// SAFETY: Sharing these pointers across thread is s safe as they have internal atomic reference +/// counting, so as long as a `VstPtr` handle exists the object will stay alive. +unsafe impl Send for VstPtr {} +unsafe impl Sync for VstPtr {}