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::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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue