1
0
Fork 0

Add state saving and restoring through GuiContext

While preventing any possible data races.
This commit is contained in:
Robbert van der Helm 2022-04-07 17:12:08 +02:00
parent 7ca855b3fc
commit d0064f87d6
8 changed files with 373 additions and 34 deletions

View file

@ -3,6 +3,7 @@
use crate::param::internals::ParamPtr; use crate::param::internals::ParamPtr;
use crate::param::Param; use crate::param::Param;
use crate::plugin::NoteEvent; use crate::plugin::NoteEvent;
use crate::wrapper::state::State;
// TODO: ProcessContext for parameter automation and sending events // 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 /// The implementing function still needs to check if `param` actually exists. This function is
/// mostly marked as unsafe for API reasons. /// mostly marked as unsafe for API reasons.
unsafe fn raw_end_set_parameter(&self, param: ParamPtr); 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 /// Information about the plugin's transport. Depending on the plugin API and the host not all

View file

@ -20,7 +20,6 @@ use crate::param::internals::Params;
/// - Sidechain inputs /// - Sidechain inputs
/// - Multiple output busses /// - Multiple output busses
/// - Special handling for offline processing /// - Special handling for offline processing
/// - Outputting parameter changes from the plugin
/// - MIDI CC handling /// - MIDI CC handling
/// - Outputting MIDI events from the process function (you can output parmaeter changes from an /// - Outputting MIDI events from the process function (you can output parmaeter changes from an
/// editor GUI) /// editor GUI)

View file

