Implement parameter changes for standalone target
Changing parameters from an editor now works just like in the other targets.
This commit is contained in:
parent
9e40ec11fe
commit
19d4b73039
|
@ -379,9 +379,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
"The plugin has duplicate parameter IDs, weird things may happen. \
|
"The plugin has duplicate parameter IDs, weird things may happen. \
|
||||||
Consider using 6 character parameter IDs to avoid collissions.."
|
Consider using 6 character parameter IDs to avoid collissions.."
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
let mut bypass_param_exists = false;
|
let mut bypass_param_exists = false;
|
||||||
for (_, _, ptr, _) in ¶m_id_hashes_ptrs_groups {
|
for (_, _, ptr, _) in ¶m_id_hashes_ptrs_groups {
|
||||||
let flags = unsafe { ptr.flags() };
|
let flags = unsafe { ptr.flags() };
|
||||||
|
|
|
@ -15,10 +15,10 @@ pub trait Backend: 'static + Send + Sync {
|
||||||
fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool);
|
fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uses JACK audio and MIDI.
|
// /// Uses JACK audio and MIDI.
|
||||||
pub struct Jack {
|
// pub struct Jack {
|
||||||
// TODO
|
// // TODO
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// This backend doesn't input or output any audio or MIDI. It only exists so the standalone
|
/// This backend doesn't input or output any audio or MIDI. It only exists so the standalone
|
||||||
/// application can continue to run even when there is no audio backend available. This can be
|
/// application can continue to run even when there is no audio backend available. This can be
|
||||||
|
@ -27,11 +27,12 @@ pub struct Dummy {
|
||||||
config: WrapperConfig,
|
config: WrapperConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for Jack {
|
// TODO: Add a JACK backend
|
||||||
fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool) {
|
// impl Backend for Jack {
|
||||||
todo!()
|
// fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool) {
|
||||||
}
|
// todo!()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
impl Backend for Dummy {
|
impl Backend for Dummy {
|
||||||
fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool) {
|
fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool) {
|
||||||
|
|
|
@ -45,19 +45,15 @@ impl<P: Plugin, B: Backend> GuiContext for WrapperGuiContext<P, B> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// All of these functions are supposed to be called from the main thread, so we'll put some
|
unsafe fn raw_begin_set_parameter(&self, _param: ParamPtr) {
|
||||||
// trust in the caller and assume that this is indeed the case
|
// Since there's no autmoation being recorded here, gestures don't mean anything
|
||||||
unsafe fn raw_begin_set_parameter(&self, param: ParamPtr) {
|
|
||||||
nih_debug_assert_failure!("TODO: WrapperGuiContext::raw_begin_set_parameter()");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32) {
|
unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32) {
|
||||||
nih_debug_assert_failure!("TODO: WrapperGuiContext::raw_set_parameter_normalized()");
|
self.wrapper.set_parameter(param, normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn raw_end_set_parameter(&self, param: ParamPtr) {
|
unsafe fn raw_end_set_parameter(&self, _param: ParamPtr) {}
|
||||||
nih_debug_assert_failure!("TODO: WrapperGuiContext::raw_end_set_parameter()");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_state(&self) -> crate::wrapper::state::PluginState {
|
fn get_state(&self) -> crate::wrapper::state::PluginState {
|
||||||
todo!("WrapperGuiContext::get_state()");
|
todo!("WrapperGuiContext::get_state()");
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use atomic_refcell::AtomicRefCell;
|
use atomic_refcell::AtomicRefCell;
|
||||||
use baseview::{EventStatus, Window, WindowHandler, WindowOpenOptions};
|
use baseview::{EventStatus, Window, WindowHandler, WindowOpenOptions};
|
||||||
|
use crossbeam::queue::ArrayQueue;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use raw_window_handle::HasRawWindowHandle;
|
use raw_window_handle::HasRawWindowHandle;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
@ -10,8 +12,14 @@ use std::thread;
|
||||||
use super::backend::Backend;
|
use super::backend::Backend;
|
||||||
use super::context::{WrapperGuiContext, WrapperProcessContext};
|
use super::context::{WrapperGuiContext, WrapperProcessContext};
|
||||||
use crate::context::Transport;
|
use crate::context::Transport;
|
||||||
|
use crate::param::internals::{ParamPtr, Params};
|
||||||
|
use crate::param::ParamFlags;
|
||||||
use crate::plugin::{BufferConfig, BusConfig, Editor, ParentWindowHandle, Plugin};
|
use crate::plugin::{BufferConfig, BusConfig, Editor, ParentWindowHandle, Plugin};
|
||||||
|
|
||||||
|
/// How many parameter changes we can store in our unprocessed parameter change queue. Storing more
|
||||||
|
/// than this many parmaeters at a time will cause changes to get lost.
|
||||||
|
const EVENT_QUEUE_CAPACITY: usize = 2048;
|
||||||
|
|
||||||
/// Configuration for a standalone plugin that would normally be provided by the DAW.
|
/// Configuration for a standalone plugin that would normally be provided by the DAW.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WrapperConfig {
|
pub struct WrapperConfig {
|
||||||
|
@ -43,6 +51,10 @@ pub struct Wrapper<P: Plugin, B: Backend> {
|
||||||
|
|
||||||
/// The wrapped plugin instance.
|
/// The wrapped plugin instance.
|
||||||
plugin: RwLock<P>,
|
plugin: RwLock<P>,
|
||||||
|
/// The plugin's parameters. These are fetched once during initialization. That way the
|
||||||
|
/// `ParamPtr`s are guaranteed to live at least as long as this object and we can interact with
|
||||||
|
/// the `Params` object without having to acquire a lock on `plugin`.
|
||||||
|
_params: Arc<dyn Params>,
|
||||||
/// The plugin's editor, if it has one. This object does not do anything on its own, but we need
|
/// The plugin's editor, if it has one. This object does not do anything on its own, but we need
|
||||||
/// to instantiate this in advance so we don't need to lock the entire [`Plugin`] object when
|
/// to instantiate this in advance so we don't need to lock the entire [`Plugin`] object when
|
||||||
/// creating an editor.
|
/// creating an editor.
|
||||||
|
@ -53,6 +65,15 @@ pub struct Wrapper<P: Plugin, B: Backend> {
|
||||||
/// The bus and buffer configurations are static for the standalone target.
|
/// The bus and buffer configurations are static for the standalone target.
|
||||||
bus_config: BusConfig,
|
bus_config: BusConfig,
|
||||||
buffer_config: BufferConfig,
|
buffer_config: BufferConfig,
|
||||||
|
|
||||||
|
/// The set of parameter pointers in `params`. This is technically not necessary, but for
|
||||||
|
/// consistency with the plugin wrappers we'll check whether the `ParamPtr` for an incoming
|
||||||
|
/// parameter change actually belongs to a registered parameter.
|
||||||
|
known_parameters: HashSet<ParamPtr>,
|
||||||
|
/// Parameter changes that have been output by the GUI that have not yet been set in the plugin.
|
||||||
|
/// This queue will be flushed at the end of every processing cycle, just like in the plugin
|
||||||
|
/// versions.
|
||||||
|
unprocessed_param_changes: ArrayQueue<(ParamPtr, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that may arise while initializing the wrapped plugins.
|
/// Errors that may arise while initializing the wrapped plugins.
|
||||||
|
@ -98,12 +119,41 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
|
||||||
/// not accept the IO configuration from the wrapper config.
|
/// not accept the IO configuration from the wrapper config.
|
||||||
pub fn new(backend: B, config: WrapperConfig) -> Result<Arc<Self>, WrapperError> {
|
pub fn new(backend: B, config: WrapperConfig) -> Result<Arc<Self>, WrapperError> {
|
||||||
let plugin = P::default();
|
let plugin = P::default();
|
||||||
|
let params = plugin.params();
|
||||||
let editor = plugin.editor().map(Arc::from);
|
let editor = plugin.editor().map(Arc::from);
|
||||||
|
|
||||||
|
// For consistency's sake we'll include the same assertions as the other backends
|
||||||
|
// TODO: Move these common checks to a function instead of repeating them in every wrapper
|
||||||
|
let param_map = params.param_map();
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let param_ids: HashSet<_> = param_map.iter().map(|(id, _, _)| id.clone()).collect();
|
||||||
|
nih_debug_assert_eq!(
|
||||||
|
param_map.len(),
|
||||||
|
param_ids.len(),
|
||||||
|
"The plugin has duplicate parameter IDs, weird things may happen. \
|
||||||
|
Consider using 6 character parameter IDs to avoid collissions.."
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut bypass_param_exists = false;
|
||||||
|
for (_, ptr, _) in ¶m_map {
|
||||||
|
let flags = unsafe { ptr.flags() };
|
||||||
|
let is_bypass = flags.contains(ParamFlags::BYPASS);
|
||||||
|
|
||||||
|
if is_bypass && bypass_param_exists {
|
||||||
|
nih_debug_assert_failure!(
|
||||||
|
"Duplicate bypass parameters found, the host will only use the first one"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bypass_param_exists |= is_bypass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let wrapper = Arc::new(Wrapper {
|
let wrapper = Arc::new(Wrapper {
|
||||||
backend: AtomicRefCell::new(backend),
|
backend: AtomicRefCell::new(backend),
|
||||||
|
|
||||||
plugin: RwLock::new(plugin),
|
plugin: RwLock::new(plugin),
|
||||||
|
_params: params,
|
||||||
editor,
|
editor,
|
||||||
|
|
||||||
bus_config: BusConfig {
|
bus_config: BusConfig {
|
||||||
|
@ -115,6 +165,9 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
|
||||||
max_buffer_size: config.period_size,
|
max_buffer_size: config.period_size,
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
|
|
||||||
|
known_parameters: param_map.into_iter().map(|(_, ptr, _)| ptr).collect(),
|
||||||
|
unprocessed_param_changes: ArrayQueue::new(EVENT_QUEUE_CAPACITY),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Right now the IO configuration is fixed in the standalone target, so if the plugin cannot
|
// Right now the IO configuration is fixed in the standalone target, so if the plugin cannot
|
||||||
|
@ -150,12 +203,37 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
|
||||||
let terminate_audio_thread = terminate_audio_thread.clone();
|
let terminate_audio_thread = terminate_audio_thread.clone();
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
this.backend.borrow_mut().run(move |buffer| {
|
this.clone().backend.borrow_mut().run(move |buffer| {
|
||||||
if terminate_audio_thread.load(Ordering::SeqCst) {
|
if terminate_audio_thread.load(Ordering::SeqCst) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Process incoming events
|
||||||
// TODO: Process audio
|
// TODO: Process audio
|
||||||
|
// TODO: Handle parameter chagnes
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let sample_rate = this.buffer_config.sample_rate;
|
||||||
|
let mut parameter_values_changed = false;
|
||||||
|
while let Some((param_ptr, normalized_value)) =
|
||||||
|
this.unprocessed_param_changes.pop()
|
||||||
|
{
|
||||||
|
unsafe { param_ptr.set_normalized_value(normalized_value) };
|
||||||
|
unsafe { param_ptr.update_smoother(sample_rate, false) };
|
||||||
|
parameter_values_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the editor to react to the new parameter values if the editor uses a reactive data
|
||||||
|
// binding model
|
||||||
|
if parameter_values_changed {
|
||||||
|
if let Some(editor) = &this.editor {
|
||||||
|
editor.param_values_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: MIDI output
|
||||||
|
// TODO: Handle state restore
|
||||||
|
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
@ -216,11 +294,31 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
terminate_audio_thread.store(true, Ordering::SeqCst);
|
terminate_audio_thread.store(true, Ordering::SeqCst);
|
||||||
audio_thread.join();
|
audio_thread.join().unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a parameter based on a `ParamPtr`. The value will be updated at the end of the next
|
||||||
|
/// processing cycle, and this won't do anything if the parameter has not been registered by the
|
||||||
|
/// plugin.
|
||||||
|
///
|
||||||
|
/// This returns false if the parmeter was not set because the `Paramptr` was either unknown or
|
||||||
|
/// the queue is full.
|
||||||
|
pub fn set_parameter(&self, param: ParamPtr, normalized: f32) -> bool {
|
||||||
|
if !self.known_parameters.contains(¶m) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let push_succesful = self
|
||||||
|
.unprocessed_param_changes
|
||||||
|
.push((param, normalized))
|
||||||
|
.is_ok();
|
||||||
|
nih_debug_assert!(push_succesful, "The parmaeter change queue was full");
|
||||||
|
|
||||||
|
push_succesful
|
||||||
|
}
|
||||||
|
|
||||||
fn make_gui_context(
|
fn make_gui_context(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
new_window_size: Arc<Mutex<Option<(u32, u32)>>>,
|
new_window_size: Arc<Mutex<Option<(u32, u32)>>>,
|
||||||
|
|
Loading…
Reference in a new issue