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:
parent
3d7a23c812
commit
8090d0ae41
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ¶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<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,18 +603,21 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
update: ClapParamUpdate,
|
||||
sample_rate: Option<f32>,
|
||||
) -> bool {
|
||||
if hash == *BYPASS_PARAM_HASH {
|
||||
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) => self
|
||||
.bypass_state
|
||||
.store(clap_plain_value >= 0.5, Ordering::SeqCst),
|
||||
ClapParamUpdate::PlainValueSet(clap_plain_value) => {
|
||||
bypass_state.store(clap_plain_value >= 0.5, Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else if let Some(param_ptr) = self.param_by_hash.get(&hash) {
|
||||
}
|
||||
(_, 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
|
||||
clap_plain_value as f32
|
||||
/ unsafe { param_ptr.step_count() }.unwrap_or(1) as f32
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -614,8 +631,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
_ => 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) {
|
||||
// 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
|
||||
} else {
|
||||
wrapper.last_process_status.store(ProcessStatus::Normal);
|
||||
ProcessStatus::Normal
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
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
|
||||
} 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;
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
*value = param_ptr.normalized_value() as f64
|
||||
* param_ptr.step_count().unwrap_or(1) as f64;
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1813,7 +1848,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
|
||||
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(¶m_id)) {
|
||||
(Bypass::Dummy(_), _) if param_id == *BYPASS_PARAM_HASH => {
|
||||
if value > 0.5 {
|
||||
strlcpy(dest, "Bypassed")
|
||||
} else {
|
||||
|
@ -1821,7 +1857,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
}
|
||||
|
||||
true
|
||||
} else if let Some(param_ptr) = wrapper.param_by_hash.get(¶m_id) {
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
strlcpy(
|
||||
dest,
|
||||
// CLAP does not have a separate unit, so we'll include the unit here
|
||||
|
@ -1832,8 +1869,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1851,16 +1888,21 @@ 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,
|
||||
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) {
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
let normalized_value = match param_ptr.string_to_normalized_value(display) {
|
||||
Some(v) => v as f64,
|
||||
None => return false,
|
||||
|
@ -1868,8 +1910,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
*value = normalized_value * param_ptr.step_count().unwrap_or(1) as f64;
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
_ => 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;
|
||||
|
|
|
@ -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
|
||||
// Don't forget about the bypass parameter if we added one for the plugin
|
||||
if let Bypass::Dummy(bypass_state) = bypass {
|
||||
params.insert(
|
||||
bypass_param_id.to_string(),
|
||||
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,8 +111,9 @@ 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 {
|
||||
// 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!(
|
||||
|
@ -122,7 +124,7 @@ pub(crate) unsafe fn deserialize(
|
|||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
_ => {
|
||||
let param_ptr = match param_id_to_hash
|
||||
.get(param_id_str.as_str())
|
||||
.and_then(|hash| param_by_hash.get(hash))
|
||||
|
@ -158,6 +160,8 @@ pub(crate) unsafe fn deserialize(
|
|||
param_ptr.update_smoother(sample_rate, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The plugin can also persist arbitrary fields alongside its parameters. This is useful for
|
||||
// storing things like sample data.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ¶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<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,12 +293,13 @@ 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) {
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
// Also update the parameter's smoothing if applicable
|
||||
match (param_ptr, sample_rate) {
|
||||
(_, Some(sample_rate)) => unsafe {
|
||||
|
@ -287,8 +310,8 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
|||
}
|
||||
|
||||
kResultOk
|
||||
} else {
|
||||
kInvalidArgument
|
||||
}
|
||||
_ => kInvalidArgument,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)* $(, )?) => {
|
||||
|
|
|
@ -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,10 +378,10 @@ 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 {
|
||||
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 {
|
||||
|
@ -381,15 +389,16 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
}
|
||||
|
||||
kResultOk
|
||||
} else if let Some(param_ptr) = self.inner.param_by_hash.get(&id) {
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
u16strlcpy(
|
||||
dest,
|
||||
¶m_ptr.normalized_value_to_string(value_normalized as f32, false),
|
||||
);
|
||||
|
||||
kResultOk
|
||||
} else {
|
||||
kInvalidArgument
|
||||
}
|
||||
_ => kInvalidArgument,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,16 +415,21 @@ 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,
|
||||
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) {
|
||||
}
|
||||
(_, Some(param_ptr)) => {
|
||||
let value = match param_ptr.string_to_normalized_value(&string) {
|
||||
Some(v) => v as f64,
|
||||
None => return kResultFalse,
|
||||
|
@ -423,42 +437,38 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
*value_normalized = value;
|
||||
|
||||
kResultOk
|
||||
} else {
|
||||
kInvalidArgument
|
||||
}
|
||||
_ => 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) {
|
||||
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,8 +931,14 @@ 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) {
|
||||
// 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 {
|
||||
|
@ -933,9 +949,7 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
}
|
||||
_ => kResultOk,
|
||||
}
|
||||
} else {
|
||||
self.inner.last_process_status.store(ProcessStatus::Normal);
|
||||
kResultOk
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue