From d0064f87d686a889cd049dbb52c94f2a3df07ebd Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 7 Apr 2022 17:12:08 +0200 Subject: [PATCH] Add state saving and restoring through GuiContext While preventing any possible data races. --- src/context.rs | 11 +++ src/plugin.rs | 1 - src/wrapper/clap/context.rs | 8 ++ src/wrapper/clap/wrapper.rs | 148 ++++++++++++++++++++++++++++++++++-- src/wrapper/state.rs | 76 ++++++++++++------ src/wrapper/vst3/context.rs | 9 +++ src/wrapper/vst3/inner.rs | 112 ++++++++++++++++++++++++++- src/wrapper/vst3/wrapper.rs | 42 +++++++++- 8 files changed, 373 insertions(+), 34 deletions(-) diff --git a/src/context.rs b/src/context.rs index 591b7ebd..2ca80dd8 100644 --- a/src/context.rs +++ b/src/context.rs @@ -3,6 +3,7 @@ use crate::param::internals::ParamPtr; use crate::param::Param; use crate::plugin::NoteEvent; +use crate::wrapper::state::State; // TODO: ProcessContext for parameter automation and sending events @@ -79,6 +80,16 @@ pub trait GuiContext: Send + Sync + 'static { /// The implementing function still needs to check if `param` actually exists. This function is /// mostly marked as unsafe for API reasons. unsafe fn raw_end_set_parameter(&self, param: ParamPtr); + + /// Serialize the plugin's current state to a serde-serializable object. Useful for implementing + /// preset handling within a plugin's GUI. + fn get_state(&self) -> State; + + /// Restore the state from a previously serialized state object. This will block the GUI thread + /// until the state has been restored and a parameter value rescan has been requested from the + /// host. If the plugin is currently processing audio, then the parameter values will be + /// restored at the end of the current processing cycle. + fn set_state(&self, state: State); } /// Information about the plugin's transport. Depending on the plugin API and the host not all diff --git a/src/plugin.rs b/src/plugin.rs index e339080b..2f4b0028 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -20,7 +20,6 @@ use crate::param::internals::Params; /// - Sidechain inputs /// - Multiple output busses /// - Special handling for offline processing -/// - Outputting parameter changes from the plugin /// - MIDI CC handling /// - Outputting MIDI events from the process function (you can output parmaeter changes from an /// editor GUI) diff --git a/src/wrapper/clap/context.rs b/src/wrapper/clap/context.rs index a968dc18..5ca8cede 100644 --- a/src/wrapper/clap/context.rs +++ b/src/wrapper/clap/context.rs @@ -79,6 +79,14 @@ impl GuiContext for WrapperGuiContext

{ None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), } } + + fn get_state(&self) -> crate::wrapper::state::State { + self.wrapper.get_state_object() + } + + fn set_state(&self, state: crate::wrapper::state::State) { + self.wrapper.set_state_object(state) + } } impl ProcessContext for WrapperProcessContext<'_, P> { diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 8fcc8a39..5f928308 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -32,7 +32,7 @@ use clap_sys::ext::note_ports::{ use clap_sys::ext::params::{ clap_host_params, clap_param_info, clap_plugin_params, CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_HIDDEN, CLAP_PARAM_IS_READONLY, - CLAP_PARAM_IS_STEPPED, + CLAP_PARAM_IS_STEPPED, CLAP_PARAM_RESCAN_VALUES, }; use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE}; use clap_sys::ext::tail::{clap_plugin_tail, CLAP_EXT_TAIL}; @@ -47,6 +47,7 @@ use clap_sys::process::{ }; use clap_sys::stream::{clap_istream, clap_ostream}; use crossbeam::atomic::AtomicCell; +use crossbeam::channel::{self, SendTimeoutError}; use crossbeam::queue::ArrayQueue; use parking_lot::RwLock; use raw_window_handle::RawWindowHandle; @@ -60,6 +61,7 @@ use std::ptr; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Weak}; use std::thread::{self, ThreadId}; +use std::time::Duration; use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::descriptor::PluginDescriptor; @@ -73,7 +75,7 @@ use crate::plugin::{ BufferConfig, BusConfig, ClapPlugin, Editor, NoteEvent, ParentWindowHandle, ProcessStatus, }; use crate::util::permit_alloc; -use crate::wrapper::state; +use crate::wrapper::state::{self, State}; use crate::wrapper::util::{hash_param_id, process_wrapper, strlcpy}; /// How many output parameter changes we can store in our output parameter change queue. Storing @@ -129,6 +131,17 @@ pub struct Wrapper { /// between process calls. This buffer owns the vector, because otherwise it would need to store /// a mutable reference to the data contained in this mutex. pub output_buffer: AtomicRefCell>, + /// The plugin is able to restore state through a method on the `GuiContext`. To avoid changing + /// parameters mid-processing and running into garbled data if the host also tries to load state + /// at the same time the restoring happens at the end of each processing call. If this zero + /// capacity channel contains state data at that point, then the audio thread will take the + /// state out of the channel, restore the state, and then send it back through the same channel. + /// In other words, the GUI thread acts as a sender and then as a receiver, while the audio + /// thread acts as a receiver and then as a sender. That way deallocation can happen on the GUI + /// thread. All of this happens without any blocking on the audio thread. + updated_state_sender: channel::Sender, + /// The receiver belonging to [`new_state_sender`][Self::new_state_sender]. + updated_state_receiver: channel::Receiver, /// Needs to be boxed because the plugin object is supposed to contain a static reference to /// this. @@ -310,6 +323,10 @@ impl Wrapper

