Skip to content

Commit

Permalink
Allow declaratively defining CLAP remote controls
Browse files Browse the repository at this point in the history
  • Loading branch information
robbert-vdh committed Apr 22, 2023
1 parent 841fe24 commit 911c0d5
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 3 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
155 changes: 153 additions & 2 deletions src/wrapper/clap/context.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
}
49 changes: 49 additions & 0 deletions src/wrapper/clap/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()),

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 911c0d5

Please sign in to comment.