1
0
Fork 0

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.
This commit is contained in:
Robbert van der Helm 2022-03-23 17:36:58 +01:00
parent 3d7a23c812
commit 8090d0ae41
10 changed files with 394 additions and 286 deletions

View file

@ -66,6 +66,9 @@ for download links.
`Params` object and annotating them with `#[persist = "key"]`. `Params` object and annotating them with `#[persist = "key"]`.
- Group your parameters into logical groups by nesting `Params` objects using - Group your parameters into logical groups by nesting `Params` objects using
the `#[nested = "Group Name"]`attribute. 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. - 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 - 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. with utilities and adapters to help with common access patterns.

View file

@ -22,13 +22,18 @@ bitflags::bitflags! {
#[repr(transparent)] #[repr(transparent)]
#[derive(Default)] #[derive(Default)]
pub struct ParamFlags: u32 { 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 /// 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 /// showing up in the host's own generic UI for this plugin. The parameter can still be
/// changed from the plugin's editor GUI. /// 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 /// Don't show this parameter when generating a generic UI for the plugin using one of
/// NIH-plug's generic UI widgets. /// NIH-plug's generic UI widgets.
const HIDE_IN_GENERIC_UI = 1 << 1; const HIDE_IN_GENERIC_UI = 1 << 2;
} }
} }

View file

