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"]`.
- 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.

View file

@ -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;
}
}

View file

@ -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.

View file

@ -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

View file

@ -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<P: ClapPlugin> {
/// The current buffer configuration, containing the sample rate and the maximum block size.
/// Will be set in `clap_plugin::activate()`.
current_buffer_config: AtomicCell<Option<BufferConfig>>,
/// 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<P: ClapPlugin> Wrapper<P> {
.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<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
.iter()
.map(|(_, hash, _, _)| *hash)
@ -439,7 +453,7 @@ impl<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
update: ClapParamUpdate,
sample_rate: Option<f32>,
) -> 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<P: ClapPlugin> Wrapper<P> {
}
}
// 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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
// 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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
} 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<P: ClapPlugin> Wrapper<P> {
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(&param_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(&param_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<P: ClapPlugin> Wrapper<P> {
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(&param_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
&param_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(&param_id) {
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
} else {
false
true
}
_ => false,
}
}
@ -1851,25 +1888,30 @@ impl<P: ClapPlugin> Wrapper<P> {
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(&param_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(&param_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<P: ClapPlugin> Wrapper<P> {
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<P: ClapPlugin> Wrapper<P> {
&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;

View file

@ -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<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>,
bypass_param_id: &str,
bypass_state: &AtomicBool,
bypass: &Bypass,
) -> serde_json::Result<Vec<u8>> {
// 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<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>,
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);
}
}
}
}

View file

@ -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;

View file

@ -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<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
@ -56,9 +57,9 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// 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,
/// 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<ProcessStatus>,
/// The current latency in samples, as set by the plugin through the [`ProcessContext`].
@ -153,10 +154,6 @@ impl<P: Vst3Plugin> WrapperInner<P> {
.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<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
.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<P: Vst3Plugin> WrapperInner<P> {
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<P: Vst3Plugin> WrapperInner<P> {
normalized_value: f32,
sample_rate: Option<f32>,
) -> 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,
}
}
}

View file

@ -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)* $(, )?) => {

View file

@ -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<P: Vst3Plugin> IComponent for Wrapper<P> {
&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<P: Vst3Plugin> IComponent for Wrapper<P> {
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<P: Vst3Plugin> IEditController for Wrapper<P> {
}
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<P: Vst3Plugin> IEditController for Wrapper<P> {
) -> 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<P: Vst3Plugin> IEditController for Wrapper<P> {
.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<P: Vst3Plugin> IEditController for Wrapper<P> {
} 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<P: Vst3Plugin> IEditController for Wrapper<P> {
) -> 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,
&param_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,
&param_ptr.normalized_value_to_string(value_normalized as f32, false),
);
kResultOk
} else {
kInvalidArgument
kResultOk
}
_ => kInvalidArgument,
}
}
@ -406,59 +415,60 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
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
}
};