diff --git a/src/plugin.rs b/src/plugin.rs index 0f070451..6d8580b1 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -9,6 +9,7 @@ use crate::context::{GuiContext, InitContext, ProcessContext}; use crate::midi::MidiConfig; use crate::params::Params; use crate::wrapper::clap::features::ClapFeature; +use crate::wrapper::state::PluginState; /// Basic functionality that needs to be implemented by a plugin. The wrappers will use this to /// expose the plugin in a particular plugin format. @@ -100,6 +101,18 @@ pub trait Plugin: Default + Send + Sync + 'static { None } + /// This function is always called just before a [`PluginState`] is loaded. This lets you + /// directly modify old plugin state to perform migrations based on the [`PluginState::version`] + /// field. Some examples of use cases for this are renaming parameter indices, remapping + /// parameter values, and preserving old preset compatibility when introducing new parameters + /// with default values that would otherwise change the sound of a preset. Keep in mind that + /// automation may still be broken in the first two use cases. + /// + /// # Note + /// + /// This is an advanced feature that the vast majority of plugins won't need to implement. + fn filter_state(state: &mut PluginState) {} + // // The following functions follow the lifetime of the plugin. // diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index fb93d010..f4538d79 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -1588,8 +1588,8 @@ impl Wrapper

{ // Otherwise we'll set the state right here and now, since this function should be // called from a GUI thread unsafe { - state::deserialize_object( - &state, + state::deserialize_object::

( + &mut state, self.params.clone(), state::make_params_getter(&self.param_by_hash, &self.param_id_to_hash), self.current_buffer_config.load().as_ref(), @@ -2231,9 +2231,9 @@ impl Wrapper

{ // FIXME: Zero capacity channels allocate on receiving, find a better alternative that // doesn't do that let updated_state = permit_alloc(|| wrapper.updated_state_receiver.try_recv()); - if let Ok(state) = updated_state { - state::deserialize_object( - &state, + if let Ok(mut state) = updated_state { + state::deserialize_object::

( + &mut state, wrapper.params.clone(), state::make_params_getter(&wrapper.param_by_hash, &wrapper.param_id_to_hash), wrapper.current_buffer_config.load().as_ref(), @@ -3130,7 +3130,7 @@ impl Wrapper

{ } read_buffer.set_len(length as usize); - let success = state::deserialize_json( + let success = state::deserialize_json::

( &read_buffer, wrapper.params.clone(), state::make_params_getter(&wrapper.param_by_hash, &wrapper.param_id_to_hash), diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index 9ffee5fe..c997cf1b 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -435,10 +435,10 @@ impl Wrapper { // FIXME: Zero capacity channels allocate on receiving, find a better // alternative that doesn't do that let updated_state = permit_alloc(|| self.updated_state_receiver.try_recv()); - if let Ok(state) = updated_state { + if let Ok(mut state) = updated_state { unsafe { - state::deserialize_object( - &state, + state::deserialize_object::

( + &mut state, self.params.clone(), |param_id| self.param_map.get(param_id).copied(), Some(&self.buffer_config), diff --git a/src/wrapper/state.rs b/src/wrapper/state.rs index 042cfed8..913076d4 100644 --- a/src/wrapper/state.rs +++ b/src/wrapper/state.rs @@ -159,12 +159,18 @@ pub(crate) unsafe fn serialize_json<'a, P: Plugin>( /// /// Make sure to reinitialize plugin after deserializing the state so it can react to the new /// parameter values. The smoothers have already been reset by this function. -pub(crate) unsafe fn deserialize_object( - state: &PluginState, +/// +/// The [`Plugin`] argument is used to call [`Plugin::filter_state()`] just before loading the +/// state. +pub(crate) unsafe fn deserialize_object( + state: &mut PluginState, plugin_params: Arc, params_getter: impl Fn(&str) -> Option, current_buffer_config: Option<&BufferConfig>, ) -> bool { + // This lets the plugin perform migrations on old state if needed + P::filter_state(state); + let sample_rate = current_buffer_config.map(|c| c.sample_rate); for (param_id_str, param_value) in &state.params { let param_ptr = match params_getter(param_id_str.as_str()) { @@ -223,14 +229,17 @@ pub(crate) unsafe fn deserialize_object( /// /// Make sure to reinitialize plugin after deserializing the state so it can react to the new /// parameter values. The smoothers have already been reset by this function. -pub(crate) unsafe fn deserialize_json( +/// +/// The [`Plugin`] argument is used to call [`Plugin::filter_state()`] just before loading the +/// state. +pub(crate) unsafe fn deserialize_json( state: &[u8], plugin_params: Arc, params_getter: impl Fn(&str) -> Option, current_buffer_config: Option<&BufferConfig>, ) -> bool { #[cfg(feature = "zstd")] - let state: PluginState = match zstd::decode_all(state) { + let mut state: PluginState = match zstd::decode_all(state) { Ok(decompressed) => match serde_json::from_slice(decompressed.as_slice()) { Ok(s) => { nih_log!("Deserialized compressed"); @@ -261,7 +270,7 @@ pub(crate) unsafe fn deserialize_json( }; #[cfg(not(feature = "zstd"))] - let state: PluginState = match serde_json::from_slice(state) { + let mut state: PluginState = match serde_json::from_slice(state) { Ok(s) => s, Err(err) => { nih_debug_assert_failure!("Error while deserializing state: {}", err); @@ -269,5 +278,10 @@ pub(crate) unsafe fn deserialize_json( } }; - deserialize_object(&state, plugin_params, params_getter, current_buffer_config) + deserialize_object::

( + &mut state, + plugin_params, + params_getter, + current_buffer_config, + ) } diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 2d5be68e..55205fb6 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -458,8 +458,8 @@ impl WrapperInner

{ // Otherwise we'll set the state right here and now, since this function should be // called from a GUI thread unsafe { - state::deserialize_object( - &state, + state::deserialize_object::

( + &mut state, self.params.clone(), state::make_params_getter(&self.param_by_hash, &self.param_id_to_hash), self.current_buffer_config.load().as_ref(), diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 46201819..7923fa14 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -504,7 +504,7 @@ impl IComponent for Wrapper

{ return kResultFalse; } - let success = state::deserialize_json( + let success = state::deserialize_json::

( &read_buffer, self.inner.params.clone(), state::make_params_getter(&self.inner.param_by_hash, &self.inner.param_id_to_hash), @@ -1769,9 +1769,9 @@ impl IAudioProcessor for Wrapper

{ // FIXME: Zero capacity channels allocate on receiving, find a better alternative that // doesn't do that let updated_state = permit_alloc(|| self.inner.updated_state_receiver.try_recv()); - if let Ok(state) = updated_state { - state::deserialize_object( - &state, + if let Ok(mut state) = updated_state { + state::deserialize_object::

( + &mut state, self.inner.params.clone(), state::make_params_getter( &self.inner.param_by_hash,