From e5a26ac19928bdf65fa9e8f05c810d05b997e14b Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 11 Jan 2023 16:59:21 +0100 Subject: [PATCH] Send individual parameter changes for VST3 plugins --- nih_plug_derive/src/params.rs | 4 +- plugins/diopser/src/editor/analyzer.rs | 2 +- plugins/diopser/src/editor/xy_pad.rs | 4 +- src/wrapper/clap/wrapper.rs | 2 +- src/wrapper/vst3/context.rs | 1 - src/wrapper/vst3/inner.rs | 55 +++++++++++++++++--------- src/wrapper/vst3/wrapper.rs | 36 ++++++----------- 7 files changed, 55 insertions(+), 49 deletions(-) diff --git a/nih_plug_derive/src/params.rs b/nih_plug_derive/src/params.rs index 955d8a62..082cfb63 100644 --- a/nih_plug_derive/src/params.rs +++ b/nih_plug_derive/src/params.rs @@ -475,7 +475,7 @@ enum NestedParams { field: syn::Ident, group: Option, }, - /// The nested struct's parameters will get an ID prefix. The original parmaeter with ID `foo` + /// The nested struct's parameters will get an ID prefix. The original parameter with ID `foo` /// will become `{id_prefix}_foo`. Prefixed { field: syn::Ident, @@ -492,7 +492,7 @@ enum NestedParams { } impl NestedParams { - /// Constrruct an iterator that iterates over all parmaeters of a nested parameter object. This + /// Constrruct an iterator that iterates over all parameters of a nested parameter object. This /// takes ID prefixes and suffixes into account, and prefixes the group to the parameter's /// existing groups if the `group` attribute on the `#[nested]` macro was specified. fn param_map_tokens(&self) -> proc_macro2::TokenStream { diff --git a/plugins/diopser/src/editor/analyzer.rs b/plugins/diopser/src/editor/analyzer.rs index 4944d8dc..7447600c 100644 --- a/plugins/diopser/src/editor/analyzer.rs +++ b/plugins/diopser/src/editor/analyzer.rs @@ -26,7 +26,7 @@ use crate::params; use crate::spectrum::SpectrumOutput; /// A very abstract spectrum analyzer. This draws the magnitude spectrum's bins as vertical lines -/// with the same distirubtion as the filter frequency parmaeter.. +/// with the same distirubtion as the filter frequency parameter.. pub struct SpectrumAnalyzer { spectrum: Arc>, sample_rate: Arc, diff --git a/plugins/diopser/src/editor/xy_pad.rs b/plugins/diopser/src/editor/xy_pad.rs index 0c3605fa..1155a5bc 100644 --- a/plugins/diopser/src/editor/xy_pad.rs +++ b/plugins/diopser/src/editor/xy_pad.rs @@ -30,7 +30,7 @@ const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1; const HANDLE_WIDTH_PX: f32 = 20.0; /// An X-Y pad that controlers two parameters at the same time by binding them to one of the two -/// axes. This specific implementation has a tooltip for the X-axis parmaeter and allows +/// axes. This specific implementation has a tooltip for the X-axis parameter and allows /// Alt+clicking to enter a specific value. /// /// The x-parameter's range is restricted when safe mode is enabled. See `RestrictedParamSlider` for @@ -309,7 +309,7 @@ impl XyPad { /// Should be called at the start of a drag operation. fn begin_set_parameters(&self, cx: &mut EventContext) { - // NOTE: Since the X-parameter is the main parmaeter, we'll always modify this parameter + // NOTE: Since the X-parameter is the main parameter, we'll always modify this parameter // last so the host will keep this parameter highlighted self.y_param_base.begin_set_parameter(cx); self.x_param_base.begin_set_parameter(cx); diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index fd4874d9..48f6f881 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -215,7 +215,7 @@ pub struct Wrapper { /// parameters belonging to the plugin. These addresses will remain stable as long as the /// `params` object does not get deallocated. param_by_hash: HashMap, - /// Mappings from parmaeter hashes to string parameter IDs. Used for notifying the plugin's + /// Mappings from parameter hashes to string parameter IDs. Used for notifying the plugin's /// editor about parameter changes. param_id_by_hash: HashMap, /// The group name of a parameter, indexed by the parameter's hash. Nested groups are delimited diff --git a/src/wrapper/vst3/context.rs b/src/wrapper/vst3/context.rs index e9efc50f..488508f8 100644 --- a/src/wrapper/vst3/context.rs +++ b/src/wrapper/vst3/context.rs @@ -166,7 +166,6 @@ impl GuiContext for WrapperGuiContext