{ let plugin = RwLock::new(P::default()); let editor = plugin.read().editor().map(Arc::from); + // This is used to allow the plugin to restore preset data from its editor, see the comment + // on `Self::updated_state_sender` + let (updated_state_sender, updated_state_receiver) = channel::bounded(0); + let plugin_descriptor = Box::new(PluginDescriptor::default()); // We're not allowed to query any extensions until the init function has been called, so we @@ -448,6 +465,8 @@ impl Wrapper

{ last_process_status: AtomicCell::new(ProcessStatus::Normal), current_latency: AtomicU32::new(0), output_buffer: AtomicRefCell::new(Buffer::default()), + updated_state_sender, + updated_state_receiver, plugin_descriptor, @@ -898,6 +917,90 @@ impl Wrapper

{ } } + /// Get the plugin's state object, may be called by the plugin's GUI as part of its own preset + /// management. The wrapper doesn't use these functions and serializes and deserializes directly + /// the JSON in the relevant plugin API methods instead. + pub fn get_state_object(&self) -> State { + unsafe { + state::serialize_object( + self.params.clone(), + &self.param_by_hash, + &self.param_id_to_hash, + ) + } + } + + /// Update the plugin's internal state, called by the plugin itself from the GUI thread. To + /// prevent corrupting data and changing parameters during processing the actual state is only + /// updated at the end of the audio processing cycle. + pub fn set_state_object(&self, mut state: State) { + // Use a loop and timeouts to handle the super rare edge case when this function gets called + // between a process call and the host disabling the plugin + loop { + if self.is_processing.load(Ordering::SeqCst) { + // If the plugin is currently processing audio, then we'll perform the restore + // operation at the end of the audio call. This involves sending the state to the + // audio thread, having the audio thread handle the state restore at the very end of + // the process function, and then sending the state back to this thread so it can be + // deallocated without blocking the audio thread. + match self + .updated_state_sender + .send_timeout(state, Duration::from_secs(1)) + { + Ok(_) => { + // As mentioned above, the state object will be passed back to this thread + // so we can deallocate it without blocking. + let state = self.updated_state_receiver.recv(); + drop(state); + break; + } + Err(SendTimeoutError::Timeout(value)) => { + state = value; + continue; + } + Err(SendTimeoutError::Disconnected(_)) => { + nih_debug_assert_failure!("State update channel got disconnected"); + return; + } + } + } else { + // 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, + self.params.clone(), + &self.param_by_hash, + &self.param_id_to_hash, + self.current_buffer_config.load().as_ref(), + ); + } + + self.notify_param_values_changed(); + let bus_config = self.current_bus_config.load(); + if let Some(buffer_config) = self.current_buffer_config.load() { + let mut plugin = self.plugin.write(); + plugin.initialize( + &bus_config, + &buffer_config, + &mut self.make_process_context(Transport::new(buffer_config.sample_rate)), + ); + process_wrapper(|| plugin.reset()); + } + + break; + } + } + + // After the state has been updated, notify the host about the new parameter values + match &*self.host_params.borrow() { + Some(host_params) => { + (host_params.rescan)(&*self.host_callback, CLAP_PARAM_RESCAN_VALUES) + } + None => nih_debug_assert_failure!("The host does not support parameters? What?"), + } + } + unsafe extern "C" fn init(plugin: *const clap_plugin) -> bool { check_null_ptr!(false, plugin); let wrapper = &*(plugin as *const Self); @@ -1025,7 +1128,7 @@ impl Wrapper

{ let mut block_start = 0; let mut block_end = process.frames_count as usize; let mut event_start_idx = 0; - loop { + let result = loop { if !process.in_events.is_null() { if P::SAMPLE_ACCURATE_AUTOMATION { let split_result = wrapper.handle_in_events_until_next_param_change( @@ -1226,7 +1329,42 @@ impl Wrapper

{ } else { block_start = block_end; } + }; + + // After processing audio, we'll check if the editor has sent us updated plugin state. + // We'll restore that here on the audio thread to prevent changing the values during the + // process call and also to prevent inconsistent state when the host also wants to load + // plugin state. + // 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, + wrapper.params.clone(), + &wrapper.param_by_hash, + &wrapper.param_id_to_hash, + wrapper.current_buffer_config.load().as_ref(), + ); + + wrapper.notify_param_values_changed(); + + // TODO: Normally we'd also call initialize after deserializing state, but that's + // not guaranteed to be realtime safe. Should we do it anyways? + let mut plugin = wrapper.plugin.write(); + plugin.reset(); + + // 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) = wrapper.updated_state_sender.send(state) { + nih_debug_assert_failure!( + "Failed to send state object back to GUI thread: {}", + err + ); + }; } + + result }) } @@ -1919,7 +2057,7 @@ impl Wrapper

{ check_null_ptr!(false, plugin, stream); let wrapper = &*(plugin as *const Self); - let serialized = state::serialize( + let serialized = state::serialize_json( wrapper.params.clone(), &wrapper.param_by_hash, &wrapper.param_id_to_hash, @@ -1978,7 +2116,7 @@ impl Wrapper

{ nih_debug_assert_eq!(num_bytes_read as u64, length); read_buffer.set_len(length as usize); - let success = state::deserialize( + let success = state::deserialize_json( &read_buffer, wrapper.params.clone(), &wrapper.param_by_hash, diff --git a/src/wrapper/state.rs b/src/wrapper/state.rs index 4525b72f..8b0b53df 100644 --- a/src/wrapper/state.rs +++ b/src/wrapper/state.rs @@ -38,13 +38,13 @@ pub struct State { pub fields: HashMap, } -/// Serialize a plugin's state to a vector containing JSON data. This can (and should) be shared -/// across plugin formats. -pub(crate) unsafe fn serialize( +/// Serialize a plugin's state to a state object. This is separate from [`serialize_json()`] to +/// allow passing the raw object directly to the plugin. +pub(crate) unsafe fn serialize_object( plugin_params: Arc, param_by_hash: &HashMap, param_id_to_hash: &HashMap, -) -> serde_json::Result> { +) -> State { // We'll serialize parmaeter values as a simple `string_param_id: display_value` map. let params: HashMap<_, _> = param_id_to_hash .iter() @@ -78,32 +78,35 @@ pub(crate) unsafe fn serialize( // storing things like sample data. let fields = plugin_params.serialize_fields(); - let plugin_state = State { params, fields }; - serde_json::to_vec(&plugin_state) + State { params, fields } } /// Serialize a plugin's state to a vector containing JSON data. This can (and should) be shared -/// across plugin formats. Returns `false` and logs an error if the state could not be deserialized. +/// across plugin formats. +pub(crate) unsafe fn serialize_json( + plugin_params: Arc, + param_by_hash: &HashMap, + param_id_to_hash: &HashMap, +) -> serde_json::Result> { + let plugin_state = serialize_object(plugin_params, param_by_hash, param_id_to_hash); + serde_json::to_vec(&plugin_state) +} + +/// Deserialize a plugin's state from a [`State`] object. This is used to allow the plugin to do its +/// own internal preset management. Returns `false` and logs an error if the state could not be +/// deserialized. /// /// 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( - state: &[u8], +pub(crate) unsafe fn deserialize_object( + state: &State, plugin_params: Arc, param_by_hash: &HashMap, param_id_to_hash: &HashMap, current_buffer_config: Option<&BufferConfig>, ) -> bool { - let state: State = match serde_json::from_slice(state) { - Ok(s) => s, - Err(err) => { - nih_debug_assert_failure!("Error while deserializing state: {}", err); - return false; - } - }; - 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 { let param_ptr = match param_id_to_hash .get(param_id_str.as_str()) .and_then(|hash| param_by_hash.get(hash)) @@ -116,13 +119,13 @@ pub(crate) unsafe fn deserialize( }; 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), + (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) + (**p).set_plain_value(*variant_idx) } (param_ptr, param_value) => { nih_debug_assert_failure!( @@ -146,3 +149,32 @@ pub(crate) unsafe fn deserialize( true } + +/// Deserialize a plugin's state from a vector containing JSON data. This can (and should) be shared +/// across plugin formats. Returns `false` and logs an error if the state could not be deserialized. +/// +/// 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( + state: &[u8], + plugin_params: Arc, + param_by_hash: &HashMap, + param_id_to_hash: &HashMap, + current_buffer_config: Option<&BufferConfig>, +) -> bool { + let state: State = match serde_json::from_slice(state) { + Ok(s) => s, + Err(err) => { + nih_debug_assert_failure!("Error while deserializing state: {}", err); + return false; + } + }; + + deserialize_object( + &state, + plugin_params, + param_by_hash, + param_id_to_hash, + current_buffer_config, + ) +} diff --git a/src/wrapper/vst3/context.rs b/src/wrapper/vst3/context.rs index 31b31517..15cdc8c3 100644 --- a/src/wrapper/vst3/context.rs +++ b/src/wrapper/vst3/context.rs @@ -9,6 +9,7 @@ use crate::context::{GuiContext, ProcessContext, Transport}; use crate::event_loop::EventLoop; use crate::param::internals::ParamPtr; use crate::plugin::{NoteEvent, Vst3Plugin}; +use crate::wrapper::state::State; /// A [`GuiContext`] implementation for the wrapper. This is passed to the plugin in /// [`Editor::spawn()`][crate::prelude::Editor::spawn()] so it can interact with the rest of the plugin and @@ -87,6 +88,14 @@ impl GuiContext for WrapperGuiContext

{ None => nih_debug_assert_failure!("Component handler not yet set"), } } + + fn get_state(&self) -> State { + self.inner.get_state_object() + } + + fn set_state(&self, state: State) { + self.inner.set_state_object(state) + } } impl ProcessContext for WrapperProcessContext<'_, P> { diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index c47af570..960b8897 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -1,13 +1,15 @@ use atomic_refcell::AtomicRefCell; use crossbeam::atomic::AtomicCell; +use crossbeam::channel::{self, SendTimeoutError}; use parking_lot::RwLock; use std::cmp::Reverse; use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use std::mem::MaybeUninit; -use std::sync::atomic::{AtomicBool, AtomicU32}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; +use std::time::Duration; use vst3_sys::base::{kInvalidArgument, kResultOk, tresult}; -use vst3_sys::vst::IComponentHandler; +use vst3_sys::vst::{IComponentHandler, RestartFlags}; use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::param_units::ParamUnits; @@ -19,7 +21,8 @@ use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; use crate::param::internals::{ParamPtr, Params}; use crate::param::ParamFlags; use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, ProcessStatus, Vst3Plugin}; -use crate::wrapper::util::hash_param_id; +use crate::wrapper::state::{self, State}; +use crate::wrapper::util::{hash_param_id, process_wrapper}; /// The actual wrapper bits. We need this as an `Arc` 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 @@ -80,6 +83,17 @@ pub(crate) struct WrapperInner { /// priority queue and the buffer will be processed in small chunks whenever there's a parameter /// change at a new sample index. pub input_param_changes: AtomicRefCell>>, + /// The plugin is able to restore state through a method on the `GuiContext`. To avoid changing + /// parameters mid-processing and running into garbled data if the host also tries to load state + /// at the same time the restoring happens at the end of each processing call. If this zero + /// capacity channel contains state data at that point, then the audio thread will take the + /// state out of the channel, restore the state, and then send it back through the same channel. + /// In other words, the GUI thread acts as a sender and then as a receiver, while the audio + /// thread acts as a receiver and then as a sender. That way deallocation can happen on the GUI + /// thread. All of this happens without any blocking on the audio thread. + pub updated_state_sender: channel::Sender, + /// The receiver belonging to [`new_state_sender`][Self::new_state_sender]. + pub updated_state_receiver: channel::Receiver, /// The keys from `param_map` in a stable order. pub param_hashes: Vec, @@ -134,6 +148,10 @@ impl WrapperInner

{ let plugin = RwLock::new(P::default()); let editor = plugin.read().editor().map(Arc::from); + // This is used to allow the plugin to restore preset data from its editor, see the comment + // on `Self::updated_state_sender` + let (updated_state_sender, updated_state_receiver) = channel::bounded(0); + // This is a mapping from the parameter IDs specified by the plugin to pointers to thsoe // parameters. These pointers are assumed to be safe to dereference as long as // `wrapper.plugin` is alive. The plugin API identifiers these parameters by hashes, which @@ -232,6 +250,8 @@ impl WrapperInner

{ 0 }, )), + updated_state_sender, + updated_state_receiver, param_hashes, param_by_hash, @@ -300,6 +320,92 @@ impl WrapperInner

{ _ => kInvalidArgument, } } + + /// Get the plugin's state object, may be called by the plugin's GUI as part of its own preset + /// management. The wrapper doesn't use these functions and serializes and deserializes directly + /// the JSON in the relevant plugin API methods instead. + pub fn get_state_object(&self) -> State { + unsafe { + state::serialize_object( + self.params.clone(), + &self.param_by_hash, + &self.param_id_to_hash, + ) + } + } + + /// Update the plugin's internal state, called by the plugin itself from the GUI thread. To + /// prevent corrupting data and changing parameters during processing the actual state is only + /// updated at the end of the audio processing cycle. + pub fn set_state_object(&self, mut state: State) { + // Use a loop and timeouts to handle the super rare edge case when this function gets called + // between a process call and the host disabling the plugin + loop { + if self.is_processing.load(Ordering::SeqCst) { + // If the plugin is currently processing audio, then we'll perform the restore + // operation at the end of the audio call. This involves sending the state to the + // audio thread, having the audio thread handle the state restore at the very end of + // the process function, and then sending the state back to this thread so it can be + // deallocated without blocking the audio thread. + match self + .updated_state_sender + .send_timeout(state, Duration::from_secs(1)) + { + Ok(_) => { + // As mentioned above, the state object will be passed back to this thread + // so we can deallocate it without blocking. + let state = self.updated_state_receiver.recv(); + drop(state); + break; + } + Err(SendTimeoutError::Timeout(value)) => { + state = value; + continue; + } + Err(SendTimeoutError::Disconnected(_)) => { + nih_debug_assert_failure!("State update channel got disconnected"); + return; + } + } + } else { + // 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, + self.params.clone(), + &self.param_by_hash, + &self.param_id_to_hash, + self.current_buffer_config.load().as_ref(), + ); + } + + self.notify_param_values_changed(); + let bus_config = self.current_bus_config.load(); + if let Some(buffer_config) = self.current_buffer_config.load() { + let mut plugin = self.plugin.write(); + plugin.initialize( + &bus_config, + &buffer_config, + &mut self.make_process_context(Transport::new(buffer_config.sample_rate)), + ); + process_wrapper(|| plugin.reset()); + } + + break; + } + } + + // After the state has been updated, notify the host about the new parameter values + match &*self.component_handler.borrow() { + Some(component_handler) => { + unsafe { + component_handler.restart_component(RestartFlags::kParamValuesChanged as i32) + }; + } + None => nih_debug_assert_failure!("The host does not support parameters? What?"), + } + } } impl MainThreadExecutor for WrapperInner

{ diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index b63727e7..8f2a9b28 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -20,6 +20,7 @@ use super::view::WrapperView; use crate::context::Transport; use crate::param::ParamFlags; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; +use crate::util::permit_alloc; use crate::wrapper::state; use crate::wrapper::util::{process_wrapper, u16strlcpy}; use crate::wrapper::vst3::inner::ParameterChange; @@ -220,7 +221,7 @@ impl IComponent for Wrapper

{ return kResultFalse; } - let success = state::deserialize( + let success = state::deserialize_json( &read_buffer, self.inner.params.clone(), &self.inner.param_by_hash, @@ -255,7 +256,7 @@ impl IComponent for Wrapper

{ let state = state.upgrade().unwrap(); - let serialized = state::serialize( + let serialized = state::serialize_json( self.inner.params.clone(), &self.inner.param_by_hash, &self.inner.param_id_to_hash, @@ -710,7 +711,7 @@ impl IAudioProcessor for Wrapper

{ let mut block_start = 0; let mut block_end = data.num_samples as usize; let mut event_start_idx = 0; - loop { + let result = loop { // In sample-accurate automation mode we'll handle any parameter changes for the // current sample, and then process the block between the current sample and the // sample containing the next parameter change, if any. All timings also need to be @@ -910,7 +911,42 @@ impl IAudioProcessor for Wrapper

{ } else { block_start = block_end; } + }; + + // After processing audio, we'll check if the editor has sent us updated plugin state. + // We'll restore that here on the audio thread to prevent changing the values during the + // process call and also to prevent inconsistent state when the host also wants to load + // plugin state. + // 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, + self.inner.params.clone(), + &self.inner.param_by_hash, + &self.inner.param_id_to_hash, + self.inner.current_buffer_config.load().as_ref(), + ); + + self.inner.notify_param_values_changed(); + + // TODO: Normally we'd also call initialize after deserializing state, but that's + // not guaranteed to be realtime safe. Should we do it anyways? + let mut plugin = self.inner.plugin.write(); + plugin.reset(); + + // 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) { + nih_debug_assert_failure!( + "Failed to send state object back to GUI thread: {}", + err + ); + }; } + + result }) }