@ -182,6 +182,15 @@ impl BoolParam {
self 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 /// 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 /// 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. /// for this plugin. The parameter can still be changed from the plugin's editor GUI.

View file

@ -21,8 +21,6 @@ use crate::param::internals::Params;
/// - Sidechain inputs /// - Sidechain inputs
/// - Multiple output busses /// - Multiple output busses
/// - Special handling for offline processing /// - 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 /// - Outputting parameter changes from the plugin
/// - MIDI CC handling /// - MIDI CC handling
/// - Outputting MIDI events from the process function (you can output parmaeter changes from an /// - Outputting MIDI events from the process function (you can output parmaeter changes from an

View file

@ -48,7 +48,6 @@ use clap_sys::process::{
use clap_sys::stream::{clap_istream, clap_ostream}; use clap_sys::stream::{clap_istream, clap_ostream};
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use crossbeam::queue::ArrayQueue; use crossbeam::queue::ArrayQueue;
use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::RwLock;
use raw_window_handle::RawWindowHandle; use raw_window_handle::RawWindowHandle;
use std::any::Any; use std::any::Any;
@ -75,15 +74,9 @@ use crate::plugin::{
}; };
use crate::util::permit_alloc; use crate::util::permit_alloc;
use crate::wrapper::state; use crate::wrapper::state;
use crate::wrapper::util::{hash_param_id, process_wrapper, strlcpy}; use crate::wrapper::util::{
hash_param_id, process_wrapper, strlcpy, Bypass, BYPASS_PARAM_HASH, BYPASS_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);
}
/// How many output parameter changes we can store in our output parameter change queue. Storing /// 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. /// more than this many parmaeters at a time will cause changes to get lost.
@ -119,9 +112,9 @@ pub struct Wrapper<P: ClapPlugin> {
/// The current buffer configuration, containing the sample rate and the maximum block size. /// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `clap_plugin::activate()`. /// Will be set in `clap_plugin::activate()`.
current_buffer_config: AtomicCell<Option<BufferConfig>>, current_buffer_config: AtomicCell<Option<BufferConfig>>,
/// Whether the plugin is currently bypassed. This is not yet integrated with the `Plugin` /// Contains either a boolean indicating whether the plugin is currently bypassed, or a bypass
/// trait. /// parameter if the plugin has one.
bypass_state: AtomicBool, bypass: Bypass,
/// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. /// 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 /// TODO: Maybe load these lazily at some point instead of needing to spool them all to this
@ -345,10 +338,6 @@ impl<P: ClapPlugin> Wrapper<P> {
.iter() .iter()
.map(|(id, _, _, _)| id.clone()) .map(|(id, _, _, _)| id.clone())
.collect(); .collect();
nih_debug_assert!(
!param_ids.contains(BYPASS_PARAM_ID),
"The wrapper already adds its own bypass parameter"
);
nih_debug_assert_eq!( nih_debug_assert_eq!(
param_map.len(), param_map.len(),
param_ids.len(), param_ids.len(),
@ -356,6 +345,31 @@ impl<P: ClapPlugin> Wrapper<P> {
); );
} }
// 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 &param_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 let param_hashes = param_id_hashes_ptrs_groups
.iter() .iter()
.map(|(_, hash, _, _)| *hash) .map(|(_, hash, _, _)| *hash)
@ -439,7 +453,7 @@ impl<P: ClapPlugin> Wrapper<P> {
num_output_channels: P::DEFAULT_NUM_OUTPUTS, num_output_channels: P::DEFAULT_NUM_OUTPUTS,
}), }),
current_buffer_config: AtomicCell::new(None), current_buffer_config: AtomicCell::new(None),
bypass_state: AtomicBool::new(false), bypass,
input_events: AtomicRefCell::new(VecDeque::with_capacity(512)), input_events: AtomicRefCell::new(VecDeque::with_capacity(512)),
last_process_status: AtomicCell::new(ProcessStatus::Normal), last_process_status: AtomicCell::new(ProcessStatus::Normal),
current_latency: AtomicU32::new(0), current_latency: AtomicU32::new(0),
@ -589,33 +603,36 @@ impl<P: ClapPlugin> Wrapper<P> {
update: ClapParamUpdate, update: ClapParamUpdate,
sample_rate: Option<f32>, sample_rate: Option<f32>,
) -> bool { ) -> bool {
if hash == *BYPASS_PARAM_HASH { match (&self.bypass, self.param_by_hash.get(&hash)) {
match update { (Bypass::Dummy(bypass_state), _) if hash == *BYPASS_PARAM_HASH => {
ClapParamUpdate::PlainValueSet(clap_plain_value) => self match update {
.bypass_state ClapParamUpdate::PlainValueSet(clap_plain_value) => {
.store(clap_plain_value >= 0.5, Ordering::SeqCst), 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
} }
};
// Also update the parameter's smoothing if applicable true
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) },
} }
(_, 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 // Also update the parameter's smoothing if applicable
} else { match (param_ptr, sample_rate) {
false (_, 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<P: ClapPlugin> Wrapper<P> {
} }
} }
// Only process audio if the plugin is not currently bypassed // Only process audio if the plugin isn't bypassed. If the plugin provides its
let result = if !wrapper.bypass_state.load(Ordering::Relaxed) { // own bypass parameter then it should decide what to do by itself.
let mut plugin = wrapper.plugin.write(); let result = match &wrapper.bypass {
let mut context = wrapper.make_process_context(transport); Bypass::Dummy(bypass_state) if bypass_state.load(Ordering::Relaxed) => {
let result = plugin.process(&mut output_buffer, &mut context); wrapper.last_process_status.store(ProcessStatus::Normal);
wrapper.last_process_status.store(result); ProcessStatus::Normal
result }
} else { _ => {
wrapper.last_process_status.store(ProcessStatus::Normal); let mut plugin = wrapper.plugin.write();
ProcessStatus::Normal 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 { let clap_result = match result {
@ -1710,9 +1731,13 @@ impl<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(0, plugin); check_null_ptr!(0, plugin);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
// NOTE: We add a bypass parameter ourselves on index `plugin.param_hashes.len()`, so // NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()` if the
// these indices are all off by one // plugin does not provide its own bypass parmaeter, in which case these indices will
wrapper.param_hashes.len() as u32 + 1 // 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( unsafe extern "C" fn ext_params_get_info(
@ -1723,8 +1748,7 @@ impl<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(false, plugin, param_info); check_null_ptr!(false, plugin, param_info);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
// Parameter index `self.param_ids.len()` is our own bypass parameter if param_index < 0 || param_index > Self::ext_params_count(plugin) as i32 {
if param_index < 0 || param_index > wrapper.param_hashes.len() as i32 {
return false; return false;
} }
@ -1733,7 +1757,9 @@ impl<P: ClapPlugin> Wrapper<P> {
// TODO: We don't use the cookies at this point. In theory this would be faster than the ID // 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. // hashmap lookup, but for now we'll stay consistent with the VST3 implementation.
let param_info = &mut *param_info; 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.id = *BYPASS_PARAM_HASH;
param_info.flags = param_info.flags =
CLAP_PARAM_IS_STEPPED | CLAP_PARAM_IS_BYPASS | CLAP_PARAM_IS_AUTOMATABLE; CLAP_PARAM_IS_STEPPED | CLAP_PARAM_IS_BYPASS | CLAP_PARAM_IS_AUTOMATABLE;
@ -1749,7 +1775,9 @@ impl<P: ClapPlugin> Wrapper<P> {
let param_ptr = &wrapper.param_by_hash[param_hash]; let param_ptr = &wrapper.param_by_hash[param_hash];
let default_value = param_ptr.default_normalized_value(); let default_value = param_ptr.default_normalized_value();
let step_count = param_ptr.step_count(); 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.id = *param_hash;
param_info.flags = if automatable { param_info.flags = if automatable {
@ -1757,6 +1785,9 @@ impl<P: ClapPlugin> Wrapper<P> {
} else { } else {
CLAP_PARAM_IS_HIDDEN | CLAP_PARAM_IS_READONLY CLAP_PARAM_IS_HIDDEN | CLAP_PARAM_IS_READONLY
}; };
if is_bypass {
param_info.flags |= CLAP_PARAM_IS_BYPASS
}
if step_count.is_some() { if step_count.is_some() {
param_info.flags |= CLAP_PARAM_IS_STEPPED param_info.flags |= CLAP_PARAM_IS_STEPPED
} }
@ -1785,19 +1816,23 @@ impl<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(false, plugin, value); check_null_ptr!(false, plugin, value);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
if param_id == *BYPASS_PARAM_HASH { match (&wrapper.bypass, wrapper.param_by_hash.get(&param_id)) {
*value = if wrapper.bypass_state.load(Ordering::SeqCst) { (Bypass::Dummy(bypass_state), _) if param_id == *BYPASS_PARAM_HASH => {
1.0 *value = if bypass_state.load(Ordering::Relaxed) {
} else { 1.0
0.0 } else {
}; 0.0
true };
} else if let Some(param_ptr) = wrapper.param_by_hash.get(&param_id) {
*value = true
param_ptr.normalized_value() as f64 * param_ptr.step_count().unwrap_or(1) as f64; }
true (_, Some(param_ptr)) => {
} else { *value = param_ptr.normalized_value() as f64
false * param_ptr.step_count().unwrap_or(1) as f64;
true
}
_ => false,
} }
} }
@ -1813,27 +1848,29 @@ impl<P: ClapPlugin> Wrapper<P> {
let dest = std::slice::from_raw_parts_mut(display, size as usize); let dest = std::slice::from_raw_parts_mut(display, size as usize);
if param_id == *BYPASS_PARAM_HASH { match (&wrapper.bypass, wrapper.param_by_hash.get(&param_id)) {
if value > 0.5 { (Bypass::Dummy(_), _) if param_id == *BYPASS_PARAM_HASH => {
strlcpy(dest, "Bypassed") if value > 0.5 {
} else { strlcpy(dest, "Bypassed")
strlcpy(dest, "Not 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
&param_ptr.normalized_value_to_string(
value as f32 / param_ptr.step_count().unwrap_or(1) as f32,
true,
),
);
true true
} else if let Some(param_ptr) = wrapper.param_by_hash.get(&param_id) { }
strlcpy( _ => false,
dest,
// CLAP does not have a separate unit, so we'll include the unit here
&param_ptr.normalized_value_to_string(
value as f32 / param_ptr.step_count().unwrap_or(1) as f32,
true,
),
);
true
} else {
false
} }
} }
@ -1851,25 +1888,30 @@ impl<P: ClapPlugin> Wrapper<P> {
Err(_) => return false, Err(_) => return false,
}; };
if param_id == *BYPASS_PARAM_HASH { match (&wrapper.bypass, wrapper.param_by_hash.get(&param_id)) {
let normalized_valeu = match display { (Bypass::Dummy(_), _) if param_id == *BYPASS_PARAM_HASH => {
"Bypassed" => 1.0, let display = display.trim();
"Not Bypassed" => 0.0, let normalized_valeu = if display.eq_ignore_ascii_case("bypassed") {
_ => return false, 1.0
}; } else if display.eq_ignore_ascii_case("not bypassed") {
*value = normalized_valeu; 0.0
} else {
return false;
};
*value = normalized_valeu;
true true
} else if let Some(param_ptr) = wrapper.param_by_hash.get(&param_id) { }
let normalized_value = match param_ptr.string_to_normalized_value(display) { (_, Some(param_ptr)) => {
Some(v) => v as f64, let normalized_value = match param_ptr.string_to_normalized_value(display) {
None => return false, Some(v) => v as f64,
}; None => return false,
*value = normalized_value * param_ptr.step_count().unwrap_or(1) as f64; };
*value = normalized_value * param_ptr.step_count().unwrap_or(1) as f64;
true true
} else { }
false _ => false,
} }
} }
@ -1901,8 +1943,7 @@ impl<P: ClapPlugin> Wrapper<P> {
wrapper.plugin.read().params(), wrapper.plugin.read().params(),
&wrapper.param_by_hash, &wrapper.param_by_hash,
&wrapper.param_id_to_hash, &wrapper.param_id_to_hash,
BYPASS_PARAM_ID, &wrapper.bypass,
&wrapper.bypass_state,
); );
match serialized { match serialized {
Ok(serialized) => { Ok(serialized) => {
@ -1964,8 +2005,7 @@ impl<P: ClapPlugin> Wrapper<P> {
&wrapper.param_by_hash, &wrapper.param_by_hash,
&wrapper.param_id_to_hash, &wrapper.param_id_to_hash,
wrapper.current_buffer_config.load().as_ref(), wrapper.current_buffer_config.load().as_ref(),
BYPASS_PARAM_ID, &wrapper.bypass,
&wrapper.bypass_state,
); );
if !success { if !success {
return false; return false;

View file

@ -3,8 +3,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::pin::Pin; 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::internals::{ParamPtr, Params};
use crate::param::Param; use crate::param::Param;
use crate::plugin::BufferConfig; use crate::plugin::BufferConfig;
@ -40,8 +41,7 @@ pub(crate) unsafe fn serialize(
plugin_params: Pin<&dyn Params>, plugin_params: Pin<&dyn Params>,
param_by_hash: &HashMap<u32, ParamPtr>, param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>, param_id_to_hash: &HashMap<String, u32>,
bypass_param_id: &str, bypass: &Bypass,
bypass_state: &AtomicBool,
) -> serde_json::Result<Vec<u8>> { ) -> serde_json::Result<Vec<u8>> {
// We'll serialize parmaeter values as a simple `string_param_id: display_value` map. // We'll serialize parmaeter values as a simple `string_param_id: display_value` map.
let mut params: HashMap<_, _> = param_id_to_hash let mut params: HashMap<_, _> = param_id_to_hash
@ -72,11 +72,13 @@ pub(crate) unsafe fn serialize(
}) })
.collect(); .collect();
// Don't forget about the bypass parameter // Don't forget about the bypass parameter if we added one for the plugin
params.insert( if let Bypass::Dummy(bypass_state) = bypass {
bypass_param_id.to_string(), params.insert(
ParamValue::Bool(bypass_state.load(Ordering::SeqCst)), 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 // The plugin can also persist arbitrary fields alongside its parameters. This is useful for
// storing things like sample data. // storing things like sample data.
@ -97,8 +99,7 @@ pub(crate) unsafe fn deserialize(
param_by_hash: &HashMap<u32, ParamPtr>, param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>, param_id_to_hash: &HashMap<String, u32>,
current_buffer_config: Option<&BufferConfig>, current_buffer_config: Option<&BufferConfig>,
bypass_param_id: &str, bypass: &Bypass,
bypass_state: &AtomicBool,
) -> bool { ) -> bool {
let state: State = match serde_json::from_slice(state) { let state: State = match serde_json::from_slice(state) {
Ok(s) => s, Ok(s) => s,
@ -110,52 +111,55 @@ pub(crate) unsafe fn deserialize(
let sample_rate = current_buffer_config.map(|c| c.sample_rate); let sample_rate = current_buffer_config.map(|c| c.sample_rate);
for (param_id_str, param_value) in state.params { for (param_id_str, param_value) in state.params {
// Handle the bypass parameter separately // Handle the automatically generated bypass parameter separately
if param_id_str == bypass_param_id { match bypass {
match param_value { Bypass::Dummy(bypass_state) if param_id_str == BYPASS_PARAM_ID => {
ParamValue::Bool(b) => bypass_state.store(b, Ordering::SeqCst), match param_value {
_ => nih_debug_assert_failure!( ParamValue::Bool(b) => bypass_state.store(b, Ordering::SeqCst),
"Invalid serialized value {:?} for parameter \"{}\"", _ => nih_debug_assert_failure!(
param_value, "Invalid serialized value {:?} for parameter \"{}\"",
param_id_str, 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; 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) { match (param_ptr, param_value) {
(ParamPtr::FloatParam(p), ParamValue::F32(v)) => (**p).set_plain_value(v), (ParamPtr::FloatParam(p), ParamValue::F32(v)) => (**p).set_plain_value(v),
(ParamPtr::IntParam(p), ParamValue::I32(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), (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 // Enums are serialized based on the active variant's index (which may not be the
// same as the discriminator) // same as the discriminator)
(ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => { (ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => {
(**p).set_plain_value(variant_idx) (**p).set_plain_value(variant_idx)
} }
(param_ptr, param_value) => { (param_ptr, param_value) => {
nih_debug_assert_failure!( nih_debug_assert_failure!(
"Invalid serialized value {:?} for parameter \"{}\" ({:?})", "Invalid serialized value {:?} for parameter \"{}\" ({:?})",
param_value, param_value,
param_id_str, param_id_str,
param_ptr, param_ptr,
); );
} }
} }
// Make sure everything starts out in sync // Make sure everything starts out in sync
if let Some(sample_rate) = sample_rate { if let Some(sample_rate) = sample_rate {
param_ptr.update_smoother(sample_rate, true); param_ptr.update_smoother(sample_rate, true);
}
}
} }
} }

View file

@ -1,13 +1,36 @@
use lazy_static::lazy_static;
use std::cmp; use std::cmp;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::os::raw::c_char; use std::os::raw::c_char;
use std::sync::atomic::AtomicBool;
use vst3_sys::vst::TChar; use vst3_sys::vst::TChar;
use widestring::U16CString; use widestring::U16CString;
use crate::param::internals::ParamPtr;
#[cfg(all(debug_assertions, feature = "assert_process_allocs"))] #[cfg(all(debug_assertions, feature = "assert_process_allocs"))]
#[global_allocator] #[global_allocator]
static A: assert_no_alloc::AllocDisabler = assert_no_alloc::AllocDisabler; 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. /// A Rabin fingerprint based string hash for parameter ID strings.
pub fn hash_param_id(id: &str) -> u32 { pub fn hash_param_id(id: &str) -> u32 {
let mut overflow; let mut overflow;

View file

@ -11,14 +11,15 @@ use vst3_sys::vst::IComponentHandler;
use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::context::{WrapperGuiContext, WrapperProcessContext};
use super::param_units::ParamUnits; 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 super::view::WrapperView;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::context::Transport; use crate::context::Transport;
use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop};
use crate::param::internals::ParamPtr; use crate::param::internals::ParamPtr;
use crate::param::ParamFlags;
use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, ProcessStatus, Vst3Plugin}; 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<T>` so we can safely use our event loop API. /// 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 /// 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<P: Vst3Plugin> {
/// The current buffer configuration, containing the sample rate and the maximum block size. /// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `IAudioProcessor::setupProcessing()`. /// Will be set in `IAudioProcessor::setupProcessing()`.
pub current_buffer_config: AtomicCell<Option<BufferConfig>>, pub current_buffer_config: AtomicCell<Option<BufferConfig>>,
/// Whether the plugin is currently bypassed. This is not yet integrated with the `Plugin` /// Contains either a boolean indicating whether the plugin is currently bypassed, or a bypass
/// trait. /// parameter if the plugin has one.
pub bypass_state: AtomicBool, pub bypass: Bypass,
/// The last process status returned by the plugin. This is used for tail handling. /// The last process status returned by the plugin. This is used for tail handling.
pub last_process_status: AtomicCell<ProcessStatus>, pub last_process_status: AtomicCell<ProcessStatus>,
/// The current latency in samples, as set by the plugin through the [`ProcessContext`]. /// The current latency in samples, as set by the plugin through the [`ProcessContext`].
@ -153,10 +154,6 @@ impl<P: Vst3Plugin> WrapperInner<P> {
.iter() .iter()
.map(|(id, _, _, _)| id.clone()) .map(|(id, _, _, _)| id.clone())
.collect(); .collect();
nih_debug_assert!(
!param_ids.contains(BYPASS_PARAM_ID),
"The wrapper already adds its own bypass parameter"
);
nih_debug_assert_eq!( nih_debug_assert_eq!(
param_map.len(), param_map.len(),
param_ids.len(), param_ids.len(),
@ -164,13 +161,38 @@ impl<P: Vst3Plugin> WrapperInner<P> {
); );
} }
// 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 &param_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 let param_hashes = param_id_hashes_ptrs_groups
.iter() .iter()
.map(|&(_, hash, _, _)| hash) .map(|(_, hash, _, _)| *hash)
.collect(); .collect();
let param_by_hash = param_id_hashes_ptrs_groups let param_by_hash = param_id_hashes_ptrs_groups
.iter() .iter()
.map(|&(_, hash, ptr, _)| (hash, ptr)) .map(|(_, hash, ptr, _)| (*hash, *ptr))
.collect(); .collect();
let param_units = ParamUnits::from_param_groups( let param_units = ParamUnits::from_param_groups(
param_id_hashes_ptrs_groups param_id_hashes_ptrs_groups
@ -207,7 +229,7 @@ impl<P: Vst3Plugin> WrapperInner<P> {
num_output_channels: P::DEFAULT_NUM_OUTPUTS, num_output_channels: P::DEFAULT_NUM_OUTPUTS,
}), }),
current_buffer_config: AtomicCell::new(None), current_buffer_config: AtomicCell::new(None),
bypass_state: AtomicBool::new(false), bypass,
last_process_status: AtomicCell::new(ProcessStatus::Normal), last_process_status: AtomicCell::new(ProcessStatus::Normal),
current_latency: AtomicU32::new(0), current_latency: AtomicU32::new(0),
output_buffer: AtomicRefCell::new(Buffer::default()), output_buffer: AtomicRefCell::new(Buffer::default()),
@ -271,24 +293,25 @@ impl<P: Vst3Plugin> WrapperInner<P> {
normalized_value: f32, normalized_value: f32,
sample_rate: Option<f32>, sample_rate: Option<f32>,
) -> tresult { ) -> tresult {
if hash == *BYPASS_PARAM_HASH { match (&self.bypass, self.param_by_hash.get(&hash)) {
self.bypass_state (Bypass::Dummy(bypass_state), _) if hash == *BYPASS_PARAM_HASH => {
.store(normalized_value >= 0.5, Ordering::SeqCst); bypass_state.store(normalized_value >= 0.5, Ordering::Relaxed);
kResultOk 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) },
} }
(_, 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 kResultOk
} else { }
kInvalidArgument _ => kInvalidArgument,
} }
} }
} }

View file

@ -1,17 +1,6 @@
use lazy_static::lazy_static;
use std::ops::Deref; use std::ops::Deref;
use vst3_sys::{interfaces::IUnknown, ComInterface}; 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 /// Early exit out of a VST3 function when one of the passed pointers is null
macro_rules! check_null_ptr { macro_rules! check_null_ptr {
($ptr:expr $(, $ptrs:expr)* $(, )?) => { ($ptr:expr $(, $ptrs:expr)* $(, )?) => {

View file

@ -15,13 +15,13 @@ use vst3_sys::VST3;
use widestring::U16CStr; use widestring::U16CStr;
use super::inner::WrapperInner; use super::inner::WrapperInner;
use super::util::{VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID}; use super::util::VstPtr;
use super::view::WrapperView; use super::view::WrapperView;
use crate::context::Transport; use crate::context::Transport;
use crate::param::ParamFlags; use crate::param::ParamFlags;
use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin};
use crate::wrapper::state; 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; use crate::wrapper::vst3::inner::ParameterChange;
// Alias needed for the VST3 attribute macro // Alias needed for the VST3 attribute macro
@ -226,8 +226,7 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
&self.inner.param_by_hash, &self.inner.param_by_hash,
&self.inner.param_id_to_hash, &self.inner.param_id_to_hash,
self.inner.current_buffer_config.load().as_ref(), self.inner.current_buffer_config.load().as_ref(),
BYPASS_PARAM_ID, &self.inner.bypass,
&self.inner.bypass_state,
); );
if !success { if !success {
return kResultFalse; return kResultFalse;
@ -261,8 +260,7 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
self.inner.plugin.read().params(), self.inner.plugin.read().params(),
&self.inner.param_by_hash, &self.inner.param_by_hash,
&self.inner.param_id_to_hash, &self.inner.param_id_to_hash,
BYPASS_PARAM_ID, &self.inner.bypass,
&self.inner.bypass_state,
); );
match serialized { match serialized {
Ok(serialized) => { Ok(serialized) => {
@ -304,9 +302,13 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
} }
unsafe fn get_parameter_count(&self) -> i32 { unsafe fn get_parameter_count(&self) -> i32 {
// NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()`, so // NOTE: We add a bypass parameter ourselves on index `self.inner.param_hashes.len()` if the
// these indices are all off by one // plugin does not provide its own bypass parmaeter, in which case these indices will
self.inner.param_hashes.len() as i32 + 1 // 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( unsafe fn get_parameter_info(
@ -316,15 +318,16 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
) -> tresult { ) -> tresult {
check_null_ptr!(info); check_null_ptr!(info);
// Parameter index `self.param_ids.len()` is our own bypass parameter if param_index < 0 || param_index > self.get_parameter_count() {
if param_index < 0 || param_index > self.inner.param_hashes.len() as i32 {
return kInvalidArgument; return kInvalidArgument;
} }
*info = std::mem::zeroed(); *info = std::mem::zeroed();
let info = &mut *info; 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; info.id = *BYPASS_PARAM_HASH;
u16strlcpy(&mut info.title, "Bypass"); u16strlcpy(&mut info.title, "Bypass");
u16strlcpy(&mut info.short_title, "Bypass"); u16strlcpy(&mut info.short_title, "Bypass");
@ -343,7 +346,9 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
.expect("Inconsistent parameter data"); .expect("Inconsistent parameter data");
let param_ptr = &self.inner.param_by_hash[param_hash]; let param_ptr = &self.inner.param_by_hash[param_hash];
let default_value = param_ptr.default_normalized_value(); 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; info.id = *param_hash;
u16strlcpy(&mut info.title, param_ptr.name()); u16strlcpy(&mut info.title, param_ptr.name());
@ -357,6 +362,9 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
} else { } else {
vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden
}; };
if is_bypass {
info.flags |= vst3_sys::vst::ParameterFlags::kIsBypass as i32;
}
} }
kResultOk kResultOk
@ -370,26 +378,27 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
) -> tresult { ) -> tresult {
check_null_ptr!(string); 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]); let dest = &mut *(string as *mut [TChar; 128]);
if id == *BYPASS_PARAM_HASH { match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) {
if value_normalized > 0.5 { (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => {
u16strlcpy(dest, "Bypassed") if value_normalized > 0.5 {
} else { u16strlcpy(dest, "Bypassed")
u16strlcpy(dest, "Not Bypassed") } else {
u16strlcpy(dest, "Not Bypassed")
}
kResultOk
} }
(_, Some(param_ptr)) => {
u16strlcpy(
dest,
&param_ptr.normalized_value_to_string(value_normalized as f32, false),
);
kResultOk kResultOk
} else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { }
u16strlcpy( _ => kInvalidArgument,
dest,
&param_ptr.normalized_value_to_string(value_normalized as f32, false),
);
kResultOk
} else {
kInvalidArgument
} }
} }
@ -406,59 +415,60 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
Err(_) => return kInvalidArgument, Err(_) => return kInvalidArgument,
}; };
if id == *BYPASS_PARAM_HASH { match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) {
let value = match string.as_str() { (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => {
"Bypassed" => 1.0, let string = string.trim();
"Not Bypassed" => 0.0, let value = if string.eq_ignore_ascii_case("bypassed") {
_ => return kResultFalse, 1.0
}; } else if string.eq_ignore_ascii_case("not bypassed") {
*value_normalized = value; 0.0
} else {
return kInvalidArgument;
};
*value_normalized = value;
kResultOk 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(param_ptr)) => {
Some(v) => v as f64, let value = match param_ptr.string_to_normalized_value(&string) {
None => return kResultFalse, Some(v) => v as f64,
}; None => return kResultFalse,
*value_normalized = value; };
*value_normalized = value;
kResultOk kResultOk
} else { }
kInvalidArgument _ => kInvalidArgument,
} }
} }
unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 { unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 {
if id == *BYPASS_PARAM_HASH { match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) {
value_normalized (Bypass::Dummy(_), _) if id == *BYPASS_PARAM_HASH => value_normalized.clamp(0.0, 1.0),
} else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { (_, Some(param_ptr)) => param_ptr.preview_plain(value_normalized as f32) as f64,
param_ptr.preview_plain(value_normalized as f32) as f64 _ => 0.5,
} else {
0.5
} }
} }
unsafe fn plain_param_to_normalized(&self, id: u32, plain_value: f64) -> f64 { unsafe fn plain_param_to_normalized(&self, id: u32, plain_value: f64) -> f64 {
if id == *BYPASS_PARAM_HASH { match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) {
plain_value.clamp(0.0, 1.0) (Bypass::Dummy(_), _) 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) { (_, Some(param_ptr)) => param_ptr.preview_normalized(plain_value as f32) as f64,
param_ptr.preview_normalized(plain_value as f32) as f64 _ => 0.5,
} else {
0.5
} }
} }
unsafe fn get_param_normalized(&self, id: u32) -> f64 { unsafe fn get_param_normalized(&self, id: u32) -> f64 {
if id == *BYPASS_PARAM_HASH { match (&self.inner.bypass, self.inner.param_by_hash.get(&id)) {
if self.inner.bypass_state.load(Ordering::SeqCst) { (Bypass::Dummy(bypass_state), _) if id == *BYPASS_PARAM_HASH => {
1.0 if bypass_state.load(Ordering::SeqCst) {
} else { 1.0
0.0 } else {
0.0
}
} }
} else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) { (_, Some(param_ptr)) => param_ptr.normalized_value() as f64,
param_ptr.normalized_value() as f64 _ => 0.5,
} else {
0.5
} }
} }
@ -921,21 +931,25 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
let mut plugin = self.inner.plugin.write(); let mut plugin = self.inner.plugin.write();
let mut context = self.inner.make_process_context(transport); let mut context = self.inner.make_process_context(transport);
// Only process audio if the plugin isn't bypassed // Only process audio if the plugin isn't bypassed. If the plugin provides its
if !self.inner.bypass_state.load(Ordering::Relaxed) { // own bypass parameter then it should decide what to do by itself.
let result = plugin.process(&mut output_buffer, &mut context); match &self.inner.bypass {
self.inner.last_process_status.store(result); Bypass::Dummy(bypass_state) if bypass_state.load(Ordering::Relaxed) => {
match result { self.inner.last_process_status.store(ProcessStatus::Normal);
ProcessStatus::Error(err) => { kResultOk
nih_debug_assert_failure!("Process error: {}", err); }
_ => {
return kResultFalse; let result = plugin.process(&mut output_buffer, &mut context);
} self.inner.last_process_status.store(result);
_ => kResultOk, 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
} }
}; };