{ .load() .map(|c| c.sample_rate), ); - self.inner.notify_param_values_changed(); } handler.perform_edit(*hash, normalized as f64); diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 9b8aa0dd..e58a652e 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -139,6 +139,9 @@ pub(crate) struct WrapperInner { /// parameters belonging to the plugin. These addresses will remain stable as long as the /// `params` object does not get deallocated. pub param_by_hash: HashMap, + /// Mappings from parameter hashes to string parameter IDs. Used for notifying the plugin's + /// editor about parameter changes. + pub param_id_by_hash: HashMap, pub param_units: ParamUnits, /// Mappings from string parameter identifiers to parameter hashes. Useful for debug logging /// and when storing and restoring plugin state. @@ -157,6 +160,11 @@ pub(crate) struct WrapperInner { pub enum Task { /// Execute one of the plugin's background tasks. PluginTask(P::BackgroundTask), + /// Inform the plugin that one or more parameter values have changed. + ParameterValuesChanged, + /// Inform the plugin that one parameter's value has changed. This uses the parameter hashes + /// since the task will be created from the audio thread. + ParameterValueChanged(u32, f32), /// Trigger a restart with the given restart flags. This is a bit set of the flags from /// [`vst3_sys::vst::RestartFlags`]. TriggerRestart(i32), @@ -264,6 +272,10 @@ impl WrapperInner

{ .iter() .map(|(_, hash, ptr, _)| (*hash, *ptr)) .collect(); + let param_id_by_hash = param_id_hashes_ptrs_groups + .iter() + .map(|(id, hash, _, _)| (*hash, id.clone())) + .collect(); let param_units = ParamUnits::from_param_groups( param_id_hashes_ptrs_groups .iter() @@ -320,6 +332,7 @@ impl WrapperInner

{ param_hashes, param_by_hash, + param_id_by_hash, param_units, param_id_to_hash, param_ptr_to_hash, @@ -420,23 +433,6 @@ impl WrapperInner

{ } } - /// If there's an editor open, let it know that parameter values have changed. This should be - /// called whenever there's been a call or multiple calls to - /// [`set_normalized_value_by_hash()[Self::set_normalized_value_by_hash()`]. In the off-chance - /// that the editor instance is currently locked then nothing will happen, and the request can - /// safely be ignored. - pub fn notify_param_values_changed(&self) { - if let Some(editor) = self.editor.borrow().as_ref() { - match editor.try_lock() { - Some(editor) => editor.param_values_changed(), - None => nih_debug_assert_failure!( - "The editor was locked when sending a parameter value change notification, \ - ignoring" - ), - } - } - } - /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter /// update. The same rate is for updating parameter smoothing. /// @@ -461,6 +457,10 @@ impl WrapperInner

{ _ => unsafe { param_ptr.set_normalized_value(normalized_value) }, } + let task_posted = + self.schedule_gui(Task::ParameterValueChanged(hash, normalized_value)); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + kResultOk } _ => kInvalidArgument, @@ -524,7 +524,6 @@ impl WrapperInner

{ ); } - self.notify_param_values_changed(); let bus_config = self.current_bus_config.load(); if let Some(buffer_config) = self.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks @@ -534,6 +533,9 @@ impl WrapperInner

{ process_wrapper(|| plugin.reset()); } + let task_posted = self.schedule_gui(Task::ParameterValuesChanged); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + break; } } @@ -566,6 +568,23 @@ impl MainThreadExecutor> for WrapperInner

