1
0
Fork 0

Add a CLAP GuiContext for sending param changes

This commit is contained in:
Robbert van der Helm 2022-03-03 17:47:41 +01:00
parent a4930dc887
commit b5993c1bb8
2 changed files with 100 additions and 12 deletions

View file

@ -1,28 +1,84 @@
use parking_lot::RwLockWriteGuard; use parking_lot::RwLockWriteGuard;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::wrapper::{Task, Wrapper}; use super::wrapper::{OutputParamChange, Task, Wrapper};
use crate::context::ProcessContext; use crate::context::ProcessContext;
use crate::event_loop::EventLoop; use crate::event_loop::EventLoop;
use crate::param::internals::ParamPtr;
use crate::plugin::{ClapPlugin, NoteEvent}; use crate::plugin::{ClapPlugin, NoteEvent};
use crate::GuiContext;
/// A [GuiContext] implementation for the wrapper. This is passed to the plugin in
/// [crate::Editor::spawn()] so it can interact with the rest of the plugin and with the host for
/// things like setting parameters.
pub(crate) struct WrapperGuiContext<P: ClapPlugin> {
pub(super) wrapper: Arc<Wrapper<P>>,
}
/// A [ProcessContext] implementation for the wrapper. This is a separate object so it can hold on /// A [ProcessContext] implementation for the wrapper. This is a separate object so it can hold on
/// to lock guards for event queues. Otherwise reading these events would require constant /// to lock guards for event queues. Otherwise reading these events would require constant
/// unnecessary atomic operations to lock the uncontested RwLocks. /// unnecessary atomic operations to lock the uncontested RwLocks.
pub(crate) struct WrapperProcessContext<'a, P: ClapPlugin> { pub(crate) struct WrapperProcessContext<'a, P: ClapPlugin> {
pub(super) plugin: &'a Wrapper<P>, pub(super) wrapper: &'a Wrapper<P>,
pub(super) input_events_guard: RwLockWriteGuard<'a, VecDeque<NoteEvent>>, pub(super) input_events_guard: RwLockWriteGuard<'a, VecDeque<NoteEvent>>,
} }
impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
// All of these functions are supposed to be called from the main thread, so we'll put some
// trust in the caller and assume that this is indeed the case
unsafe fn raw_begin_set_parameter(&self, _param: ParamPtr) {
// TODO: Parameter event gestures are a bit weird in CLAP right now because they're
// implemented as flags on events, and you don't know when a gesture ends before it
// has ended. Implement this once that's a bit clearer.
}
unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32) {
match self.wrapper.param_ptr_to_hash.get(&param) {
Some(hash) => {
// We queue the parameter change event here, and it will be sent to the host either
// at the end of the current processing cycle or after requesting an explicit flush
// (when the plugin isn't processing audio). The parameter's actual value will only
// be changed when the output event is written to prevent changing parameter values
// in the middle of processing audio.
let clap_plain_value = normalized as f64 * param.step_count().unwrap_or(1) as f64;
let success = self.wrapper.queue_parameter_change(OutputParamChange {
param_hash: *hash,
clap_plain_value,
});
nih_debug_assert!(success, "Parameter output queue was full, parameter change will not be sent to the host");
}
None => nih_debug_assert_failure!("Unknown parameter: {:?}", param),
}
}
unsafe fn raw_end_set_parameter(&self, _param: ParamPtr) {
// TODO: Parameter event gestures are a bit weird in CLAP right now because they're
// implemented as flags on events, and you don't know when a gesture ends before it
// has ended. Implement this once that's a bit clearer.
}
unsafe fn raw_default_normalized_param_value(&self, param: ParamPtr) -> f32 {
match self.wrapper.param_ptr_to_hash.get(&param) {
Some(hash) => self.wrapper.param_defaults_normalized[hash],
None => {
nih_debug_assert_failure!("Unknown parameter: {:?}", param);
0.5
}
}
}
}
impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> { impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> {
fn set_latency_samples(&self, samples: u32) { fn set_latency_samples(&self, samples: u32) {
// Only make a callback if it's actually needed // Only make a callback if it's actually needed
// XXX: For CLAP we could move this handling to the Plugin struct, but it may be worthwhile // XXX: For CLAP we could move this handling to the Plugin struct, but it may be worthwhile
// to keep doing it this way to stay consistent with VST3. // to keep doing it this way to stay consistent with VST3.
let old_latency = self.plugin.current_latency.swap(samples, Ordering::SeqCst); let old_latency = self.wrapper.current_latency.swap(samples, Ordering::SeqCst);
if old_latency != samples { if old_latency != samples {
let task_posted = self.plugin.do_maybe_async(Task::LatencyChanged); let task_posted = self.wrapper.do_maybe_async(Task::LatencyChanged);
nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); nih_debug_assert!(task_posted, "The task queue is full, dropping task...");
} }
} }

View file

@ -18,7 +18,7 @@ use clap_sys::ext::audio_ports_config::{
use clap_sys::ext::gui::{clap_plugin_gui, CLAP_EXT_GUI}; use clap_sys::ext::gui::{clap_plugin_gui, CLAP_EXT_GUI};
use clap_sys::ext::latency::{clap_host_latency, clap_plugin_latency, CLAP_EXT_LATENCY}; use clap_sys::ext::latency::{clap_host_latency, clap_plugin_latency, CLAP_EXT_LATENCY};
use clap_sys::ext::params::{ use clap_sys::ext::params::{
clap_param_info, clap_plugin_params, CLAP_EXT_PARAMS, CLAP_PARAM_IS_BYPASS, clap_host_params, clap_param_info, clap_plugin_params, CLAP_EXT_PARAMS, CLAP_PARAM_IS_BYPASS,
CLAP_PARAM_IS_STEPPED, CLAP_PARAM_IS_STEPPED,
}; };
use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE}; use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE};
@ -135,6 +135,7 @@ pub struct Wrapper<P: ClapPlugin> {
host_latency: Option<ClapPtr<clap_host_latency>>, host_latency: Option<ClapPtr<clap_host_latency>>,
clap_plugin_params: clap_plugin_params, clap_plugin_params: clap_plugin_params,
host_params: Option<ClapPtr<clap_host_params>>,
// These fiels are exactly the same as their VST3 wrapper counterparts. // These fiels are exactly the same as their VST3 wrapper counterparts.
// //
/// The keys from `param_map` in a stable order. /// The keys from `param_map` in a stable order.
@ -146,14 +147,14 @@ pub struct Wrapper<P: ClapPlugin> {
/// The default normalized parameter value for every parameter in `param_ids`. We need to store /// The default normalized parameter value for every parameter in `param_ids`. We need to store
/// this in case the host requeries the parmaeter later. This is also indexed by the hash so we /// this in case the host requeries the parmaeter later. This is also indexed by the hash so we
/// can retrieve them later for the UI if needed. /// can retrieve them later for the UI if needed.
param_defaults_normalized: HashMap<u32, f32>, pub param_defaults_normalized: HashMap<u32, f32>,
/// Mappings from string parameter indentifiers to parameter hashes. Useful for debug logging /// Mappings from string parameter indentifiers to parameter hashes. Useful for debug logging
/// and when storing and restoring plugin state. /// and when storing and restoring plugin state.
param_id_to_hash: HashMap<&'static str, u32>, param_id_to_hash: HashMap<&'static str, u32>,
/// The inverse mapping from [Self::param_by_hash]. This is needed to be able to have an /// The inverse mapping from [Self::param_by_hash]. This is needed to be able to have an
/// ergonomic parameter setting API that uses references to the parameters instead of having to /// ergonomic parameter setting API that uses references to the parameters instead of having to
/// add a setter function to the parameter (or even worse, have it be completely untyped). /// add a setter function to the parameter (or even worse, have it be completely untyped).
param_ptr_to_hash: HashMap<ParamPtr, u32>, pub param_ptr_to_hash: HashMap<ParamPtr, u32>,
/// A queue of parameter changes that should be output in either the next process call or in the /// A queue of parameter changes that should be output in either the next process call or in the
/// next parameter flush. /// next parameter flush.
/// ///
@ -198,12 +199,12 @@ pub enum ClapParamUpdate {
/// A parameter change that should be output by the plugin, stored in a queue on the wrapper and /// A parameter change that should be output by the plugin, stored in a queue on the wrapper and
/// written to the host either at the end of the process function or during a flush. /// written to the host either at the end of the process function or during a flush.
struct OutputParamChange { pub struct OutputParamChange {
/// The internal hash for the parameter. /// The internal hash for the parameter.
param_hash: u32, pub param_hash: u32,
/// The 'plain' value as reported to CLAP. This is the normalized value multiplied by /// The 'plain' value as reported to CLAP. This is the normalized value multiplied by
/// [crate::Param::step_size]. /// [crate::Param::step_size].
clap_plain_value: f64, pub clap_plain_value: f64,
} }
/// Because CLAP has this [clap_host::request_host_callback()] function, we don't need to use /// Because CLAP has this [clap_host::request_host_callback()] function, we don't need to use
@ -262,6 +263,8 @@ impl<P: ClapPlugin> Wrapper<P> {
let host_callback = unsafe { ClapPtr::new(host_callback) }; let host_callback = unsafe { ClapPtr::new(host_callback) };
let host_latency = let host_latency =
unsafe { query_host_extension::<clap_host_latency>(&host_callback, CLAP_EXT_LATENCY) }; unsafe { query_host_extension::<clap_host_latency>(&host_callback, CLAP_EXT_LATENCY) };
let host_params =
unsafe { query_host_extension::<clap_host_params>(&host_callback, CLAP_EXT_PARAMS) };
let host_thread_check = unsafe { let host_thread_check = unsafe {
query_host_extension::<clap_host_thread_check>(&host_callback, CLAP_EXT_THREAD_CHECK) query_host_extension::<clap_host_thread_check>(&host_callback, CLAP_EXT_THREAD_CHECK)
}; };
@ -345,6 +348,7 @@ impl<P: ClapPlugin> Wrapper<P> {
text_to_value: Self::ext_params_text_to_value, text_to_value: Self::ext_params_text_to_value,
flush: Self::ext_params_flush, flush: Self::ext_params_flush,
}, },
host_params,
param_hashes: Vec::new(), param_hashes: Vec::new(),
param_by_hash: HashMap::new(), param_by_hash: HashMap::new(),
param_defaults_normalized: HashMap::new(), param_defaults_normalized: HashMap::new(),
@ -441,11 +445,30 @@ impl<P: ClapPlugin> Wrapper<P> {
fn make_process_context(&self) -> WrapperProcessContext<'_, P> { fn make_process_context(&self) -> WrapperProcessContext<'_, P> {
WrapperProcessContext { WrapperProcessContext {
plugin: self, wrapper: self,
input_events_guard: self.input_events.write(), input_events_guard: self.input_events.write(),
} }
} }
/// Queue a parmeter change to be sent to the host at the end of the audio processing cycle, and
/// request a parameter flush from the host if the plugin is not currently processing audio. The
/// parameter's actual value will only be updated at that point so the value won't change in the
/// middle of a processing call.
///
/// Returns `false` if the parameter value queue was full and the update will not be sent to the
/// host (it will still be set on the plugin either way).
pub fn queue_parameter_change(&self, change: OutputParamChange) -> bool {
let result = self.output_parameter_changes.push(change).is_ok();
match &self.host_params {
Some(host_params) if !self.is_processing.load(Ordering::SeqCst) => {
unsafe { (host_params.request_flush)(&*self.host_callback) };
}
_ => nih_debug_assert_failure!("The host does not support parameters? What?"),
}
result
}
/// Convenience function for setting a value for a parameter as triggered by a VST3 parameter /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter
/// update. The same rate is for updating parameter smoothing. /// update. The same rate is for updating parameter smoothing.
/// ///
@ -514,11 +537,20 @@ impl<P: ClapPlugin> Wrapper<P> {
} }
} }
/// Write the unflushed parameter changes to the host's output event queue. /// Write the unflushed parameter changes to the host's output event queue. This will also
/// modify the actual parameter values, since we should only do that while the wrapped plugin is
/// not actually processing audio.
pub unsafe fn handle_out_events(&self, out: &clap_output_events) { pub unsafe fn handle_out_events(&self, out: &clap_output_events) {
// We'll always write these events to the first sample, so even when we add note output we // We'll always write these events to the first sample, so even when we add note output we
// shouldn't have to think about interleaving events here // shouldn't have to think about interleaving events here
let sample_rate = self.current_buffer_config.load().map(|c| c.sample_rate);
while let Some(change) = self.output_parameter_changes.pop() { while let Some(change) = self.output_parameter_changes.pop() {
self.update_plain_value_by_hash(
change.param_hash,
ClapParamUpdate::PlainValueSet(change.clap_plain_value),
sample_rate,
);
let event = clap_event_param_value { let event = clap_event_param_value {
header: clap_event_header { header: clap_event_header {
size: mem::size_of::<clap_event_param_value>() as u32, size: mem::size_of::<clap_event_param_value>() as u32,