From 8090d0ae419255552f244828dab6492d10ef317d Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 23 Mar 2022 17:36:58 +0100 Subject: [PATCH] Add explicit bypass parameter handling Plugins can mark a `BoolParam` with `.is_bypass()`. Hosts can then link use that parameter directly in their own UI. --- README.md | 3 + src/param.rs | 9 +- src/param/boolean.rs | 9 ++ src/plugin.rs | 2 - src/wrapper/clap/wrapper.rs | 262 +++++++++++++++++++++--------------- src/wrapper/state.rs | 108 ++++++++------- src/wrapper/util.rs | 23 ++++ src/wrapper/vst3/inner.rs | 77 +++++++---- src/wrapper/vst3/util.rs | 11 -- src/wrapper/vst3/wrapper.rs | 176 +++++++++++++----------- 10 files changed, 394 insertions(+), 286 deletions(-) diff --git a/README.md b/README.md index 16c76d8f..f31d8b3c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ for download links. `Params` object and annotating them with `#[persist = "key"]`. - Group your parameters into logical groups by nesting `Params` objects using the `#[nested = "Group Name"]`attribute. + - When needed, you can also provide your own implementation for the `Params` + trait to enable dynamically generated parameters and arrays of if mostly + identical parameter objects. - Stateful. Behaves mostly like JUCE, just without all of the boilerplate. - Does not make any assumptions on how you want to process audio, but does come with utilities and adapters to help with common access patterns. diff --git a/src/param.rs b/src/param.rs index bb46c70b..9bdec7da 100644 --- a/src/param.rs +++ b/src/param.rs @@ -22,13 +22,18 @@ bitflags::bitflags! { #[repr(transparent)] #[derive(Default)] pub struct ParamFlags: u32 { + /// When applied to a [`BoolParam`], this will cause the parameter to be linked to the + /// host's bypass control. Only a single parameter can be marked as a bypass parameter. If + /// you don't have a bypass parameter, then NIH-plug will add one for you. You will need to + /// implement this yourself if your plugin introduces latency. + const BYPASS = 1 << 0; /// The parameter cannot be automated from the host. Setting this flag also prevents it from /// showing up in the host's own generic UI for this plugin. The parameter can still be /// changed from the plugin's editor GUI. - const NON_AUTOMATABLE = 1 << 0; + const NON_AUTOMATABLE = 1 << 1; /// Don't show this parameter when generating a generic UI for the plugin using one of /// NIH-plug's generic UI widgets. - const HIDE_IN_GENERIC_UI = 1 << 1; + const HIDE_IN_GENERIC_UI = 1 << 2; } } diff --git a/src/param/boolean.rs b/src/param/boolean.rs index d3e584a5..61b3b7ef 100644 --- a/src/param/boolean.rs +++ b/src/param/boolean.rs @@ -182,6 +182,15 @@ impl BoolParam { self } + /// Mark this parameter as a bypass parameter. Plugin hosts can integrate this parameter into + /// their UI. Only a single [`BoolParam`] can be a bypass parmaeter, and NIH-plug will add one + /// if you don't create one yourself. You will need to implement this yourself if your plugin + /// introduces latency. + pub fn is_bypass(mut self) -> Self { + self.flags.insert(ParamFlags::BYPASS); + self + } + /// Mark the paramter as non-automatable. This means that the parameter cannot be automated from /// the host. Setting this flag also prevents it from showing up in the host's own generic UI /// for this plugin. The parameter can still be changed from the plugin's editor GUI. diff --git a/src/plugin.rs b/src/plugin.rs index 11f556a8..50cd6a04 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -21,8 +21,6 @@ use crate::param::internals::Params; /// - Sidechain inputs /// - Multiple output busses /// - Special handling for offline processing -/// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to -/// interact with it yet /// - Outputting parameter changes from the plugin /// - MIDI CC handling /// - Outputting MIDI events from the process function (you can output parmaeter changes from an diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 5922fec8..3d9a4521 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -48,7 +48,6 @@ use clap_sys::process::{ use clap_sys::stream::{clap_istream, clap_ostream}; use crossbeam::atomic::AtomicCell; use crossbeam::queue::ArrayQueue; -use lazy_static::lazy_static; use parking_lot::RwLock; use raw_window_handle::RawWindowHandle; use std::any::Any; @@ -75,15 +74,9 @@ use crate::plugin::{ }; use crate::util::permit_alloc; use crate::wrapper::state; -use crate::wrapper::util::{hash_param_id, process_wrapper, strlcpy}; - -/// 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); -} +use crate::wrapper::util::{ + hash_param_id, process_wrapper, strlcpy, Bypass, BYPASS_PARAM_HASH, BYPASS_PARAM_ID, +}; /// How many output parameter changes we can store in our output parameter change queue. Storing /// more than this many parmaeters at a time will cause changes to get lost. @@ -119,9 +112,9 @@ pub struct Wrapper { /// The current buffer configuration, containing the sample rate and the maximum block size. /// Will be set in `clap_plugin::activate()`. current_buffer_config: AtomicCell>, - /// Whether the plugin is currently bypassed. This is not yet integrated with the `Plugin` - /// trait. - bypass_state: AtomicBool, + /// Contains either a boolean indicating whether the plugin is currently bypassed, or a bypass + /// parameter if the plugin has one. + bypass: Bypass, /// 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 @@ -345,10 +338,6 @@ impl Wrapper

{ .iter() .map(|(id, _, _, _)| id.clone()) .collect(); - nih_debug_assert!( - !param_ids.contains(BYPASS_PARAM_ID), - "The wrapper already adds its own bypass parameter" - ); nih_debug_assert_eq!( param_map.len(), param_ids.len(), @@ -356,6 +345,31 @@ impl Wrapper

{ ); } + // The plugin either supplies its own bypass parameter, or we'll create one for it + let mut bypass = Bypass::Dummy(AtomicBool::new(false)); + for (id, _, ptr, _) in ¶m_id_hashes_ptrs_groups { + let flags = unsafe { ptr.flags() }; + let is_bypass = flags.contains(ParamFlags::BYPASS); + if cfg!(debug_assertions) { + if id == BYPASS_PARAM_ID && !is_bypass { + nih_debug_assert_failure!("Bypass parameters need to be marked with `.is_bypass()`, weird things will happen"); + } + if is_bypass && matches!(bypass, Bypass::Parameter(_)) { + nih_debug_assert_failure!( + "Duplicate bypass parameters found, using the first one" + ); + continue; + } + } + + if is_bypass { + bypass = Bypass::Parameter(*ptr); + if !cfg!(debug_assertions) { + break; + } + } + } + let param_hashes = param_id_hashes_ptrs_groups .iter() .map(|(_, hash, _, _)| *hash) @@ -439,7 +453,7 @@ impl Wrapper

{ num_output_channels: P::DEFAULT_NUM_OUTPUTS, }), current_buffer_config: AtomicCell::new(None), - bypass_state: AtomicBool::new(false), + bypass, input_events: AtomicRefCell::new(VecDeque::with_capacity(512)), last_process_status: AtomicCell::new(ProcessStatus::Normal), current_latency: AtomicU32::new(0), @@ -589,33 +603,36 @@ impl Wrapper

{ update: ClapParamUpdate, sample_rate: Option, ) -> bool { - if hash == *BYPASS_PARAM_HASH { - match update { - ClapParamUpdate::PlainValueSet(clap_plain_value) => self - .bypass_state - .store(clap_plain_value >= 0.5, Ordering::SeqCst), - } - - true - } else if let Some(param_ptr) = self.param_by_hash.get(&hash) { - let normalized_value = match update { - ClapParamUpdate::PlainValueSet(clap_plain_value) => { - clap_plain_value as f32 / unsafe { param_ptr.step_count() }.unwrap_or(1) as f32 + match (&self.bypass, self.param_by_hash.get(&hash)) { + (Bypass::Dummy(bypass_state), _) if hash == *BYPASS_PARAM_HASH => { + match update { + ClapParamUpdate::PlainValueSet(clap_plain_value) => { + bypass_state.store(clap_plain_value >= 0.5, Ordering::SeqCst) + } } - }; - // Also update the parameter's smoothing if applicable - match (param_ptr, sample_rate) { - (_, Some(sample_rate)) => unsafe { - param_ptr.set_normalized_value(normalized_value); - param_ptr.update_smoother(sample_rate, false); - }, - _ => unsafe { param_ptr.set_normalized_value(normalized_value) }, + true } + (_, Some(param_ptr)) => { + let normalized_value = match update { + ClapParamUpdate::PlainValueSet(clap_plain_value) => { + clap_plain_value as f32 + / unsafe { param_ptr.step_count() }.unwrap_or(1) as f32 + } + }; - true - } else { - false + // Also update the parameter's smoothing if applicable + match (param_ptr, sample_rate) { + (_, Some(sample_rate)) => unsafe { + param_ptr.set_normalized_value(normalized_value); + param_ptr.update_smoother(sample_rate, false); + }, + _ => unsafe { param_ptr.set_normalized_value(normalized_value) }, + } + + true + } + _ => false, } } @@ -1170,16 +1187,20 @@ impl Wrapper

{ } } - // Only process audio if the plugin is not currently bypassed - let result = if !wrapper.bypass_state.load(Ordering::Relaxed) { - let mut plugin = wrapper.plugin.write(); - let mut context = wrapper.make_process_context(transport); - let result = plugin.process(&mut output_buffer, &mut context); - wrapper.last_process_status.store(result); - result - } else { - wrapper.last_process_status.store(ProcessStatus::Normal); - ProcessStatus::Normal + // Only process audio if the plugin isn't bypassed. If the plugin provides its + // own bypass parameter then it should decide what to do by itself. + let result = match &wrapper.bypass { + Bypass::Dummy(bypass_state) if bypass_state.load(Ordering::Relaxed) => { + wrapper.last_process_status.store(ProcessStatus::Normal); + ProcessStatus::Normal + } + _ => { + let mut plugin = wrapper.plugin.write(); + let mut context = wrapper.make_process_context(transport); + let result = plugin.process(&mut output_buffer, &mut context); + wrapper.last_process_status.store(result); + result + } }; let clap_result = match result { @@ -1710,9 +1731,13 @@ impl Wrapper

{ check_null_ptr!(0, plugin); let wrapper = &*(plugin as *const Self); - // NOTE: We add a bypass parameter ourselves on index `plugin.param_hashes.len()`, so - // these indices are all off by one - wrapper.param_hashes.len() as u32 + 1 + // NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()` if the + // plugin does not provide its own bypass parmaeter, in which case these indices will + // all be off by one + match wrapper.bypass { + Bypass::Parameter(_) => wrapper.param_hashes.len() as u32, + Bypass::Dummy(_) => wrapper.param_hashes.len() as u32 + 1, + } } unsafe extern "C" fn ext_params_get_info( @@ -1723,8 +1748,7 @@ impl Wrapper

{ check_null_ptr!(false, plugin, param_info); let wrapper = &*(plugin as *const Self); - // Parameter index `self.param_ids.len()` is our own bypass parameter - if param_index < 0 || param_index > wrapper.param_hashes.len() as i32 { + if param_index < 0 || param_index > Self::ext_params_count(plugin) as i32 { return false; } @@ -1733,7 +1757,9 @@ impl Wrapper

{ // TODO: We don't use the cookies at this point. In theory this would be faster than the ID // hashmap lookup, but for now we'll stay consistent with the VST3 implementation. let param_info = &mut *param_info; - if param_index == wrapper.param_hashes.len() as i32 { + if matches!(wrapper.bypass, Bypass::Dummy(_)) + && param_index == wrapper.param_hashes.len() as i32 + { param_info.id = *BYPASS_PARAM_HASH; param_info.flags = CLAP_PARAM_IS_STEPPED | CLAP_PARAM_IS_BYPASS | CLAP_PARAM_IS_AUTOMATABLE; @@ -1749,7 +1775,9 @@ impl Wrapper

{ let param_ptr = &wrapper.param_by_hash[param_hash]; let default_value = param_ptr.default_normalized_value(); let step_count = param_ptr.step_count(); - let automatable = !param_ptr.flags().contains(ParamFlags::NON_AUTOMATABLE); + let flags = param_ptr.flags(); + let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE); + let is_bypass = flags.contains(ParamFlags::BYPASS); param_info.id = *param_hash; param_info.flags = if automatable { @@ -1757,6 +1785,9 @@ impl Wrapper

{ } else { CLAP_PARAM_IS_HIDDEN | CLAP_PARAM_IS_READONLY }; + if is_bypass { + param_info.flags |= CLAP_PARAM_IS_BYPASS + } if step_count.is_some() { param_info.flags |= CLAP_PARAM_IS_STEPPED } @@ -1785,19 +1816,23 @@ impl Wrapper

{ check_null_ptr!(false, plugin, value); let wrapper = &*(plugin as *const Self); - if param_id == *BYPASS_PARAM_HASH { - *value = if wrapper.bypass_state.load(Ordering::SeqCst) { - 1.0 - } else { - 0.0 - }; - true - } else if let Some(param_ptr) = wrapper.param_by_hash.get(¶m_id) { - *value = - param_ptr.normalized_value() as f64 * param_ptr.step_count().unwrap_or(1) as f64; - true - } else { - false + match (&wrapper.bypass, wrapper.param_by_hash.get(¶m_id)) { + (Bypass::Dummy(bypass_state), _) if param_id == *BYPASS_PARAM_HASH => { + *value = if bypass_state.load(Ordering::Relaxed) { + 1.0 + } else { + 0.0 + }; + + true + } + (_, Some(param_ptr)) => { + *value = param_ptr.normalized_value() as f64 + * param_ptr.step_count().unwrap_or(1) as f64; + + true + } + _ => false, } } @@ -1813,27 +1848,29 @@ impl Wrapper

{ let dest = std::slice::from_raw_parts_mut(display, size as usize); - if param_id == *BYPASS_PARAM_HASH { - if value > 0.5 { - strlcpy(dest, "Bypassed") - } else { - strlcpy(dest, "Not Bypassed") + match (&wrapper.bypass, wrapper.param_by_hash.get(¶m_id)) { + (Bypass::Dummy(_), _) if param_id == *BYPASS_PARAM_HASH => { + if value > 0.5 { + strlcpy(dest, "Bypassed") + } else { + strlcpy(dest, "Not Bypassed") + } + + true } + (_, Some(param_ptr)) => { + strlcpy( + dest, + // CLAP does not have a separate unit, so we'll include the unit here + ¶m_ptr.normalized_value_to_string( + value as f32 / param_ptr.step_count().unwrap_or(1) as f32, + true, + ), + ); - true - } else if let Some(param_ptr) = wrapper.param_by_hash.get(¶m_id) { - strlcpy( - dest, - // CLAP does not have a separate unit, so we'll include the unit here - ¶m_ptr.normalized_value_to_string( - value as f32 / param_ptr.step_count().unwrap_or(1) as f32, - true, - ), - ); - - true - } else { - false + true + } + _ => false, } } @@ -1851,25 +1888,30 @@ impl Wrapper

{ Err(_) => return false, }; - if param_id == *BYPASS_PARAM_HASH { - let normalized_valeu = match display { - "Bypassed" => 1.0, - "Not Bypassed" => 0.0, - _ => return false, - }; - *value = normalized_valeu; + match (&wrapper.bypass, wrapper.param_by_hash.get(¶m_id)) { + (Bypass::Dummy(_), _) if param_id == *BYPASS_PARAM_HASH => { + let display = display.trim(); + let normalized_valeu = if display.eq_ignore_ascii_case("bypassed") { + 1.0 + } else if display.eq_ignore_ascii_case("not bypassed") { + 0.0 + } else { + return false; + }; + *value = normalized_valeu; - true - } else if let Some(param_ptr) = wrapper.param_by_hash.get(¶m_id) { - let normalized_value = match param_ptr.string_to_normalized_value(display) { - Some(v) => v as f64, - None => return false, - }; - *value = normalized_value * param_ptr.step_count().unwrap_or(1) as f64; + true + } + (_, Some(param_ptr)) => { + let normalized_value = match param_ptr.string_to_normalized_value(display) { + Some(v) => v as f64, + None => return false, + }; + *value = normalized_value * param_ptr.step_count().unwrap_or(1) as f64; - true - } else { - false + true + } + _ => false, } } @@ -1901,8 +1943,7 @@ impl Wrapper

{ wrapper.plugin.read().params(), &wrapper.param_by_hash, &wrapper.param_id_to_hash, - BYPASS_PARAM_ID, - &wrapper.bypass_state, + &wrapper.bypass, ); match serialized { Ok(serialized) => { @@ -1964,8 +2005,7 @@ impl Wrapper

{ &wrapper.param_by_hash, &wrapper.param_id_to_hash, wrapper.current_buffer_config.load().as_ref(), - BYPASS_PARAM_ID, - &wrapper.bypass_state, + &wrapper.bypass, ); if !success { return false; diff --git a/src/wrapper/state.rs b/src/wrapper/state.rs index ec3e78b5..81236d1b 100644 --- a/src/wrapper/state.rs +++ b/src/wrapper/state.rs @@ -3,8 +3,9 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::pin::Pin; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; +use super::util::{Bypass, BYPASS_PARAM_ID}; use crate::param::internals::{ParamPtr, Params}; use crate::param::Param; use crate::plugin::BufferConfig; @@ -40,8 +41,7 @@ pub(crate) unsafe fn serialize( plugin_params: Pin<&dyn Params>, param_by_hash: &HashMap, param_id_to_hash: &HashMap, - bypass_param_id: &str, - bypass_state: &AtomicBool, + bypass: &Bypass, ) -> serde_json::Result> { // We'll serialize parmaeter values as a simple `string_param_id: display_value` map. let mut params: HashMap<_, _> = param_id_to_hash @@ -72,11 +72,13 @@ pub(crate) unsafe fn serialize( }) .collect(); - // Don't forget about the bypass parameter - params.insert( - bypass_param_id.to_string(), - ParamValue::Bool(bypass_state.load(Ordering::SeqCst)), - ); + // Don't forget about the bypass parameter if we added one for the plugin + if let Bypass::Dummy(bypass_state) = bypass { + params.insert( + String::from(BYPASS_PARAM_ID), + ParamValue::Bool(bypass_state.load(Ordering::SeqCst)), + ); + } // The plugin can also persist arbitrary fields alongside its parameters. This is useful for // storing things like sample data. @@ -97,8 +99,7 @@ pub(crate) unsafe fn deserialize( param_by_hash: &HashMap, param_id_to_hash: &HashMap, current_buffer_config: Option<&BufferConfig>, - bypass_param_id: &str, - bypass_state: &AtomicBool, + bypass: &Bypass, ) -> bool { let state: State = match serde_json::from_slice(state) { Ok(s) => s, @@ -110,52 +111,55 @@ pub(crate) unsafe fn deserialize( let sample_rate = current_buffer_config.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 { - match param_value { - ParamValue::Bool(b) => bypass_state.store(b, Ordering::SeqCst), - _ => nih_debug_assert_failure!( - "Invalid serialized value {:?} for parameter \"{}\"", - param_value, - param_id_str, - ), - }; - continue; - } - - let param_ptr = match param_id_to_hash - .get(param_id_str.as_str()) - .and_then(|hash| param_by_hash.get(hash)) - { - Some(ptr) => ptr, - None => { - nih_debug_assert_failure!("Unknown parameter: {}", param_id_str); + // Handle the automatically generated bypass parameter separately + match bypass { + Bypass::Dummy(bypass_state) if param_id_str == BYPASS_PARAM_ID => { + match param_value { + ParamValue::Bool(b) => bypass_state.store(b, Ordering::SeqCst), + _ => nih_debug_assert_failure!( + "Invalid serialized value {:?} for parameter \"{}\"", + param_value, + param_id_str, + ), + }; continue; } - }; + _ => { + let param_ptr = match param_id_to_hash + .get(param_id_str.as_str()) + .and_then(|hash| param_by_hash.get(hash)) + { + Some(ptr) => ptr, + None => { + nih_debug_assert_failure!("Unknown parameter: {}", param_id_str); + continue; + } + }; - match (param_ptr, param_value) { - (ParamPtr::FloatParam(p), ParamValue::F32(v)) => (**p).set_plain_value(v), - (ParamPtr::IntParam(p), ParamValue::I32(v)) => (**p).set_plain_value(v), - (ParamPtr::BoolParam(p), ParamValue::Bool(v)) => (**p).set_plain_value(v), - // Enums are serialized based on the active variant's index (which may not be the - // same as the discriminator) - (ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => { - (**p).set_plain_value(variant_idx) - } - (param_ptr, param_value) => { - nih_debug_assert_failure!( - "Invalid serialized value {:?} for parameter \"{}\" ({:?})", - param_value, - param_id_str, - param_ptr, - ); - } - } + match (param_ptr, param_value) { + (ParamPtr::FloatParam(p), ParamValue::F32(v)) => (**p).set_plain_value(v), + (ParamPtr::IntParam(p), ParamValue::I32(v)) => (**p).set_plain_value(v), + (ParamPtr::BoolParam(p), ParamValue::Bool(v)) => (**p).set_plain_value(v), + // Enums are serialized based on the active variant's index (which may not be the + // same as the discriminator) + (ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => { + (**p).set_plain_value(variant_idx) + } + (param_ptr, param_value) => { + nih_debug_assert_failure!( + "Invalid serialized value {:?} for parameter \"{}\" ({:?})", + param_value, + param_id_str, + param_ptr, + ); + } + } - // Make sure everything starts out in sync - if let Some(sample_rate) = sample_rate { - param_ptr.update_smoother(sample_rate, true); + // Make sure everything starts out in sync + if let Some(sample_rate) = sample_rate { + param_ptr.update_smoother(sample_rate, true); + } + } } } diff --git a/src/wrapper/util.rs b/src/wrapper/util.rs index c73d766d..1787a3eb 100644 --- a/src/wrapper/util.rs +++ b/src/wrapper/util.rs @@ -1,13 +1,36 @@ +use lazy_static::lazy_static; use std::cmp; use std::marker::PhantomData; use std::os::raw::c_char; +use std::sync::atomic::AtomicBool; use vst3_sys::vst::TChar; use widestring::U16CString; +use crate::param::internals::ParamPtr; + #[cfg(all(debug_assertions, feature = "assert_process_allocs"))] #[global_allocator] static A: assert_no_alloc::AllocDisabler = assert_no_alloc::AllocDisabler; +/// The ID of the automatically generated bypass parameter. Added if the plugin does not define its +/// own bypass parameter. +pub const BYPASS_PARAM_ID: &str = "bypass"; +lazy_static! { + pub static ref BYPASS_PARAM_HASH: u32 = hash_param_id(BYPASS_PARAM_ID); +} + +/// The plugin's bypass parameter. If [`Plugin::params()`] contains a [`ParamFlags::BYPASS`] +/// parameter then that will be used as the plugin's bypass parameter. Otherwise NIH-plug will add a +/// dummy boolean parameter. +#[derive(Debug)] +pub enum Bypass { + /// A parameter from `P` that's marked as a bypass parameter. + Parameter(ParamPtr), + /// A boolean that keeps track of the plugin's bypass state. Added automatically if the plugin + /// doesn't have a bypass parameter. + Dummy(AtomicBool), +} + /// A Rabin fingerprint based string hash for parameter ID strings. pub fn hash_param_id(id: &str) -> u32 { let mut overflow; diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 6cadf03f..b1235538 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -11,14 +11,15 @@ use vst3_sys::vst::IComponentHandler; use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::param_units::ParamUnits; -use super::util::{ObjectPtr, VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID}; +use super::util::{ObjectPtr, VstPtr}; use super::view::WrapperView; use crate::buffer::Buffer; use crate::context::Transport; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; use crate::param::internals::ParamPtr; +use crate::param::ParamFlags; use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, ProcessStatus, Vst3Plugin}; -use crate::wrapper::util::hash_param_id; +use crate::wrapper::util::{hash_param_id, Bypass, BYPASS_PARAM_HASH, 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 @@ -56,9 +57,9 @@ pub(crate) struct WrapperInner { /// 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, + /// Contains either a boolean indicating whether the plugin is currently bypassed, or a bypass + /// parameter if the plugin has one. + pub bypass: Bypass, /// 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`]. @@ -153,10 +154,6 @@ impl WrapperInner

{ .iter() .map(|(id, _, _, _)| id.clone()) .collect(); - nih_debug_assert!( - !param_ids.contains(BYPASS_PARAM_ID), - "The wrapper already adds its own bypass parameter" - ); nih_debug_assert_eq!( param_map.len(), param_ids.len(), @@ -164,13 +161,38 @@ impl WrapperInner

{ ); } + // The plugin either supplies its own bypass parameter, or we'll create one for it + let mut bypass = Bypass::Dummy(AtomicBool::new(false)); + for (id, _, ptr, _) in ¶m_id_hashes_ptrs_groups { + let flags = unsafe { ptr.flags() }; + let is_bypass = flags.contains(ParamFlags::BYPASS); + if cfg!(debug_assertions) { + if id == BYPASS_PARAM_ID && !is_bypass { + nih_debug_assert_failure!("Bypass parameters need to be marked with `.is_bypass()`, weird things will happen"); + } + if is_bypass && matches!(bypass, Bypass::Parameter(_)) { + nih_debug_assert_failure!( + "Duplicate bypass parameters found, using the first one" + ); + continue; + } + } + + if is_bypass { + bypass = Bypass::Parameter(*ptr); + if !cfg!(debug_assertions) { + break; + } + } + } + let param_hashes = param_id_hashes_ptrs_groups .iter() - .map(|&(_, hash, _, _)| hash) + .map(|(_, hash, _, _)| *hash) .collect(); let param_by_hash = param_id_hashes_ptrs_groups .iter() - .map(|&(_, hash, ptr, _)| (hash, ptr)) + .map(|(_, hash, ptr, _)| (*hash, *ptr)) .collect(); let param_units = ParamUnits::from_param_groups( param_id_hashes_ptrs_groups @@ -207,7 +229,7 @@ impl WrapperInner

{ num_output_channels: P::DEFAULT_NUM_OUTPUTS, }), current_buffer_config: AtomicCell::new(None), - bypass_state: AtomicBool::new(false), + bypass, last_process_status: AtomicCell::new(ProcessStatus::Normal), current_latency: AtomicU32::new(0), output_buffer: AtomicRefCell::new(Buffer::default()), @@ -271,24 +293,25 @@ impl WrapperInner

{ normalized_value: f32, sample_rate: Option, ) -> tresult { - if hash == *BYPASS_PARAM_HASH { - self.bypass_state - .store(normalized_value >= 0.5, Ordering::SeqCst); + match (&self.bypass, self.param_by_hash.get(&hash)) { + (Bypass::Dummy(bypass_state), _) if hash == *BYPASS_PARAM_HASH => { + bypass_state.store(normalized_value >= 0.5, Ordering::Relaxed); - 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)) => unsafe { - param_ptr.set_normalized_value(normalized_value); - param_ptr.update_smoother(sample_rate, false); - }, - _ => unsafe { param_ptr.set_normalized_value(normalized_value) }, + kResultOk } + (_, Some(param_ptr)) => { + // Also update the parameter's smoothing if applicable + match (param_ptr, sample_rate) { + (_, Some(sample_rate)) => unsafe { + param_ptr.set_normalized_value(normalized_value); + param_ptr.update_smoother(sample_rate, false); + }, + _ => unsafe { param_ptr.set_normalized_value(normalized_value) }, + } - kResultOk - } else { - kInvalidArgument + kResultOk + } + _ => kInvalidArgument, } } } diff --git a/src/wrapper/vst3/util.rs b/src/wrapper/vst3/util.rs index 3acd90f7..870a4c95 100644 --- a/src/wrapper/vst3/util.rs +++ b/src/wrapper/vst3/util.rs @@ -1,17 +1,6 @@ -use lazy_static::lazy_static; use std::ops::Deref; use vst3_sys::{interfaces::IUnknown, ComInterface}; -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)* $(, )?) => { diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 2ac76984..7e9af8f4 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -15,13 +15,13 @@ use vst3_sys::VST3; use widestring::U16CStr; use super::inner::WrapperInner; -use super::util::{VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID}; +use super::util::VstPtr; use super::view::WrapperView; use crate::context::Transport; use crate::param::ParamFlags; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; use crate::wrapper::state; -use crate::wrapper::util::{process_wrapper, u16strlcpy}; +use crate::wrapper::util::{process_wrapper, u16strlcpy, Bypass, BYPASS_PARAM_HASH}; use crate::wrapper::vst3::inner::ParameterChange; // Alias needed for the VST3 attribute macro @@ -226,8 +226,7 @@ impl IComponent for Wrapper

{ &self.inner.param_by_hash, &self.inner.param_id_to_hash, self.inner.current_buffer_config.load().as_ref(), - BYPASS_PARAM_ID, - &self.inner.bypass_state, + &self.inner.bypass, ); if !success { return kResultFalse; @@ -261,8 +260,7 @@ impl IComponent for Wrapper

{ self.inner.plugin.read().params(), &self.inner.param_by_hash, &self.inner.param_id_to_hash, - BYPASS_PARAM_ID, - &self.inner.bypass_state, + &self.inner.bypass, ); match serialized { Ok(serialized) => { @@ -304,9 +302,13 @@ impl IEditController for Wrapper

{ } unsafe fn get_parameter_count(&self) -> i32 { - // NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()`, so - // these indices are all off by one - self.inner.param_hashes.len() as i32 + 1 + // NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()` if the + // plugin does not provide its own bypass parmaeter, in which case these indices will + // all be off by one + match self.inner.bypass { + Bypass::Parameter(_) => self.inner.param_hashes.len() as i32, + Bypass::Dummy(_) => self.inner.param_hashes.len() as i32 + 1, + } } unsafe fn get_parameter_info( @@ -316,15 +318,16 @@ impl IEditController for Wrapper

{ ) -> tresult { check_null_ptr!(info); - // Parameter index `self.param_ids.len()` is our own bypass parameter - if param_index < 0 || param_index > self.inner.param_hashes.len() as i32 { + if param_index < 0 || param_index > self.get_parameter_count() { return kInvalidArgument; } *info = std::mem::zeroed(); let info = &mut *info; - if param_index == self.inner.param_hashes.len() as i32 { + if matches!(self.inner.bypass, Bypass::Dummy(_)) + && param_index == self.inner.param_hashes.len() as i32 + { info.id = *BYPASS_PARAM_HASH; u16strlcpy(&mut info.title, "Bypass"); u16strlcpy(&mut info.short_title, "Bypass"); @@ -343,7 +346,9 @@ impl IEditController for Wrapper

{ .expect("Inconsistent parameter data"); let param_ptr = &self.inner.param_by_hash[param_hash]; let default_value = param_ptr.default_normalized_value(); - let automatable = !param_ptr.flags().contains(ParamFlags::NON_AUTOMATABLE); + let flags = param_ptr.flags(); + let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE); + let is_bypass = flags.contains(ParamFlags::BYPASS); info.id = *param_hash; u16strlcpy(&mut info.title, param_ptr.name()); @@ -357,6 +362,9 @@ impl IEditController for Wrapper

{ } else { vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden }; + if is_bypass { + info.flags |= vst3_sys::vst::ParameterFlags::kIsBypass as i32; + } } kResultOk @@ -370,26 +378,27 @@ impl IEditController for Wrapper

{ ) -> tresult { check_null_ptr!(string); - // Somehow there's no length there, so we'll assume our own maximum let dest = &mut *(string as *mut [TChar; 128]); - if id == *BYPASS_PARAM_HASH { - if value_normalized > 0.5 { - u16strlcpy(dest, "Bypassed") - } else { - u16strlcpy(dest, "Not Bypassed") + match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) { + (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => { + if value_normalized > 0.5 { + u16strlcpy(dest, "Bypassed") + } else { + u16strlcpy(dest, "Not Bypassed") + } + + kResultOk } + (_, Some(param_ptr)) => { + u16strlcpy( + dest, + ¶m_ptr.normalized_value_to_string(value_normalized as f32, false), + ); - kResultOk - } else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { - u16strlcpy( - dest, - ¶m_ptr.normalized_value_to_string(value_normalized as f32, false), - ); - - kResultOk - } else { - kInvalidArgument + kResultOk + } + _ => kInvalidArgument, } } @@ -406,59 +415,60 @@ impl IEditController for Wrapper

{ Err(_) => return kInvalidArgument, }; - if id == *BYPASS_PARAM_HASH { - let value = match string.as_str() { - "Bypassed" => 1.0, - "Not Bypassed" => 0.0, - _ => return kResultFalse, - }; - *value_normalized = value; + match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) { + (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => { + let string = string.trim(); + let value = if string.eq_ignore_ascii_case("bypassed") { + 1.0 + } else if string.eq_ignore_ascii_case("not bypassed") { + 0.0 + } else { + return kInvalidArgument; + }; + *value_normalized = value; - kResultOk - } else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { - let value = match param_ptr.string_to_normalized_value(&string) { - Some(v) => v as f64, - None => return kResultFalse, - }; - *value_normalized = value; + kResultOk + } + (_, Some(param_ptr)) => { + let value = match param_ptr.string_to_normalized_value(&string) { + Some(v) => v as f64, + None => return kResultFalse, + }; + *value_normalized = value; - kResultOk - } else { - kInvalidArgument + kResultOk + } + _ => kInvalidArgument, } } unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 { - if id == *BYPASS_PARAM_HASH { - value_normalized - } else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { - param_ptr.preview_plain(value_normalized as f32) as f64 - } else { - 0.5 + match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) { + (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => value_normalized.clamp(0.0, 1.0), + (_, Some(param_ptr)) => param_ptr.preview_plain(value_normalized as f32) as f64, + _ => 0.5, } } unsafe fn plain_param_to_normalized(&self, id: u32, plain_value: f64) -> f64 { - if id == *BYPASS_PARAM_HASH { - plain_value.clamp(0.0, 1.0) - } else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { - param_ptr.preview_normalized(plain_value as f32) as f64 - } else { - 0.5 + match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) { + (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => plain_value.clamp(0.0, 1.0), + (_, Some(param_ptr)) => param_ptr.preview_normalized(plain_value as f32) as f64, + _ => 0.5, } } unsafe fn get_param_normalized(&self, id: u32) -> f64 { - if id == *BYPASS_PARAM_HASH { - if self.inner.bypass_state.load(Ordering::SeqCst) { - 1.0 - } else { - 0.0 + match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) { + (Bypass::Dummy(bypass_state), _) if id == *BYPASS_PARAM_HASH => { + if bypass_state.load(Ordering::SeqCst) { + 1.0 + } else { + 0.0 + } } - } else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { - param_ptr.normalized_value() as f64 - } else { - 0.5 + (_, Some(param_ptr)) => param_ptr.normalized_value() as f64, + _ => 0.5, } } @@ -921,21 +931,25 @@ impl IAudioProcessor for Wrapper

{ let mut plugin = self.inner.plugin.write(); let mut context = self.inner.make_process_context(transport); - // Only process audio if the plugin isn't bypassed - if !self.inner.bypass_state.load(Ordering::Relaxed) { - let result = plugin.process(&mut output_buffer, &mut context); - self.inner.last_process_status.store(result); - match result { - ProcessStatus::Error(err) => { - nih_debug_assert_failure!("Process error: {}", err); - - return kResultFalse; - } - _ => kResultOk, + // Only process audio if the plugin isn't bypassed. If the plugin provides its + // own bypass parameter then it should decide what to do by itself. + match &self.inner.bypass { + Bypass::Dummy(bypass_state) if bypass_state.load(Ordering::Relaxed) => { + self.inner.last_process_status.store(ProcessStatus::Normal); + kResultOk + } + _ => { + let result = plugin.process(&mut output_buffer, &mut context); + self.inner.last_process_status.store(result); + match result { + ProcessStatus::Error(err) => { + nih_debug_assert_failure!("Process error: {}", err); + + return kResultFalse; + } + _ => kResultOk, + } } - } else { - self.inner.last_process_status.store(ProcessStatus::Normal); - kResultOk } };