{ // This function is always called from the main thread match task { Task::PluginTask(task) => (self.task_executor.lock())(task), + Task::ParameterValuesChanged => { + if self.plug_view.read().is_some() { + if let Some(editor) = self.editor.borrow().as_ref() { + editor.lock().param_values_changed(); + } + } + } + Task::ParameterValueChanged(param_hash, normalized_value) => { + if self.plug_view.read().is_some() { + if let Some(editor) = self.editor.borrow().as_ref() { + let param_id = &self.param_id_by_hash[¶m_hash]; + editor + .lock() + .param_value_changed(param_id, normalized_value); + } + } + } Task::TriggerRestart(flags) => match &*self.component_handler.borrow() { Some(handler) => unsafe { nih_debug_assert!(is_gui_thread); diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index fca87729..25567b6d 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -18,10 +18,12 @@ use vst3_sys::vst::{ use vst3_sys::VST3; use widestring::U16CStr; -use super::inner::WrapperInner; +use super::inner::{ProcessEvent, Task, WrapperInner}; +use super::note_expressions::{self, NoteExpressionController}; use super::util::{ u16strlcpy, VstPtr, VST3_MIDI_CCS, VST3_MIDI_NUM_PARAMS, VST3_MIDI_PARAMS_START, }; +use super::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END}; use super::view::WrapperView; use crate::buffer::Buffer; use crate::context::process::Transport; @@ -34,9 +36,6 @@ use crate::plugin::{ use crate::util::permit_alloc; use crate::wrapper::state; use crate::wrapper::util::process_wrapper; -use crate::wrapper::vst3::inner::ProcessEvent; -use crate::wrapper::vst3::note_expressions::{self, NoteExpressionController}; -use crate::wrapper::vst3::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END}; // Alias needed for the VST3 attribute macro use vst3_sys as vst3_com; @@ -512,9 +511,6 @@ impl IComponent for Wrapper

{ return kResultFalse; } - // Reinitialize the plugin after loading state so it can respond to the new parameter values - self.inner.notify_param_values_changed(); - if let Some(buffer_config) = self.inner.current_buffer_config.load() { // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.inner.make_init_context(); @@ -527,6 +523,10 @@ impl IComponent for Wrapper

{ process_wrapper(|| plugin.reset()); } + // Reinitialize the plugin after loading state so it can respond to the new parameter values + let task_posted = self.inner.schedule_gui(Task::ParameterValuesChanged); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + nih_trace!("Loaded state ({} bytes)", read_buffer.len()); kResultOk @@ -747,12 +747,8 @@ impl IEditController for Wrapper

{ .current_buffer_config .load() .map(|c| c.sample_rate); - let result = self - .inner - .set_normalized_value_by_hash(id, value as f32, sample_rate); - self.inner.notify_param_values_changed(); - - result + self.inner + .set_normalized_value_by_hash(id, value as f32, sample_rate) } unsafe fn set_component_handler( @@ -1073,7 +1069,6 @@ impl IAudioProcessor for Wrapper

{ // messages are sent through parameter changes. This vector gets sorted at the end so we // can treat it as a sort of queue. let mut process_events = self.inner.process_events.borrow_mut(); - let mut parameter_values_changed = false; process_events.clear(); // First we'll go through the parameter changes. This may also include MIDI CC messages @@ -1152,7 +1147,6 @@ impl IAudioProcessor for Wrapper

{ value, Some(sample_rate), ); - parameter_values_changed = true; } } } @@ -1297,7 +1291,6 @@ impl IAudioProcessor for Wrapper

{ normalized_value, Some(sample_rate), ); - parameter_values_changed = true; } ProcessEvent::NoteEvent { timing: _, @@ -1312,12 +1305,6 @@ impl IAudioProcessor for Wrapper

{ } } - // This allows the GUI to react to incoming parameter changes - if parameter_values_changed { - self.inner.notify_param_values_changed(); - parameter_values_changed = false; - } - // This vector has been preallocated to contain enough slices as there are output // channels. In case the does does not provide an output or if they don't provide // all of the channels (this should not happen, but Ableton Live might do it) then @@ -1797,8 +1784,6 @@ impl IAudioProcessor for Wrapper

{ self.inner.current_buffer_config.load().as_ref(), ); - self.inner.notify_param_values_changed(); - // NOTE: This needs to be dropped after the `plugin` lock to avoid deadlocks let mut init_context = self.inner.make_init_context(); let bus_config = self.inner.current_bus_config.load(); @@ -1811,6 +1796,9 @@ impl IAudioProcessor for Wrapper

{ permit_alloc(|| plugin.initialize(&bus_config, &buffer_config, &mut init_context)); plugin.reset(); + let task_posted = self.inner.schedule_gui(Task::ParameterValuesChanged); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + // We'll pass the state object back to the GUI thread so deallocation can happen // there without potentially blocking the audio thread if let Err(err) = self.inner.updated_state_sender.send(state) {