Move inner wrapper to its own module
We'll need to do this with the other parts as well. Navigating this module has become practically impossible
This commit is contained in:
parent
427c7674b9
commit
9db4c0d691
|
@ -14,12 +14,6 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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<T>` 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<P: Plugin> {
|
||||
/// The wrapped plugin instance.
|
||||
plugin: RwLock<P>,
|
||||
/// 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<Arc<dyn Editor>>,
|
||||
|
||||
/// The host's `IComponentHandler` instance, if passed through
|
||||
/// `IEditController::set_component_handler`.
|
||||
component_handler: RwLock<Option<VstPtr<dyn IComponentHandler>>>,
|
||||
|
||||
/// 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<MaybeUninit<OsEventLoop<Task, Self>>>,
|
||||
|
||||
/// 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<BusConfig>,
|
||||
/// The current buffer configuration, containing the sample rate and the maximum block size.
|
||||
/// Will be set in `IAudioProcessor::setupProcessing()`.
|
||||
current_buffer_config: AtomicCell<Option<BufferConfig>>,
|
||||
/// 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<ProcessStatus>,
|
||||
/// 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<Buffer<'static>>,
|
||||
/// 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<VecDeque<NoteEvent>>,
|
||||
|
||||
/// The keys from `param_map` in a stable order.
|
||||
param_hashes: Vec<u32>,
|
||||
/// 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<u32, ParamPtr>,
|
||||
/// 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<f32>,
|
||||
/// 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<ParamPtr, u32>,
|
||||
}
|
||||
|
||||
#[VST3(implements(IComponent, IEditController, IAudioProcessor))]
|
||||
pub(crate) struct Wrapper<P: Plugin> {
|
||||
struct Wrapper<P: Plugin> {
|
||||
inner: Arc<WrapperInner<P>>,
|
||||
}
|
||||
|
||||
|
@ -171,7 +91,7 @@ struct WrapperView<P: Plugin> {
|
|||
/// 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<P>,
|
||||
input_events_guard: RwLockWriteGuard<'a, VecDeque<NoteEvent>>,
|
||||
}
|
||||
|
@ -182,7 +102,7 @@ impl<P: Plugin> 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<P: Plugin> 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<Self as GuiContext>` to the plugin and hope it doesn't do anything
|
||||
// weird with it.
|
||||
impl<P: Plugin> GuiContext for WrapperInner<P> {
|
||||
// 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<T: vst3_sys::ComInterface + ?Sized> {
|
||||
ptr: vst3_sys::VstPtr<T>,
|
||||
}
|
||||
|
||||
impl<T: vst3_sys::ComInterface + ?Sized> std::ops::Deref for VstPtr<T> {
|
||||
type Target = vst3_sys::VstPtr<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: vst3_sys::ComInterface + ?Sized> From<vst3_sys::VstPtr<T>> for VstPtr<T> {
|
||||
fn from(ptr: vst3_sys::VstPtr<T>) -> 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<T>` handle exists the object will stay alive.
|
||||
unsafe impl<T: vst3_sys::ComInterface + ?Sized> Send for VstPtr<T> {}
|
||||
unsafe impl<T: vst3_sys::ComInterface + ?Sized> Sync for VstPtr<T> {}
|
||||
|
||||
impl<P: Plugin> WrapperInner<P> {
|
||||
// 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<Self> {
|
||||
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<WrapperInner<P>> = 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<f32>,
|
||||
) -> 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<P: Plugin> Wrapper<P> {
|
||||
pub fn new() -> Box<Self> {
|
||||
Self::allocate(WrapperInner::new())
|
||||
|
@ -426,25 +126,6 @@ impl<P: Plugin> WrapperView<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: Plugin> MainThreadExecutor<Task> for WrapperInner<P> {
|
||||
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<P: Plugin> IPluginBase for Wrapper<P> {
|
||||
unsafe fn initialize(&self, _context: *mut c_void) -> tresult {
|
||||
// We currently don't need or allow any initialization logic
|
||||
|
@ -642,7 +323,7 @@ impl<P: Plugin> IComponent for Wrapper<P> {
|
|||
.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<P: Plugin> IComponent for Wrapper<P> {
|
|||
|
||||
// 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)),
|
||||
);
|
||||
|
||||
|
|
314
src/wrapper/vst3/inner.rs
Normal file
314
src/wrapper/vst3/inner.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<T>` 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<P: Plugin> {
|
||||
/// The wrapped plugin instance.
|
||||
pub plugin: RwLock<P>,
|
||||
/// 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<Arc<dyn Editor>>,
|
||||
|
||||
/// The host's `IComponentHandler` instance, if passed through
|
||||
/// `IEditController::set_component_handler`.
|
||||
pub component_handler: RwLock<Option<VstPtr<dyn IComponentHandler>>>,
|
||||
|
||||
/// 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<MaybeUninit<OsEventLoop<Task, Self>>>,
|
||||
|
||||
/// 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<BusConfig>,
|
||||
/// The current buffer configuration, containing the sample rate and the maximum block size.
|
||||
/// Will be set in `IAudioProcessor::setupProcessing()`.
|
||||
pub current_buffer_config: AtomicCell<Option<BufferConfig>>,
|
||||
/// 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<ProcessStatus>,
|
||||
/// 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<Buffer<'static>>,
|
||||
/// 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<VecDeque<NoteEvent>>,
|
||||
|
||||
/// The keys from `param_map` in a stable order.
|
||||
pub param_hashes: Vec<u32>,
|
||||
/// 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<u32, ParamPtr>,
|
||||
/// 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<f32>,
|
||||
/// 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<ParamPtr, u32>,
|
||||
}
|
||||
|
||||
/// 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<P: Plugin> WrapperInner<P> {
|
||||
// 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<Self> {
|
||||
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<WrapperInner<P>> = 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<f32>,
|
||||
) -> 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<Self as GuiContext>` to the plugin and hope it doesn't do anything
|
||||
// weird with it.
|
||||
impl<P: Plugin> GuiContext for WrapperInner<P> {
|
||||
// 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<P: Plugin> MainThreadExecutor<Task> for WrapperInner<P> {
|
||||
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"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,18 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<T: vst3_sys::ComInterface + ?Sized> {
|
||||
ptr: vst3_sys::VstPtr<T>,
|
||||
}
|
||||
|
||||
impl<T: vst3_sys::ComInterface + ?Sized> std::ops::Deref for VstPtr<T> {
|
||||
type Target = vst3_sys::VstPtr<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: vst3_sys::ComInterface + ?Sized> From<vst3_sys::VstPtr<T>> for VstPtr<T> {
|
||||
fn from(ptr: vst3_sys::VstPtr<T>) -> 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<T>` handle exists the object will stay alive.
|
||||
unsafe impl<T: vst3_sys::ComInterface + ?Sized> Send for VstPtr<T> {}
|
||||
unsafe impl<T: vst3_sys::ComInterface + ?Sized> Sync for VstPtr<T> {}
|
||||
|
|
Loading…
Reference in a new issue