@ -79,6 +79,14 @@ impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
None => nih_debug_assert_failure!("Unknown parameter: {:?}", param), 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<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> { impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> {

View file

@ -32,7 +32,7 @@ use clap_sys::ext::note_ports::{
use clap_sys::ext::params::{ use clap_sys::ext::params::{
clap_host_params, clap_param_info, clap_plugin_params, CLAP_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_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::state::{clap_plugin_state, CLAP_EXT_STATE};
use clap_sys::ext::tail::{clap_plugin_tail, CLAP_EXT_TAIL}; 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 clap_sys::stream::{clap_istream, clap_ostream};
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use crossbeam::channel::{self, SendTimeoutError};
use crossbeam::queue::ArrayQueue; use crossbeam::queue::ArrayQueue;
use parking_lot::RwLock; use parking_lot::RwLock;
use raw_window_handle::RawWindowHandle; use raw_window_handle::RawWindowHandle;
@ -60,6 +61,7 @@ use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::thread::{self, ThreadId}; use std::thread::{self, ThreadId};
use std::time::Duration;
use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::context::{WrapperGuiContext, WrapperProcessContext};
use super::descriptor::PluginDescriptor; use super::descriptor::PluginDescriptor;
@ -73,7 +75,7 @@ use crate::plugin::{
BufferConfig, BusConfig, ClapPlugin, Editor, NoteEvent, ParentWindowHandle, ProcessStatus, BufferConfig, BusConfig, ClapPlugin, Editor, NoteEvent, ParentWindowHandle, ProcessStatus,
}; };
use crate::util::permit_alloc; 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}; 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 /// How many output parameter changes we can store in our output parameter change queue. Storing
@ -129,6 +131,17 @@ pub struct Wrapper<P: ClapPlugin> {
/// between process calls. This buffer owns the vector, because otherwise it would need to store /// 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. /// a mutable reference to the data contained in this mutex.
pub output_buffer: AtomicRefCell<Buffer<'static>>, pub output_buffer: AtomicRefCell<Buffer<'static>>,
/// 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<State>,
/// The receiver belonging to [`new_state_sender`][Self::new_state_sender].
updated_state_receiver: channel::Receiver<State>,
/// Needs to be boxed because the plugin object is supposed to contain a static reference to /// Needs to be boxed because the plugin object is supposed to contain a static reference to
/// this. /// this.
@ -310,6 +323,10 @@ impl<P: ClapPlugin> Wrapper<P> {
let plugin = RwLock::new(P::default()); let plugin = RwLock::new(P::default());
let editor = plugin.read().editor().map(Arc::from); 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()); let plugin_descriptor = Box::new(PluginDescriptor::default());
// We're not allowed to query any extensions until the init function has been called, so we // We're not allowed to query any extensions until the init function has been called, so we
@ -448,6 +465,8 @@ impl<P: ClapPlugin> Wrapper<P> {
last_process_status: AtomicCell::new(ProcessStatus::Normal), last_process_status: AtomicCell::new(ProcessStatus::Normal),
current_latency: AtomicU32::new(0), current_latency: AtomicU32::new(0),
output_buffer: AtomicRefCell::new(Buffer::default()), output_buffer: AtomicRefCell::new(Buffer::default()),
updated_state_sender,
updated_state_receiver,
plugin_descriptor, plugin_descriptor,
@ -898,6 +917,90 @@ impl<P: ClapPlugin> Wrapper<P> {
} }
} }
/// 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 { unsafe extern "C" fn init(plugin: *const clap_plugin) -> bool {
check_null_ptr!(false, plugin); check_null_ptr!(false, plugin);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
@ -1025,7 +1128,7 @@ impl<P: ClapPlugin> Wrapper<P> {
let mut block_start = 0; let mut block_start = 0;
let mut block_end = process.frames_count as usize; let mut block_end = process.frames_count as usize;
let mut event_start_idx = 0; let mut event_start_idx = 0;
loop { let result = loop {
if !process.in_events.is_null() { if !process.in_events.is_null() {
if P::SAMPLE_ACCURATE_AUTOMATION { if P::SAMPLE_ACCURATE_AUTOMATION {
let split_result = wrapper.handle_in_events_until_next_param_change( let split_result = wrapper.handle_in_events_until_next_param_change(
@ -1226,7 +1329,42 @@ impl<P: ClapPlugin> Wrapper<P> {
} else { } else {
block_start = block_end; 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<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(false, plugin, stream); check_null_ptr!(false, plugin, stream);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
let serialized = state::serialize( let serialized = state::serialize_json(
wrapper.params.clone(), wrapper.params.clone(),
&wrapper.param_by_hash, &wrapper.param_by_hash,
&wrapper.param_id_to_hash, &wrapper.param_id_to_hash,
@ -1978,7 +2116,7 @@ impl<P: ClapPlugin> Wrapper<P> {
nih_debug_assert_eq!(num_bytes_read as u64, length); nih_debug_assert_eq!(num_bytes_read as u64, length);
read_buffer.set_len(length as usize); read_buffer.set_len(length as usize);
let success = state::deserialize( let success = state::deserialize_json(
&read_buffer, &read_buffer,
wrapper.params.clone(), wrapper.params.clone(),
&wrapper.param_by_hash, &wrapper.param_by_hash,

View file

@ -38,13 +38,13 @@ pub struct State {
pub fields: HashMap<String, String>, pub fields: HashMap<String, String>,
} }
/// Serialize a plugin's state to a vector containing JSON data. This can (and should) be shared /// Serialize a plugin's state to a state object. This is separate from [`serialize_json()`] to
/// across plugin formats. /// allow passing the raw object directly to the plugin.
pub(crate) unsafe fn serialize( pub(crate) unsafe fn serialize_object(
plugin_params: Arc<dyn Params>, plugin_params: Arc<dyn Params>,
param_by_hash: &HashMap<u32, ParamPtr>, param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>, param_id_to_hash: &HashMap<String, u32>,
) -> serde_json::Result<Vec<u8>> { ) -> State {
// We'll serialize parmaeter values as a simple `string_param_id: display_value` map. // We'll serialize parmaeter values as a simple `string_param_id: display_value` map.
let params: HashMap<_, _> = param_id_to_hash let params: HashMap<_, _> = param_id_to_hash
.iter() .iter()
@ -78,32 +78,35 @@ pub(crate) unsafe fn serialize(
// storing things like sample data. // storing things like sample data.
let fields = plugin_params.serialize_fields(); let fields = plugin_params.serialize_fields();
let plugin_state = State { params, fields }; State { params, fields }
serde_json::to_vec(&plugin_state)
} }
/// Serialize a plugin's state to a vector containing JSON data. This can (and should) be shared /// 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<dyn Params>,
param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>,
) -> serde_json::Result<Vec<u8>> {
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 /// 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. /// parameter values. The smoothers have already been reset by this function.
pub(crate) unsafe fn deserialize( pub(crate) unsafe fn deserialize_object(
state: &[u8], state: &State,
plugin_params: Arc<dyn Params>, plugin_params: Arc<dyn Params>,
param_by_hash: &HashMap<u32, ParamPtr>, param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>, param_id_to_hash: &HashMap<String, u32>,
current_buffer_config: Option<&BufferConfig>, current_buffer_config: Option<&BufferConfig>,
) -> bool { ) -> 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); 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 let param_ptr = match param_id_to_hash
.get(param_id_str.as_str()) .get(param_id_str.as_str())
.and_then(|hash| param_by_hash.get(hash)) .and_then(|hash| param_by_hash.get(hash))
@ -116,13 +119,13 @@ pub(crate) unsafe fn deserialize(
}; };
match (param_ptr, param_value) { match (param_ptr, param_value) {
(ParamPtr::FloatParam(p), ParamValue::F32(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::IntParam(p), ParamValue::I32(v)) => (**p).set_plain_value(*v),
(ParamPtr::BoolParam(p), ParamValue::Bool(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 // Enums are serialized based on the active variant's index (which may not be the same
// as the discriminator) // as the discriminator)
(ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => { (ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => {
(**p).set_plain_value(variant_idx) (**p).set_plain_value(*variant_idx)
} }
(param_ptr, param_value) => { (param_ptr, param_value) => {
nih_debug_assert_failure!( nih_debug_assert_failure!(
@ -146,3 +149,32 @@ pub(crate) unsafe fn deserialize(
true 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<dyn Params>,
param_by_hash: &HashMap<u32, ParamPtr>,
param_id_to_hash: &HashMap<String, u32>,
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,
)
}

View file

@ -9,6 +9,7 @@ use crate::context::{GuiContext, ProcessContext, Transport};
use crate::event_loop::EventLoop; use crate::event_loop::EventLoop;
use crate::param::internals::ParamPtr; use crate::param::internals::ParamPtr;
use crate::plugin::{NoteEvent, Vst3Plugin}; use crate::plugin::{NoteEvent, Vst3Plugin};
use crate::wrapper::state::State;
/// A [`GuiContext`] implementation for the wrapper. This is passed to the plugin in /// 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 /// [`Editor::spawn()`][crate::prelude::Editor::spawn()] so it can interact with the rest of the plugin and
@ -87,6 +88,14 @@ impl<P: Vst3Plugin> GuiContext for WrapperGuiContext<P> {
None => nih_debug_assert_failure!("Component handler not yet set"), 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<P: Vst3Plugin> ProcessContext for WrapperProcessContext<'_, P> { impl<P: Vst3Plugin> ProcessContext for WrapperProcessContext<'_, P> {

View file

@ -1,13 +1,15 @@
use atomic_refcell::AtomicRefCell; use atomic_refcell::AtomicRefCell;
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use crossbeam::channel::{self, SendTimeoutError};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, AtomicU32}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use vst3_sys::base::{kInvalidArgument, kResultOk, tresult}; 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::context::{WrapperGuiContext, WrapperProcessContext};
use super::param_units::ParamUnits; 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::internals::{ParamPtr, Params};
use crate::param::ParamFlags; use crate::param::ParamFlags;
use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, ProcessStatus, Vst3Plugin}; 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<T>` so we can safely use our event loop API. /// 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 /// 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<P: Vst3Plugin> {
/// priority queue and the buffer will be processed in small chunks whenever there's a parameter /// priority queue and the buffer will be processed in small chunks whenever there's a parameter
/// change at a new sample index. /// change at a new sample index.
pub input_param_changes: AtomicRefCell<BinaryHeap<Reverse<(usize, ParameterChange)>>>, pub input_param_changes: AtomicRefCell<BinaryHeap<Reverse<(usize, ParameterChange)>>>,
/// 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<State>,
/// The receiver belonging to [`new_state_sender`][Self::new_state_sender].
pub updated_state_receiver: channel::Receiver<State>,
/// The keys from `param_map` in a stable order. /// The keys from `param_map` in a stable order.
pub param_hashes: Vec<u32>, pub param_hashes: Vec<u32>,
@ -134,6 +148,10 @@ impl<P: Vst3Plugin> WrapperInner<P> {
let plugin = RwLock::new(P::default()); let plugin = RwLock::new(P::default());
let editor = plugin.read().editor().map(Arc::from); 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 // 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 // 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 // `wrapper.plugin` is alive. The plugin API identifiers these parameters by hashes, which
@ -232,6 +250,8 @@ impl<P: Vst3Plugin> WrapperInner<P> {
0 0
}, },
)), )),
updated_state_sender,
updated_state_receiver,
param_hashes, param_hashes,
param_by_hash, param_by_hash,
@ -300,6 +320,92 @@ impl<P: Vst3Plugin> WrapperInner<P> {
_ => kInvalidArgument, _ => 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<P: Vst3Plugin> MainThreadExecutor<Task> for WrapperInner<P> { impl<P: Vst3Plugin> MainThreadExecutor<Task> for WrapperInner<P> {

View file

@ -20,6 +20,7 @@ use super::view::WrapperView;
use crate::context::Transport; use crate::context::Transport;
use crate::param::ParamFlags; use crate::param::ParamFlags;
use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin};
use crate::util::permit_alloc;
use crate::wrapper::state; use crate::wrapper::state;
use crate::wrapper::util::{process_wrapper, u16strlcpy}; use crate::wrapper::util::{process_wrapper, u16strlcpy};
use crate::wrapper::vst3::inner::ParameterChange; use crate::wrapper::vst3::inner::ParameterChange;
@ -220,7 +221,7 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
return kResultFalse; return kResultFalse;
} }
let success = state::deserialize( let success = state::deserialize_json(
&read_buffer, &read_buffer,
self.inner.params.clone(), self.inner.params.clone(),
&self.inner.param_by_hash, &self.inner.param_by_hash,
@ -255,7 +256,7 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
let state = state.upgrade().unwrap(); let state = state.upgrade().unwrap();
let serialized = state::serialize( let serialized = state::serialize_json(
self.inner.params.clone(), self.inner.params.clone(),
&self.inner.param_by_hash, &self.inner.param_by_hash,
&self.inner.param_id_to_hash, &self.inner.param_id_to_hash,
@ -710,7 +711,7 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
let mut block_start = 0; let mut block_start = 0;
let mut block_end = data.num_samples as usize; let mut block_end = data.num_samples as usize;
let mut event_start_idx = 0; let mut event_start_idx = 0;
loop { let result = loop {
// In sample-accurate automation mode we'll handle any parameter changes for the // 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 // 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 // sample containing the next parameter change, if any. All timings also need to be
@ -910,7 +911,42 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
} else { } else {
block_start = block_end; 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
}) })
} }