diff --git a/CHANGELOG.md b/CHANGELOG.md index e1447b31..74bf96f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 4ca91bf2..d1261a0c 100644 --- a/README.md +++ b/README.md @@ -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 ` command that automatically detects which plugin targets your plugin exposes and creates the correct diff --git a/src/wrapper/clap/context.rs b/src/wrapper/clap/context.rs index 2e5e87a8..6f95ac28 100644 --- a/src/wrapper/clap/context.rs +++ b/src/wrapper/clap/context.rs @@ -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 { atomic_refcell::AtomicRefCell, } +/// 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, + /// 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, +} + impl Drop for WrapperInitContext<'_, P> { fn drop(&mut self) { if let Some(samples) = self.pending_requests.latency_changed.take() { @@ -225,3 +241,138 @@ impl GuiContext for WrapperGuiContext

{ 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, +} + +/// 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>, +} + +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( + plugin: &P, + pages: &'a mut Vec, + param_ptr_to_hash: &'a HashMap, + ) { + // 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>, + ) { + 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, 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( + §ion_name, + &subpage_name, + subpage_params.iter().copied(), + ); + } + } else { + self.add_clap_page(§ion_name, &page.name, page.params); + } + } + } +} + +impl RemoteControlsSection for Section { + type Page = Page; + + fn add_page(&mut self, name: impl Into, 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); + } +} diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 885757bf..63867a3b 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -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 { host_thread_check: AtomicRefCell>>, + 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_plugin_render: clap_plugin_render, clap_plugin_state: clap_plugin_state, @@ -513,6 +521,14 @@ impl Wrapper

{ } } + // Support for the remote controls extension + let mut remote_control_pages = Vec::new(); + RemoteControlPages::define_remote_control_pages( + &plugin, + &mut remote_control_pages, + ¶m_ptr_to_hash, + ); + let wrapper = Self { this: AtomicRefCell::new(Weak::new()), @@ -626,6 +642,12 @@ impl Wrapper

{ 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 Wrapper

{ &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 Wrapper

{ } } + 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 {