1
0
Fork 0

Allow declaratively defining CLAP remote controls

This commit is contained in:
Robbert van der Helm 2023-04-22 16:59:03 +02:00
parent 841fe2424c
commit 911c0d57d5
4 changed files with 214 additions and 3 deletions

View file

@ -12,6 +12,14 @@ state is to list breaking changes.
## [2023-04-22]
### Added
- CLAP plugins can optionally declare pages of [remote
controls](https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/remote-controls.h)
so DAWs can more automatically map pages of the plugin's parameters to
hardware controllers. This is currently a draft extension, so until the
extension is finalized host support may break at any moment.
### Changed
- The CLAP version has been updated to 1.1.8.

View file

@ -134,7 +134,6 @@ Scroll down for more information on the underlying plugin framework.
- Optional sample accurate automation support for VST3 and CLAP that can be
enabled by setting the `Plugin::SAMPLE_ACCURATE_AUTOMATION` constant to
`true`.
- Support for CLAP's polyphonic modulation on a per-parameter basis.
- Optional support for compressing the human readable JSON state files using
[Zstandard](https://en.wikipedia.org/wiki/Zstd).
- Comes with adapters for popular Rust GUI frameworks as well as some basic
@ -152,6 +151,10 @@ Scroll down for more information on the underlying plugin framework.
byte buffers in the process function.
- Support for flexible dynamic buffer configurations, including variable numbers
of input and output ports.
- First-class support several more exotic CLAP features:
- Both monophonic and polyphonic parameter modulation are supported.
- Plugins can declaratively define pages of remote controls that DAWs can bind
to hardware controllers.
- A plugin bundler accessible through the
`cargo xtask bundle <package> <build_arguments>` command that automatically
detects which plugin targets your plugin exposes and creates the correct

View file

@ -1,14 +1,20 @@
use atomic_refcell::AtomicRefMut;
use clap_sys::ext::draft::remote_controls::{
clap_remote_controls_page, CLAP_REMOTE_CONTROLS_COUNT,
};
use clap_sys::id::{clap_id, CLAP_INVALID_ID};
use clap_sys::string_sizes::CLAP_NAME_SIZE;
use std::cell::Cell;
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use super::wrapper::{OutputParamEvent, Task, Wrapper};
use crate::event_loop::EventLoop;
use crate::prelude::{
ClapPlugin, GuiContext, InitContext, ParamPtr, PluginApi, PluginNoteEvent, ProcessContext,
Transport,
RemoteControlsContext, RemoteControlsPage, RemoteControlsSection, Transport,
};
use crate::wrapper::util::strlcpy;
/// An [`InitContext`] implementation for the wrapper.
///
@ -49,6 +55,16 @@ pub(crate) struct WrapperGuiContext<P: ClapPlugin> {
atomic_refcell::AtomicRefCell<crate::wrapper::util::context_checks::ParamGestureChecker>,
}
/// A [`RemoteControlsContext`] implementation for the wrapper. This is used during initialization
/// to allow the plugin to declare remote control pages. This struct defines the pages in the
/// correct format.
pub(crate) struct RemoteControlPages<'a> {
param_ptr_to_hash: &'a HashMap<ParamPtr, u32>,
/// The remote control pages, as defined by the plugin. These don't reference any heap data so
/// we can store them directly.
pages: &'a mut Vec<clap_remote_controls_page>,
}
impl<P: ClapPlugin> Drop for WrapperInitContext<'_, P> {
fn drop(&mut self) {
if let Some(samples) = self.pending_requests.latency_changed.take() {
@ -225,3 +241,138 @@ impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
self.wrapper.set_state_object_from_gui(state)
}
}
/// A remote control section. The plugin can fill this with information for one or more pages.
pub(crate) struct Section {
pages: Vec<Page>,
}
/// A remote control page. These are automatically split into multiple pages if the number of
/// controls exceeds 8.
pub(crate) struct Page {
name: String,
params: Vec<Option<ParamPtr>>,
}
impl<'a> RemoteControlPages<'a> {
/// Allow the plugin to define remote control pages and add them to `pages`. This does not clear
/// `pages` first.
pub fn define_remote_control_pages<P: ClapPlugin>(
plugin: &P,
pages: &'a mut Vec<clap_remote_controls_page>,
param_ptr_to_hash: &'a HashMap<ParamPtr, u32>,
) {
// The magic happens in the `add_section()` function defined below
plugin.remote_controls(&mut Self {
pages,
param_ptr_to_hash,
});
}
/// Perform the boilerplate needed for creating and adding a new [`clap_remote_controls_page`].
/// If `params` contains more than eight parameters then any further parameters will be lost.
fn add_clap_page(
&mut self,
section: &str,
page_name: &str,
params: impl IntoIterator<Item = Option<ParamPtr>>,
) {
let mut page = clap_remote_controls_page {
section_name: [0; CLAP_NAME_SIZE],
// Pages are numbered sequentially
page_id: self.pages.len() as clap_id,
page_name: [0; CLAP_NAME_SIZE],
param_ids: [CLAP_INVALID_ID; CLAP_REMOTE_CONTROLS_COUNT],
is_for_preset: false,
};
strlcpy(&mut page.section_name, section);
strlcpy(&mut page.page_name, page_name);
let mut params = params.into_iter();
for (param_id, param_ptr) in page.param_ids.iter_mut().zip(&mut params) {
// `param_id` already has the correct value if `param_ptr` is empty/a spacer
if let Some(param_ptr) = param_ptr {
*param_id = self.param_ptr_to_id(param_ptr);
}
}
nih_debug_assert!(
params.next().is_none(),
"More than eight parameters were passed to 'RemoteControlPages::add_page()', this is \
a NIH-plug bug."
);
self.pages.push(page);
}
/// Transform a `ParamPtr` to the associated CLAP parameter ID/hash. Returns -1/invalid
/// parameter and triggers a debug assertion when the parameter is not known.
fn param_ptr_to_id(&self, ptr: ParamPtr) -> clap_id {
match self.param_ptr_to_hash.get(&ptr) {
Some(id) => *id,
None => {
nih_debug_assert_failure!(
"An unknown parameter was added to a remote control page, ignoring..."
);
CLAP_INVALID_ID
}
}
}
}
impl RemoteControlsContext for RemoteControlPages<'_> {
type Section = Section;
fn add_section(&mut self, name: impl Into<String>, f: impl FnOnce(&mut Self::Section)) {
let section_name = name.into();
let mut section = Section {
pages: Vec::with_capacity(1),
};
f(&mut section);
// The pages in the section may need to be split up into multiple pages if it defines more
// than eight parameters. This keeps the interface flexible for potential future expansion
// and makes manual paging unnecessary in some situations.
for page in section.pages {
if page.params.len() > CLAP_REMOTE_CONTROLS_COUNT {
for (subpage_idx, subpage_params) in
page.params.chunks(CLAP_REMOTE_CONTROLS_COUNT).enumerate()
{
let subpage_name = format!("{} {}", page.name, subpage_idx + 1);
self.add_clap_page(
&section_name,
&subpage_name,
subpage_params.iter().copied(),
);
}
} else {
self.add_clap_page(&section_name, &page.name, page.params);
}
}
}
}
impl RemoteControlsSection for Section {
type Page = Page;
fn add_page(&mut self, name: impl Into<String>, f: impl FnOnce(&mut Self::Page)) {
let mut page = Page {
name: name.into(),
params: Vec::with_capacity(CLAP_REMOTE_CONTROLS_COUNT),
};
f(&mut page);
self.pages.push(page);
}
}
impl RemoteControlsPage for Page {
fn add_param(&mut self, param: &impl crate::prelude::Param) {
self.params.push(Some(param.as_ptr()));
}
fn add_spacer(&mut self) {
self.params.push(None);
}
}

View file

@ -22,6 +22,9 @@ use clap_sys::ext::audio_ports::{
use clap_sys::ext::audio_ports_config::{
clap_audio_ports_config, clap_plugin_audio_ports_config, CLAP_EXT_AUDIO_PORTS_CONFIG,
};
use clap_sys::ext::draft::remote_controls::{
clap_plugin_remote_controls, clap_remote_controls_page, CLAP_EXT_REMOTE_CONTROLS,
};
use clap_sys::ext::gui::{
clap_gui_resize_hints, clap_host_gui, clap_plugin_gui, clap_window, CLAP_EXT_GUI,
CLAP_WINDOW_API_COCOA, CLAP_WINDOW_API_WIN32, CLAP_WINDOW_API_X11,
@ -86,6 +89,7 @@ use crate::prelude::{
ProcessMode, ProcessStatus, SysExMessage, TaskExecutor, Transport,
};
use crate::util::permit_alloc;
use crate::wrapper::clap::context::RemoteControlPages;
use crate::wrapper::clap::util::{read_stream, write_stream};
use crate::wrapper::state::{self, PluginState};
use crate::wrapper::util::buffer_management::{BufferManager, ChannelPointers};
@ -222,6 +226,10 @@ pub struct Wrapper<P: ClapPlugin> {
host_thread_check: AtomicRefCell<Option<ClapPtr<clap_host_thread_check>>>,
clap_plugin_remote_controls: clap_plugin_remote_controls,
/// The plugin's remote control pages, if it defines any. Filled when initializing the plugin.
remote_control_pages: Vec<clap_remote_controls_page>,
clap_plugin_render: clap_plugin_render,
clap_plugin_state: clap_plugin_state,
@ -513,6 +521,14 @@ impl<P: ClapPlugin> Wrapper<P> {
}
}
// Support for the remote controls extension
let mut remote_control_pages = Vec::new();
RemoteControlPages::define_remote_control_pages(
&plugin,
&mut remote_control_pages,
&param_ptr_to_hash,
);
let wrapper = Self {
this: AtomicRefCell::new(Weak::new()),
@ -626,6 +642,12 @@ impl<P: ClapPlugin> Wrapper<P> {
host_thread_check: AtomicRefCell::new(None),
clap_plugin_remote_controls: clap_plugin_remote_controls {
count: Some(Self::ext_remote_controls_count),
get: Some(Self::ext_remote_controls_get),
},
remote_control_pages,
clap_plugin_render: clap_plugin_render {
has_hard_realtime_requirement: Some(Self::ext_render_has_hard_realtime_requirement),
set: Some(Self::ext_render_set),
@ -2282,6 +2304,8 @@ impl<P: ClapPlugin> Wrapper<P> {
&wrapper.clap_plugin_note_ports as *const _ as *const c_void
} else if id == CLAP_EXT_PARAMS {
&wrapper.clap_plugin_params as *const _ as *const c_void
} else if id == CLAP_EXT_REMOTE_CONTROLS {
&wrapper.clap_plugin_remote_controls as *const _ as *const c_void
} else if id == CLAP_EXT_RENDER {
&wrapper.clap_plugin_render as *const _ as *const c_void
} else if id == CLAP_EXT_STATE {
@ -2997,6 +3021,31 @@ impl<P: ClapPlugin> Wrapper<P> {
}
}
unsafe extern "C" fn ext_remote_controls_count(plugin: *const clap_plugin) -> u32 {
check_null_ptr!(0, plugin, (*plugin).plugin_data);
let wrapper = &*((*plugin).plugin_data as *const Self);
wrapper.remote_control_pages.len() as u32
}
unsafe extern "C" fn ext_remote_controls_get(
plugin: *const clap_plugin,
page_index: u32,
page: *mut clap_remote_controls_page,
) -> bool {
check_null_ptr!(false, plugin, (*plugin).plugin_data, page);
let wrapper = &*((*plugin).plugin_data as *const Self);
nih_debug_assert!(page_index as usize <= wrapper.remote_control_pages.len());
match wrapper.remote_control_pages.get(page_index as usize) {
Some(p) => {
*page = *p;
true
}
None => false,
}
}
unsafe extern "C" fn ext_render_has_hard_realtime_requirement(
_plugin: *const clap_plugin,
) -> bool {