Add state saving and restoring through GuiContext
While preventing any possible data races.
This commit is contained in:
parent
7ca855b3fc
commit
d0064f87d6
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -79,6 +79,14 @@ impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
|
|||
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> {
|
||||
|
|
|
@ -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<P: ClapPlugin> {
|
|||
/// 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<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
|
||||
/// this.
|
||||
|
@ -310,6 +323,10 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
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<P: ClapPlugin> Wrapper<P> {
|
|||
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<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 {
|
||||
check_null_ptr!(false, plugin);
|
||||
let wrapper = &*(plugin as *const Self);
|
||||
|
@ -1025,7 +1128,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
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<P: ClapPlugin> Wrapper<P> {
|
|||
} 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<P: ClapPlugin> Wrapper<P> {
|
|||
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<P: ClapPlugin> Wrapper<P> {
|
|||
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,
|
||||
|
|
|
@ -38,13 +38,13 @@ pub struct State {
|
|||
pub fields: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// 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<dyn Params>,
|
||||
param_by_hash: &HashMap<u32, ParamPtr>,
|
||||
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.
|
||||
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<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
|
||||
/// 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<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;
|
||||
}
|
||||
};
|
||||
|
||||
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<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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<P: Vst3Plugin> GuiContext for WrapperGuiContext<P> {
|
|||
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> {
|
||||
|
|
|
@ -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<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
|
||||
|
@ -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
|
||||
/// change at a new sample index.
|
||||
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.
|
||||
pub param_hashes: Vec<u32>,
|
||||
|
@ -134,6 +148,10 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
|||
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<P: Vst3Plugin> WrapperInner<P> {
|
|||
0
|
||||
},
|
||||
)),
|
||||
updated_state_sender,
|
||||
updated_state_receiver,
|
||||
|
||||
param_hashes,
|
||||
param_by_hash,
|
||||
|
@ -300,6 +320,92 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
|||
_ => 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> {
|
||||
|
|
|
@ -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<P: Vst3Plugin> IComponent for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IComponent for Wrapper<P> {
|
|||
|
||||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
} 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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue