use crate::audio::io::AudioIO;
use crate::midi::io::MidiEvent;
use crate::mutex::UnsafeMutex;
#[cfg(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd"
))]
use crate::plugins::paths;
use libloading::Library;
use serde::{Deserialize, Serialize};
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::ffi::{CStr, CString, c_char, c_void};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::{Duration, Instant};
#[derive(Clone, Debug, PartialEq)]
pub struct ClapParameterInfo {
pub id: u32,
pub name: String,
pub module: String,
pub min_value: f64,
pub max_value: f64,
pub default_value: f64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginState {
pub bytes: Vec<u8>,
}
type AudioPortLayout = (Vec<usize>, usize);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClapMidiOutputEvent {
pub port: usize,
pub event: MidiEvent,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ClapTransportInfo {
pub transport_sample: usize,
pub playing: bool,
pub loop_enabled: bool,
pub loop_range_samples: Option<(usize, usize)>,
pub bpm: f64,
pub tsig_num: u16,
pub tsig_denom: u16,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClapGuiInfo {
pub api: String,
pub supports_embedded: bool,
}
#[derive(Clone, Copy, Debug)]
struct PendingParamValue {
param_id: u32,
value: f64,
}
#[derive(Clone, Copy, Debug)]
struct PendingParamGesture {
param_id: u32,
is_begin: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct ClapParamUpdate {
pub param_id: u32,
pub value: f64,
}
#[derive(Clone, Copy, Debug)]
enum PendingParamEvent {
Value {
param_id: u32,
value: f64,
frame: u32,
},
GestureBegin {
param_id: u32,
frame: u32,
},
GestureEnd {
param_id: u32,
frame: u32,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginInfo {
pub name: String,
pub path: String,
pub capabilities: Option<ClapPluginCapabilities>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginCapabilities {
pub has_gui: bool,
pub gui_apis: Vec<String>,
pub supports_embedded: bool,
pub supports_floating: bool,
pub has_params: bool,
pub has_state: bool,
pub audio_inputs: usize,
pub audio_outputs: usize,
pub midi_inputs: usize,
pub midi_outputs: usize,
}
pub struct ClapProcessor {
path: String,
plugin_id: String,
name: String,
sample_rate: f64,
audio_inputs: Vec<Arc<AudioIO>>,
audio_outputs: Vec<Arc<AudioIO>>,
input_port_channels: Vec<usize>,
output_port_channels: Vec<usize>,
midi_input_ports: usize,
midi_output_ports: usize,
main_audio_inputs: usize,
main_audio_outputs: usize,
host_runtime: Arc<HostRuntime>,
plugin_handle: Arc<PluginHandle>,
param_infos: Arc<Vec<ClapParameterInfo>>,
param_values: UnsafeMutex<HashMap<u32, f64>>,
pending_param_events: UnsafeMutex<Vec<PendingParamEvent>>,
pending_param_events_ui: UnsafeMutex<Vec<PendingParamEvent>>,
active_local_gestures: UnsafeMutex<HashSet<u32>>,
bypassed: Arc<AtomicBool>,
}
pub type SharedClapProcessor = Arc<UnsafeMutex<ClapProcessor>>;
impl fmt::Debug for ClapProcessor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClapProcessor")
.field("path", &self.path)
.field("plugin_id", &self.plugin_id)
.field("name", &self.name)
.field("audio_inputs", &self.audio_inputs.len())
.field("audio_outputs", &self.audio_outputs.len())
.field("input_port_channels", &self.input_port_channels)
.field("output_port_channels", &self.output_port_channels)
.field("midi_input_ports", &self.midi_input_ports)
.field("midi_output_ports", &self.midi_output_ports)
.field("main_audio_inputs", &self.main_audio_inputs)
.field("main_audio_outputs", &self.main_audio_outputs)
.finish()
}
}
impl ClapProcessor {
pub fn new(
sample_rate: f64,
buffer_size: usize,
plugin_spec: &str,
input_count: usize,
output_count: usize,
) -> Result<Self, String> {
let _thread_scope = HostThreadScope::enter_main();
let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
let host_runtime = Arc::new(HostRuntime::new()?);
let plugin_handle = Arc::new(PluginHandle::load(
plugin_path,
plugin_id,
host_runtime.clone(),
sample_rate,
buffer_size as u32,
)?);
let (input_layout_opt, output_layout_opt) = plugin_handle.audio_port_channels();
let input_port_channels_opt = input_layout_opt.as_ref().map(|(c, _)| c.clone());
let output_port_channels_opt = output_layout_opt.as_ref().map(|(c, _)| c.clone());
let discovered_inputs = input_layout_opt.as_ref().map(|(c, _)| c.len());
let discovered_outputs = output_layout_opt.as_ref().map(|(c, _)| c.len());
let (discovered_midi_inputs, discovered_midi_outputs) = plugin_handle.note_port_layout();
let resolved_inputs = discovered_inputs.unwrap_or(input_count);
let resolved_outputs = discovered_outputs.unwrap_or(output_count);
let main_audio_inputs = input_layout_opt
.as_ref()
.map(|(_, main)| *main)
.unwrap_or(input_count);
let main_audio_outputs = output_layout_opt
.as_ref()
.map(|(_, main)| *main)
.unwrap_or(output_count);
let audio_inputs = (0..resolved_inputs)
.map(|_| Arc::new(AudioIO::new(buffer_size)))
.collect();
let audio_outputs = (0..resolved_outputs)
.map(|_| Arc::new(AudioIO::new(buffer_size)))
.collect();
let param_infos = Arc::new(plugin_handle.parameter_infos());
let param_values = UnsafeMutex::new(plugin_handle.parameter_values(¶m_infos));
Ok(Self {
path: plugin_spec.to_string(),
plugin_id: plugin_handle.plugin_id().to_string(),
name: plugin_handle.plugin_name().to_string(),
sample_rate,
audio_inputs,
audio_outputs,
input_port_channels: input_port_channels_opt
.unwrap_or_else(|| vec![1; resolved_inputs]),
output_port_channels: output_port_channels_opt
.unwrap_or_else(|| vec![1; resolved_outputs]),
midi_input_ports: discovered_midi_inputs.unwrap_or(0),
midi_output_ports: discovered_midi_outputs.unwrap_or(0),
main_audio_inputs,
main_audio_outputs,
host_runtime,
plugin_handle,
param_infos,
param_values,
pending_param_events: UnsafeMutex::new(Vec::new()),
pending_param_events_ui: UnsafeMutex::new(Vec::new()),
active_local_gestures: UnsafeMutex::new(HashSet::new()),
bypassed: Arc::new(AtomicBool::new(false)),
})
}
pub fn setup_audio_ports(&self) {
for port in &self.audio_inputs {
port.setup();
}
for port in &self.audio_outputs {
port.setup();
}
}
pub fn process_with_audio_io(&self, frames: usize) {
let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
}
pub fn set_bypassed(&self, bypassed: bool) {
self.bypassed.store(bypassed, Ordering::Relaxed);
}
pub fn is_bypassed(&self) -> bool {
self.bypassed.load(Ordering::Relaxed)
}
fn bypass_copy_inputs_to_outputs(&self) {
for (input, output) in self.audio_inputs.iter().zip(self.audio_outputs.iter()) {
let src = input.buffer.lock();
let dst = output.buffer.lock();
dst.fill(0.0);
for (d, s) in dst.iter_mut().zip(src.iter()) {
*d = *s;
}
*output.finished.lock() = true;
}
for output in self.audio_outputs.iter().skip(self.audio_inputs.len()) {
output.buffer.lock().fill(0.0);
*output.finished.lock() = true;
}
}
pub fn process_with_midi(
&self,
frames: usize,
midi_in: &[MidiEvent],
transport: ClapTransportInfo,
) -> Vec<ClapMidiOutputEvent> {
let started = Instant::now();
for port in &self.audio_inputs {
if port.ready() {
port.process();
}
}
if self.bypassed.load(Ordering::Relaxed) {
self.bypass_copy_inputs_to_outputs();
return Vec::new();
}
let (processed, processed_midi) = match self.process_native(frames, midi_in, transport) {
Ok(ok) => ok,
Err(err) => {
tracing::warn!(
"CLAP process failed for '{}' ({}): {}",
self.name,
self.path,
err
);
(false, Vec::new())
}
};
let elapsed = started.elapsed();
if elapsed > Duration::from_millis(20) {
tracing::warn!(
"Slow CLAP process '{}' ({}) took {:.3} ms for {} frames",
self.name,
self.path,
elapsed.as_secs_f64() * 1000.0,
frames
);
}
if !processed {
for out in &self.audio_outputs {
let out_buf = out.buffer.lock();
out_buf.fill(0.0);
*out.finished.lock() = true;
}
}
processed_midi
}
pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
self.param_infos.as_ref().clone()
}
pub fn parameter_values(&self) -> HashMap<u32, f64> {
self.param_values.lock().clone()
}
pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
self.set_parameter_at(param_id, value, 0)
}
pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
let Some(info) = self.param_infos.iter().find(|p| p.id == param_id) else {
return Err(format!("Unknown CLAP parameter id: {param_id}"));
};
let clamped = value.clamp(info.min_value, info.max_value);
if self.is_parameter_edit_active(param_id) {
self.param_values.lock().insert(param_id, clamped);
return Ok(());
}
self.pending_param_events
.lock()
.push(PendingParamEvent::Value {
param_id,
value: clamped,
frame,
});
self.pending_param_events_ui
.lock()
.push(PendingParamEvent::Value {
param_id,
value: clamped,
frame,
});
self.param_values.lock().insert(param_id, clamped);
Ok(())
}
pub fn begin_parameter_edit(&self, param_id: u32) -> Result<(), String> {
self.begin_parameter_edit_at(param_id, 0)
}
pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
if !self.param_infos.iter().any(|p| p.id == param_id) {
return Err(format!("Unknown CLAP parameter id: {param_id}"));
}
self.pending_param_events
.lock()
.push(PendingParamEvent::GestureBegin { param_id, frame });
self.pending_param_events_ui
.lock()
.push(PendingParamEvent::GestureBegin { param_id, frame });
self.active_local_gestures.lock().insert(param_id);
Ok(())
}
pub fn end_parameter_edit(&self, param_id: u32) -> Result<(), String> {
self.end_parameter_edit_at(param_id, 0)
}
pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
if !self.param_infos.iter().any(|p| p.id == param_id) {
return Err(format!("Unknown CLAP parameter id: {param_id}"));
}
self.pending_param_events
.lock()
.push(PendingParamEvent::GestureEnd { param_id, frame });
self.pending_param_events_ui
.lock()
.push(PendingParamEvent::GestureEnd { param_id, frame });
self.active_local_gestures.lock().remove(¶m_id);
Ok(())
}
pub fn is_parameter_edit_active(&self, param_id: u32) -> bool {
self.active_local_gestures.lock().contains(¶m_id)
}
pub fn snapshot_state(&self) -> Result<ClapPluginState, String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.snapshot_state()
}
pub fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.restore_state(state)
}
pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
&self.audio_inputs
}
pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
&self.audio_outputs
}
pub fn main_audio_input_count(&self) -> usize {
self.main_audio_inputs
}
pub fn main_audio_output_count(&self) -> usize {
self.main_audio_outputs
}
pub fn midi_input_count(&self) -> usize {
self.midi_input_ports
}
pub fn midi_output_count(&self) -> usize {
self.midi_output_ports
}
pub fn path(&self) -> &str {
&self.path
}
pub fn plugin_id(&self) -> &str {
&self.plugin_id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn ui_begin_session(&self) {
self.host_runtime.begin_ui_session();
}
pub fn ui_end_session(&self) {
self.host_runtime.end_ui_session();
}
pub fn ui_should_close(&self) -> bool {
self.host_runtime.ui_should_close()
}
pub fn ui_take_due_timers(&self) -> Vec<u32> {
self.host_runtime.ui_take_due_timers()
}
pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
let pending_ui_events = std::mem::take(self.pending_param_events_ui.lock());
if pending_ui_events.is_empty() && !self.host_runtime.ui_take_param_flush_requested() {
return Vec::new();
}
let _thread_scope = HostThreadScope::enter_main();
let (updates, gestures) = self.plugin_handle.flush_params(&pending_ui_events);
for gesture in gestures {
if gesture.is_begin {
self.active_local_gestures.lock().insert(gesture.param_id);
} else {
self.active_local_gestures.lock().remove(&gesture.param_id);
}
}
if updates.is_empty() {
return Vec::new();
}
let values = &mut *self.param_values.lock();
let mut out = Vec::with_capacity(updates.len());
for update in updates {
values.insert(update.param_id, update.value);
out.push(ClapParamUpdate {
param_id: update.param_id,
value: update.value,
});
}
out
}
pub fn ui_take_state_update(&self) -> Option<ClapPluginState> {
if !self.host_runtime.ui_take_state_dirty_requested() {
return None;
}
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.snapshot_state().ok()
}
pub fn gui_info(&self) -> Result<ClapGuiInfo, String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_info()
}
pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_create(api, is_floating)
}
pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_get_size()
}
pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_set_parent_x11(window)
}
pub fn gui_show(&self) -> Result<(), String> {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_show()
}
pub fn gui_hide(&self) {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_hide();
}
pub fn gui_destroy(&self) {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_destroy();
}
pub fn gui_on_main_thread(&self) {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.on_main_thread();
}
pub fn gui_on_timer(&self, timer_id: u32) {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.gui_on_timer(timer_id);
}
pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
self.plugin_handle.get_note_names()
}
pub fn run_host_callbacks_main_thread(&self) {
let host_flags = self.host_runtime.take_callback_flags();
if host_flags.restart {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.reset();
}
if host_flags.callback {
let _thread_scope = HostThreadScope::enter_main();
self.plugin_handle.on_main_thread();
}
if host_flags.process {
}
}
fn process_native(
&self,
frames: usize,
midi_in: &[MidiEvent],
transport: ClapTransportInfo,
) -> Result<(bool, Vec<ClapMidiOutputEvent>), String> {
if frames == 0 {
return Ok((true, Vec::new()));
}
let mut in_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_inputs.len());
let mut out_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_outputs.len());
let mut in_channel_scratch: Vec<Vec<f32>> = Vec::new();
let mut out_channel_scratch: Vec<Vec<f32>> = Vec::new();
let mut out_channel_scratch_ranges: Vec<(usize, usize)> =
Vec::with_capacity(self.audio_outputs.len());
let mut in_buffers = Vec::with_capacity(self.audio_inputs.len());
let mut out_buffers = Vec::with_capacity(self.audio_outputs.len());
for (port_idx, input) in self.audio_inputs.iter().enumerate() {
let buf = input.buffer.lock();
let channel_count = self
.input_port_channels
.get(port_idx)
.copied()
.unwrap_or(1)
.max(1);
let mut ptrs = Vec::with_capacity(channel_count);
ptrs.push(buf.as_ptr() as *mut f32);
for _ in 1..channel_count {
in_channel_scratch.push(buf.to_vec());
let idx = in_channel_scratch.len().saturating_sub(1);
ptrs.push(in_channel_scratch[idx].as_mut_ptr());
}
in_channel_ptrs.push(ptrs);
in_buffers.push(buf);
}
for (port_idx, output) in self.audio_outputs.iter().enumerate() {
let buf = output.buffer.lock();
let channel_count = self
.output_port_channels
.get(port_idx)
.copied()
.unwrap_or(1)
.max(1);
let mut ptrs = Vec::with_capacity(channel_count);
ptrs.push(buf.as_mut_ptr());
let scratch_start = out_channel_scratch.len();
for _ in 1..channel_count {
out_channel_scratch.push(vec![0.0; frames]);
let idx = out_channel_scratch.len().saturating_sub(1);
ptrs.push(out_channel_scratch[idx].as_mut_ptr());
}
let scratch_end = out_channel_scratch.len();
out_channel_scratch_ranges.push((scratch_start, scratch_end));
out_channel_ptrs.push(ptrs);
out_buffers.push(buf);
}
let mut in_audio = Vec::with_capacity(self.audio_inputs.len());
let mut out_audio = Vec::with_capacity(self.audio_outputs.len());
for ptrs in &mut in_channel_ptrs {
in_audio.push(ClapAudioBuffer {
data32: ptrs.as_mut_ptr(),
data64: std::ptr::null_mut(),
channel_count: ptrs.len() as u32,
latency: 0,
constant_mask: 0,
});
}
for ptrs in &mut out_channel_ptrs {
out_audio.push(ClapAudioBuffer {
data32: ptrs.as_mut_ptr(),
data64: std::ptr::null_mut(),
channel_count: ptrs.len() as u32,
latency: 0,
constant_mask: 0,
});
}
let pending_params = std::mem::take(self.pending_param_events.lock());
let (in_events, in_ctx) = input_events_from(
midi_in,
&pending_params,
self.sample_rate,
transport,
self.midi_input_ports > 0,
);
let out_cap = midi_in
.len()
.saturating_add(self.midi_output_ports.saturating_mul(64));
let (mut out_events, mut out_ctx) = output_events_ctx(out_cap);
let mut process = ClapProcess {
steady_time: -1,
frames_count: frames as u32,
transport: std::ptr::null(),
audio_inputs: in_audio.as_mut_ptr(),
audio_outputs: out_audio.as_mut_ptr(),
audio_inputs_count: in_audio.len() as u32,
audio_outputs_count: out_audio.len() as u32,
in_events: &in_events,
out_events: &mut out_events,
};
let _thread_scope = HostThreadScope::enter_audio();
let result = self.plugin_handle.process(&mut process);
drop(in_ctx);
for output in &self.audio_outputs {
*output.finished.lock() = true;
}
let processed = result?;
if processed {
for (port_idx, out_buf) in out_buffers.iter_mut().enumerate() {
let Some((scratch_start, scratch_end)) = out_channel_scratch_ranges.get(port_idx)
else {
continue;
};
let scratch_count = scratch_end.saturating_sub(*scratch_start);
if scratch_count == 0 {
continue;
}
let ch_count = scratch_count + 1;
for scratch in &out_channel_scratch[*scratch_start..*scratch_end] {
let len = frames.min(scratch.len()).min(out_buf.len());
crate::simd::add_inplace(&mut out_buf[..len], &scratch[..len]);
}
let inv = 1.0_f32 / ch_count as f32;
crate::simd::mul_inplace(&mut out_buf[..frames], inv);
}
for update in &out_ctx.param_values {
self.param_values
.lock()
.insert(update.param_id, update.value);
}
for gesture in &out_ctx.param_gestures {
if gesture.is_begin {
self.active_local_gestures.lock().insert(gesture.param_id);
} else {
self.active_local_gestures.lock().remove(&gesture.param_id);
}
}
Ok((true, std::mem::take(&mut out_ctx.midi_events)))
} else {
Ok((false, Vec::new()))
}
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct ClapVersion {
major: u32,
minor: u32,
revision: u32,
}
const CLAP_VERSION: ClapVersion = ClapVersion {
major: 1,
minor: 2,
revision: 0,
};
#[repr(C)]
struct ClapHost {
clap_version: ClapVersion,
host_data: *mut c_void,
name: *const c_char,
vendor: *const c_char,
url: *const c_char,
version: *const c_char,
get_extension: Option<unsafe extern "C" fn(*const ClapHost, *const c_char) -> *const c_void>,
request_restart: Option<unsafe extern "C" fn(*const ClapHost)>,
request_process: Option<unsafe extern "C" fn(*const ClapHost)>,
request_callback: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapPluginEntry {
clap_version: ClapVersion,
init: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
deinit: Option<unsafe extern "C" fn()>,
get_factory: Option<unsafe extern "C" fn(*const c_char) -> *const c_void>,
}
#[repr(C)]
struct ClapPluginFactory {
get_plugin_count: Option<unsafe extern "C" fn(*const ClapPluginFactory) -> u32>,
get_plugin_descriptor:
Option<unsafe extern "C" fn(*const ClapPluginFactory, u32) -> *const ClapPluginDescriptor>,
create_plugin: Option<
unsafe extern "C" fn(
*const ClapPluginFactory,
*const ClapHost,
*const c_char,
) -> *const ClapPlugin,
>,
}
#[repr(C)]
struct ClapPluginDescriptor {
clap_version: ClapVersion,
id: *const c_char,
name: *const c_char,
vendor: *const c_char,
url: *const c_char,
manual_url: *const c_char,
support_url: *const c_char,
version: *const c_char,
description: *const c_char,
features: *const *const c_char,
}
#[repr(C)]
struct ClapPlugin {
desc: *const ClapPluginDescriptor,
plugin_data: *mut c_void,
init: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
activate: Option<unsafe extern "C" fn(*const ClapPlugin, f64, u32, u32) -> bool>,
deactivate: Option<unsafe extern "C" fn(*const ClapPlugin)>,
start_processing: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
stop_processing: Option<unsafe extern "C" fn(*const ClapPlugin)>,
reset: Option<unsafe extern "C" fn(*const ClapPlugin)>,
process: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapProcess) -> i32>,
get_extension: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char) -> *const c_void>,
on_main_thread: Option<unsafe extern "C" fn(*const ClapPlugin)>,
}
#[repr(C)]
struct ClapInputEvents {
ctx: *const c_void,
size: Option<unsafe extern "C" fn(*const ClapInputEvents) -> u32>,
get: Option<unsafe extern "C" fn(*const ClapInputEvents, u32) -> *const ClapEventHeader>,
}
#[repr(C)]
struct ClapOutputEvents {
ctx: *mut c_void,
try_push: Option<unsafe extern "C" fn(*const ClapOutputEvents, *const ClapEventHeader) -> bool>,
}
#[repr(C)]
struct ClapEventHeader {
size: u32,
time: u32,
space_id: u16,
type_: u16,
flags: u32,
}
const CLAP_CORE_EVENT_SPACE_ID: u16 = 0;
const CLAP_EVENT_NOTE_ON: u16 = 0;
const CLAP_EVENT_NOTE_OFF: u16 = 1;
const CLAP_EVENT_MIDI: u16 = 10;
const CLAP_EVENT_PARAM_VALUE: u16 = 5;
const CLAP_EVENT_PARAM_GESTURE_BEGIN: u16 = 6;
const CLAP_EVENT_PARAM_GESTURE_END: u16 = 7;
const CLAP_EVENT_IS_LIVE: u32 = 1 << 0;
const CLAP_EVENT_TRANSPORT: u16 = 9;
const CLAP_TRANSPORT_HAS_TEMPO: u32 = 1 << 0;
const CLAP_TRANSPORT_HAS_BEATS_TIMELINE: u32 = 1 << 1;
const CLAP_TRANSPORT_HAS_SECONDS_TIMELINE: u32 = 1 << 2;
const CLAP_TRANSPORT_HAS_TIME_SIGNATURE: u32 = 1 << 3;
const CLAP_TRANSPORT_IS_PLAYING: u32 = 1 << 4;
const CLAP_TRANSPORT_IS_LOOP_ACTIVE: u32 = 1 << 6;
const CLAP_BEATTIME_FACTOR: i64 = 1_i64 << 31;
const CLAP_SECTIME_FACTOR: i64 = 1_i64 << 31;
#[repr(C)]
struct ClapEventMidi {
header: ClapEventHeader,
port_index: u16,
data: [u8; 3],
}
#[repr(C)]
struct ClapEventNote {
header: ClapEventHeader,
note_id: i32,
port_index: i16,
channel: i16,
key: i16,
velocity: f64,
}
#[repr(C)]
struct ClapEventParamValue {
header: ClapEventHeader,
param_id: u32,
cookie: *mut c_void,
note_id: i32,
port_index: i16,
channel: i16,
key: i16,
value: f64,
}
#[repr(C)]
struct ClapEventParamGesture {
header: ClapEventHeader,
param_id: u32,
}
#[repr(C)]
struct ClapEventTransport {
header: ClapEventHeader,
flags: u32,
song_pos_beats: i64,
song_pos_seconds: i64,
tempo: f64,
tempo_inc: f64,
loop_start_beats: i64,
loop_end_beats: i64,
loop_start_seconds: i64,
loop_end_seconds: i64,
bar_start: i64,
bar_number: i32,
tsig_num: u16,
tsig_denom: u16,
}
#[repr(C)]
struct ClapParamInfoRaw {
id: u32,
flags: u32,
cookie: *mut c_void,
name: [c_char; 256],
module: [c_char; 1024],
min_value: f64,
max_value: f64,
default_value: f64,
}
#[repr(C)]
struct ClapPluginParams {
count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
get_info: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapParamInfoRaw) -> bool>,
get_value: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut f64) -> bool>,
value_to_text:
Option<unsafe extern "C" fn(*const ClapPlugin, u32, f64, *mut c_char, u32) -> bool>,
text_to_value:
Option<unsafe extern "C" fn(*const ClapPlugin, u32, *const c_char, *mut f64) -> bool>,
flush: Option<
unsafe extern "C" fn(*const ClapPlugin, *const ClapInputEvents, *const ClapOutputEvents),
>,
}
#[repr(C)]
struct ClapPluginStateExt {
save: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapOStream) -> bool>,
load: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapIStream) -> bool>,
}
#[repr(C)]
struct ClapAudioPortInfoRaw {
id: u32,
name: [c_char; 256],
flags: u32,
channel_count: u32,
port_type: *const c_char,
in_place_pair: u32,
}
#[repr(C)]
struct ClapPluginAudioPorts {
count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
get: Option<
unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapAudioPortInfoRaw) -> bool,
>,
}
#[repr(C)]
struct ClapNotePortInfoRaw {
id: u16,
supported_dialects: u32,
preferred_dialect: u32,
name: [c_char; 256],
}
#[repr(C)]
struct ClapPluginNotePorts {
count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
get: Option<
unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapNotePortInfoRaw) -> bool,
>,
}
#[repr(C)]
struct ClapPluginGui {
is_api_supported: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
get_preferred_api:
Option<unsafe extern "C" fn(*const ClapPlugin, *mut *const c_char, *mut bool) -> bool>,
create: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
set_scale: Option<unsafe extern "C" fn(*const ClapPlugin, f64) -> bool>,
get_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
can_resize: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
get_resize_hints: Option<unsafe extern "C" fn(*const ClapPlugin, *mut c_void) -> bool>,
adjust_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
set_size: Option<unsafe extern "C" fn(*const ClapPlugin, u32, u32) -> bool>,
set_parent: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
set_transient: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
suggest_title: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char)>,
show: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
hide: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
}
#[repr(C)]
union ClapWindowHandle {
x11: usize,
native: *mut c_void,
cocoa: *mut c_void,
}
#[repr(C)]
struct ClapWindow {
api: *const c_char,
handle: ClapWindowHandle,
}
#[repr(C)]
struct ClapPluginTimerSupport {
on_timer: Option<unsafe extern "C" fn(*const ClapPlugin, u32)>,
}
#[repr(C)]
struct ClapHostThreadCheck {
is_main_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
is_audio_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
}
#[repr(C)]
struct ClapHostLatency {
changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapHostTail {
changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapHostTimerSupport {
register_timer: Option<unsafe extern "C" fn(*const ClapHost, u32, *mut u32) -> bool>,
unregister_timer: Option<unsafe extern "C" fn(*const ClapHost, u32) -> bool>,
}
#[repr(C)]
struct ClapHostGui {
resize_hints_changed: Option<unsafe extern "C" fn(*const ClapHost)>,
request_resize: Option<unsafe extern "C" fn(*const ClapHost, u32, u32) -> bool>,
request_show: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
request_hide: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
closed: Option<unsafe extern "C" fn(*const ClapHost, bool)>,
}
#[repr(C)]
struct ClapHostParams {
rescan: Option<unsafe extern "C" fn(*const ClapHost, u32)>,
clear: Option<unsafe extern "C" fn(*const ClapHost, u32, u32)>,
request_flush: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapHostState {
mark_dirty: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapNoteName {
name: [c_char; 256],
port: i16,
key: i16,
channel: i16,
}
#[repr(C)]
struct ClapPluginNoteName {
count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
get: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapNoteName) -> bool>,
}
#[repr(C)]
struct ClapHostNoteName {
changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}
#[repr(C)]
struct ClapOStream {
ctx: *mut c_void,
write: Option<unsafe extern "C" fn(*const ClapOStream, *const c_void, u64) -> i64>,
}
#[repr(C)]
struct ClapIStream {
ctx: *mut c_void,
read: Option<unsafe extern "C" fn(*const ClapIStream, *mut c_void, u64) -> i64>,
}
#[repr(C)]
struct ClapAudioBuffer {
data32: *mut *mut f32,
data64: *mut *mut f64,
channel_count: u32,
latency: u32,
constant_mask: u64,
}
#[repr(C)]
struct ClapProcess {
steady_time: i64,
frames_count: u32,
transport: *const c_void,
audio_inputs: *mut ClapAudioBuffer,
audio_outputs: *mut ClapAudioBuffer,
audio_inputs_count: u32,
audio_outputs_count: u32,
in_events: *const ClapInputEvents,
out_events: *mut ClapOutputEvents,
}
enum ClapInputEvent {
Note(ClapEventNote),
Midi(ClapEventMidi),
ParamValue(ClapEventParamValue),
ParamGesture(ClapEventParamGesture),
Transport(ClapEventTransport),
}
impl UnsafeMutex<ClapProcessor> {
pub fn setup_audio_ports(&self) {
self.lock().setup_audio_ports();
}
pub fn process_with_midi(
&self,
frames: usize,
midi_events: &[MidiEvent],
transport: ClapTransportInfo,
) -> Vec<ClapMidiOutputEvent> {
self.lock()
.process_with_midi(frames, midi_events, transport)
}
pub fn set_bypassed(&self, bypassed: bool) {
self.lock().set_bypassed(bypassed);
}
pub fn is_bypassed(&self) -> bool {
self.lock().is_bypassed()
}
pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
self.lock().parameter_infos()
}
pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
self.lock().set_parameter(param_id, value)
}
pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
self.lock().set_parameter_at(param_id, value, frame)
}
pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
self.lock().begin_parameter_edit_at(param_id, frame)
}
pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
self.lock().end_parameter_edit_at(param_id, frame)
}
pub fn snapshot_state(&self) -> Result<ClapPluginState, String> {
self.lock().snapshot_state()
}
pub fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
self.lock().restore_state(state)
}
pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
self.lock().audio_inputs()
}
pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
self.lock().audio_outputs()
}
pub fn main_audio_input_count(&self) -> usize {
self.lock().main_audio_input_count()
}
pub fn main_audio_output_count(&self) -> usize {
self.lock().main_audio_output_count()
}
pub fn midi_input_count(&self) -> usize {
self.lock().midi_input_count()
}
pub fn midi_output_count(&self) -> usize {
self.lock().midi_output_count()
}
pub fn path(&self) -> String {
self.lock().path().to_string()
}
pub fn plugin_id(&self) -> String {
self.lock().plugin_id().to_string()
}
pub fn name(&self) -> String {
self.lock().name().to_string()
}
pub fn run_host_callbacks_main_thread(&self) {
self.lock().run_host_callbacks_main_thread();
}
pub fn ui_begin_session(&self) {
self.lock().ui_begin_session();
}
pub fn ui_end_session(&self) {
self.lock().ui_end_session();
}
pub fn ui_should_close(&self) -> bool {
self.lock().ui_should_close()
}
pub fn ui_take_due_timers(&self) -> Vec<u32> {
self.lock().ui_take_due_timers()
}
pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
self.lock().ui_take_param_updates()
}
pub fn ui_take_state_update(&self) -> Option<ClapPluginState> {
self.lock().ui_take_state_update()
}
pub fn gui_info(&self) -> Result<ClapGuiInfo, String> {
self.lock().gui_info()
}
pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
self.lock().gui_create(api, is_floating)
}
pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
self.lock().gui_get_size()
}
pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
self.lock().gui_set_parent_x11(window)
}
pub fn gui_show(&self) -> Result<(), String> {
self.lock().gui_show()
}
pub fn gui_hide(&self) {
self.lock().gui_hide();
}
pub fn gui_destroy(&self) {
self.lock().gui_destroy();
}
pub fn gui_on_main_thread(&self) {
self.lock().gui_on_main_thread();
}
pub fn gui_on_timer(&self, timer_id: u32) {
self.lock().gui_on_timer(timer_id);
}
pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
self.lock().note_names()
}
}
impl ClapInputEvent {
fn header_ptr(&self) -> *const ClapEventHeader {
match self {
Self::Note(e) => &e.header as *const ClapEventHeader,
Self::Midi(e) => &e.header as *const ClapEventHeader,
Self::ParamValue(e) => &e.header as *const ClapEventHeader,
Self::ParamGesture(e) => &e.header as *const ClapEventHeader,
Self::Transport(e) => &e.header as *const ClapEventHeader,
}
}
}
struct ClapInputEventsCtx {
events: Vec<ClapInputEvent>,
}
struct ClapOutputEventsCtx {
midi_events: Vec<ClapMidiOutputEvent>,
param_values: Vec<PendingParamValue>,
param_gestures: Vec<PendingParamGesture>,
}
struct ClapIStreamCtx<'a> {
bytes: &'a [u8],
offset: usize,
}
#[derive(Default, Clone, Copy)]
struct HostCallbackFlags {
restart: bool,
process: bool,
callback: bool,
}
#[derive(Clone, Copy)]
struct HostTimer {
id: u32,
period: Duration,
next_tick: Instant,
}
struct HostRuntimeState {
callback_flags: UnsafeMutex<HostCallbackFlags>,
timers: UnsafeMutex<Vec<HostTimer>>,
ui_should_close: AtomicU32,
ui_active: AtomicU32,
param_flush_requested: AtomicU32,
state_dirty_requested: AtomicU32,
note_names_dirty: AtomicU32,
}
thread_local! {
static CLAP_HOST_MAIN_THREAD: Cell<bool> = const { Cell::new(true) };
static CLAP_HOST_AUDIO_THREAD: Cell<bool> = const { Cell::new(false) };
}
struct HostThreadScope {
main: bool,
prev: bool,
}
impl HostThreadScope {
fn enter_main() -> Self {
let prev = CLAP_HOST_MAIN_THREAD.with(|flag| {
let prev = flag.get();
flag.set(true);
prev
});
Self { main: true, prev }
}
fn enter_audio() -> Self {
let prev = CLAP_HOST_AUDIO_THREAD.with(|flag| {
let prev = flag.get();
flag.set(true);
prev
});
Self { main: false, prev }
}
}
impl Drop for HostThreadScope {
fn drop(&mut self) {
if self.main {
CLAP_HOST_MAIN_THREAD.with(|flag| flag.set(self.prev));
} else {
CLAP_HOST_AUDIO_THREAD.with(|flag| flag.set(self.prev));
}
}
}
struct HostRuntime {
state: Box<HostRuntimeState>,
host: ClapHost,
}
impl HostRuntime {
fn new() -> Result<Self, String> {
let mut state = Box::new(HostRuntimeState {
callback_flags: UnsafeMutex::new(HostCallbackFlags::default()),
timers: UnsafeMutex::new(Vec::new()),
ui_should_close: AtomicU32::new(0),
ui_active: AtomicU32::new(0),
param_flush_requested: AtomicU32::new(0),
state_dirty_requested: AtomicU32::new(0),
note_names_dirty: AtomicU32::new(0),
});
let host = ClapHost {
clap_version: CLAP_VERSION,
host_data: (&mut *state as *mut HostRuntimeState).cast::<c_void>(),
name: c"Maolan".as_ptr(),
vendor: c"Maolan".as_ptr(),
url: c"https://example.invalid".as_ptr(),
version: c"0.0.1".as_ptr(),
get_extension: Some(host_get_extension),
request_restart: Some(host_request_restart),
request_process: Some(host_request_process),
request_callback: Some(host_request_callback),
};
Ok(Self { state, host })
}
fn take_callback_flags(&self) -> HostCallbackFlags {
let flags = self.state.callback_flags.lock();
let out = *flags;
*flags = HostCallbackFlags::default();
out
}
fn begin_ui_session(&self) {
self.state.ui_should_close.store(0, Ordering::Release);
self.state.ui_active.store(1, Ordering::Release);
self.state.param_flush_requested.store(0, Ordering::Release);
self.state.state_dirty_requested.store(0, Ordering::Release);
self.state.timers.lock().clear();
}
fn end_ui_session(&self) {
self.state.ui_active.store(0, Ordering::Release);
self.state.ui_should_close.store(0, Ordering::Release);
self.state.param_flush_requested.store(0, Ordering::Release);
self.state.state_dirty_requested.store(0, Ordering::Release);
self.state.timers.lock().clear();
}
fn ui_should_close(&self) -> bool {
self.state.ui_should_close.load(Ordering::Acquire) != 0
}
fn ui_take_due_timers(&self) -> Vec<u32> {
let now = Instant::now();
let timers = &mut *self.state.timers.lock();
let mut due = Vec::new();
for timer in timers.iter_mut() {
if now >= timer.next_tick {
due.push(timer.id);
timer.next_tick = now + timer.period;
}
}
due
}
fn ui_take_param_flush_requested(&self) -> bool {
self.state.param_flush_requested.swap(0, Ordering::AcqRel) != 0
}
fn ui_take_state_dirty_requested(&self) -> bool {
self.state.state_dirty_requested.swap(0, Ordering::AcqRel) != 0
}
}
unsafe impl Send for HostRuntime {}
unsafe impl Sync for HostRuntime {}
struct PluginHandle {
_library: Library,
entry: *const ClapPluginEntry,
plugin: *const ClapPlugin,
plugin_id: String,
plugin_name: String,
}
unsafe impl Send for PluginHandle {}
unsafe impl Sync for PluginHandle {}
impl PluginHandle {
fn load(
plugin_path: &str,
plugin_id: Option<&str>,
host_runtime: Arc<HostRuntime>,
sample_rate: f64,
frames: u32,
) -> Result<Self, String> {
let factory_id = c"clap.plugin-factory";
let library = unsafe { Library::new(plugin_path) }.map_err(|e| e.to_string())?;
let entry_ptr = unsafe {
let sym = library
.get::<*const ClapPluginEntry>(b"clap_entry\0")
.map_err(|e| e.to_string())?;
*sym
};
if entry_ptr.is_null() {
return Err("CLAP entry symbol is null".to_string());
}
let entry = unsafe { &*entry_ptr };
let init = entry
.init
.ok_or_else(|| "CLAP entry missing init()".to_string())?;
let host_ptr = &host_runtime.host as *const ClapHost;
if unsafe { !init(host_ptr) } {
return Err(format!("CLAP entry init failed for {plugin_path}"));
}
let get_factory = entry
.get_factory
.ok_or_else(|| "CLAP entry missing get_factory()".to_string())?;
let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
if factory.is_null() {
return Err("CLAP plugin factory not found".to_string());
}
let factory_ref = unsafe { &*factory };
let get_count = factory_ref
.get_plugin_count
.ok_or_else(|| "CLAP factory missing get_plugin_count()".to_string())?;
let get_desc = factory_ref
.get_plugin_descriptor
.ok_or_else(|| "CLAP factory missing get_plugin_descriptor()".to_string())?;
let create = factory_ref
.create_plugin
.ok_or_else(|| "CLAP factory missing create_plugin()".to_string())?;
let count = unsafe { get_count(factory) };
if count == 0 {
return Err("CLAP factory returned zero plugins".to_string());
}
let mut selected_id = None::<CString>;
let mut selected_name = None::<String>;
for i in 0..count {
let desc = unsafe { get_desc(factory, i) };
if desc.is_null() {
continue;
}
let desc = unsafe { &*desc };
if desc.id.is_null() {
continue;
}
let id = unsafe { CStr::from_ptr(desc.id) };
let id_str = id.to_string_lossy();
let name_str = if desc.name.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(desc.name) }
.to_string_lossy()
.into_owned()
};
if plugin_id.is_none() || plugin_id == Some(id_str.as_ref()) {
selected_id = Some(
CString::new(id_str.as_ref()).map_err(|e| format!("Invalid plugin id: {e}"))?,
);
selected_name = Some(name_str);
break;
}
}
let selected_id = selected_id.ok_or_else(|| {
if let Some(id) = plugin_id {
format!("CLAP descriptor id not found in bundle: {id}")
} else {
"CLAP descriptor not found".to_string()
}
})?;
let plugin_name = selected_name.unwrap_or_else(|| {
Path::new(plugin_path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| plugin_path.to_string())
});
let plugin = unsafe { create(factory, &host_runtime.host, selected_id.as_ptr()) };
if plugin.is_null() {
return Err("CLAP factory create_plugin failed".to_string());
}
let plugin_ref = unsafe { &*plugin };
let plugin_init = plugin_ref
.init
.ok_or_else(|| "CLAP plugin missing init()".to_string())?;
if unsafe { !plugin_init(plugin) } {
return Err("CLAP plugin init() failed".to_string());
}
if let Some(activate) = plugin_ref.activate {
if unsafe { !activate(plugin, sample_rate, frames.max(1), frames.max(1)) } {
return Err("CLAP plugin activate() failed".to_string());
}
}
if let Some(start_processing) = plugin_ref.start_processing {
if unsafe { !start_processing(plugin) } {
return Err("CLAP plugin start_processing() failed".to_string());
}
}
let plugin_id_str = selected_id.to_string_lossy().into_owned();
Ok(Self {
_library: library,
entry: entry_ptr,
plugin,
plugin_id: plugin_id_str,
plugin_name,
})
}
fn plugin_id(&self) -> &str {
&self.plugin_id
}
fn plugin_name(&self) -> &str {
&self.plugin_name
}
fn process(&self, process: &mut ClapProcess) -> Result<bool, String> {
let plugin = unsafe { &*self.plugin };
let Some(process_fn) = plugin.process else {
return Ok(false);
};
let _status = unsafe { process_fn(self.plugin, process as *const _) };
Ok(true)
}
fn reset(&self) {
let plugin = unsafe { &*self.plugin };
if let Some(reset) = plugin.reset {
unsafe { reset(self.plugin) };
}
}
fn on_main_thread(&self) {
let plugin = unsafe { &*self.plugin };
if let Some(on_main_thread) = plugin.on_main_thread {
unsafe { on_main_thread(self.plugin) };
}
}
fn params_ext(&self) -> Option<&ClapPluginParams> {
let ext_id = c"clap.params";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginParams) })
}
fn state_ext(&self) -> Option<&ClapPluginStateExt> {
let ext_id = c"clap.state";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginStateExt) })
}
fn audio_ports_ext(&self) -> Option<&ClapPluginAudioPorts> {
let ext_id = c"clap.audio-ports";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginAudioPorts) })
}
fn note_ports_ext(&self) -> Option<&ClapPluginNotePorts> {
let ext_id = c"clap.note-ports";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginNotePorts) })
}
fn note_name_ext(&self) -> Option<&ClapPluginNoteName> {
let ext_id = c"clap.note-name";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginNoteName) })
}
fn get_note_names(&self) -> std::collections::HashMap<u8, String> {
let mut result = std::collections::HashMap::new();
let Some(ext) = self.note_name_ext() else {
return result;
};
let Some(count_fn) = ext.count else {
return result;
};
let Some(get_fn) = ext.get else {
return result;
};
let count = unsafe { count_fn(self.plugin) };
for i in 0..count {
let mut nn = ClapNoteName {
name: [0; 256],
port: -1,
key: -1,
channel: -1,
};
if unsafe { get_fn(self.plugin, i, &mut nn) } {
let name = unsafe {
std::ffi::CStr::from_ptr(nn.name.as_ptr())
.to_string_lossy()
.into_owned()
};
if nn.key >= 0 && nn.key <= 127 && !name.is_empty() {
result.insert(nn.key as u8, name);
}
}
}
result
}
fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
let Some(params) = self.params_ext() else {
return Vec::new();
};
let Some(count_fn) = params.count else {
return Vec::new();
};
let Some(get_info_fn) = params.get_info else {
return Vec::new();
};
let count = unsafe { count_fn(self.plugin) };
let mut out = Vec::with_capacity(count as usize);
for idx in 0..count {
let mut info = ClapParamInfoRaw {
id: 0,
flags: 0,
cookie: std::ptr::null_mut(),
name: [0; 256],
module: [0; 1024],
min_value: 0.0,
max_value: 1.0,
default_value: 0.0,
};
if unsafe { !get_info_fn(self.plugin, idx, &mut info as *mut _) } {
continue;
}
out.push(ClapParameterInfo {
id: info.id,
name: c_char_buf_to_string(&info.name),
module: c_char_buf_to_string(&info.module),
min_value: info.min_value,
max_value: info.max_value,
default_value: info.default_value,
});
}
out
}
fn parameter_values(&self, infos: &[ClapParameterInfo]) -> HashMap<u32, f64> {
let mut out = HashMap::new();
let Some(params) = self.params_ext() else {
for info in infos {
out.insert(info.id, info.default_value);
}
return out;
};
let Some(get_value_fn) = params.get_value else {
for info in infos {
out.insert(info.id, info.default_value);
}
return out;
};
for info in infos {
let mut value = info.default_value;
if unsafe { !get_value_fn(self.plugin, info.id, &mut value as *mut _) } {
value = info.default_value;
}
out.insert(info.id, value);
}
out
}
fn flush_params(
&self,
param_events: &[PendingParamEvent],
) -> (Vec<PendingParamValue>, Vec<PendingParamGesture>) {
let Some(params) = self.params_ext() else {
return (Vec::new(), Vec::new());
};
let Some(flush_fn) = params.flush else {
return (Vec::new(), Vec::new());
};
let (in_events, _in_ctx) = param_input_events_from(param_events);
let out_cap = param_events.len().max(32);
let (out_events, mut out_ctx) = output_events_ctx(out_cap);
unsafe {
flush_fn(self.plugin, &in_events, &out_events);
}
(
std::mem::take(&mut out_ctx.param_values),
std::mem::take(&mut out_ctx.param_gestures),
)
}
fn snapshot_state(&self) -> Result<ClapPluginState, String> {
let Some(state_ext) = self.state_ext() else {
return Ok(ClapPluginState { bytes: Vec::new() });
};
let Some(save_fn) = state_ext.save else {
return Ok(ClapPluginState { bytes: Vec::new() });
};
let mut bytes = Vec::<u8>::new();
let mut stream = ClapOStream {
ctx: (&mut bytes as *mut Vec<u8>).cast::<c_void>(),
write: Some(clap_ostream_write),
};
if unsafe {
!save_fn(
self.plugin,
&mut stream as *mut ClapOStream as *const ClapOStream,
)
} {
return Err("CLAP state save failed".to_string());
}
Ok(ClapPluginState { bytes })
}
fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
let Some(state_ext) = self.state_ext() else {
return Ok(());
};
let Some(load_fn) = state_ext.load else {
return Ok(());
};
let mut ctx = ClapIStreamCtx {
bytes: &state.bytes,
offset: 0,
};
let mut stream = ClapIStream {
ctx: (&mut ctx as *mut ClapIStreamCtx).cast::<c_void>(),
read: Some(clap_istream_read),
};
if unsafe {
!load_fn(
self.plugin,
&mut stream as *mut ClapIStream as *const ClapIStream,
)
} {
return Err("CLAP state load failed".to_string());
}
Ok(())
}
const CLAP_AUDIO_PORT_IS_MAIN: u32 = 1;
fn audio_port_channels(&self) -> (Option<AudioPortLayout>, Option<AudioPortLayout>) {
let Some(ext) = self.audio_ports_ext() else {
return (None, None);
};
let Some(count_fn) = ext.count else {
return (None, None);
};
let Some(get_fn) = ext.get else {
return (None, None);
};
let read_ports = |is_input: bool| -> AudioPortLayout {
let mut channels = Vec::new();
let mut main_count = 0;
let count = unsafe { count_fn(self.plugin, is_input) } as usize;
channels.reserve(count);
for idx in 0..count {
let mut info = ClapAudioPortInfoRaw {
id: 0,
name: [0; 256],
flags: 0,
channel_count: 1,
port_type: std::ptr::null(),
in_place_pair: u32::MAX,
};
if unsafe { get_fn(self.plugin, idx as u32, is_input, &mut info as *mut _) } {
channels.push((info.channel_count as usize).max(1));
if info.flags & Self::CLAP_AUDIO_PORT_IS_MAIN != 0 {
main_count += 1;
}
}
}
(channels, main_count)
};
(Some(read_ports(true)), Some(read_ports(false)))
}
fn note_port_layout(&self) -> (Option<usize>, Option<usize>) {
let Some(ext) = self.note_ports_ext() else {
return (None, None);
};
let Some(count_fn) = ext.count else {
return (None, None);
};
let in_count = unsafe { count_fn(self.plugin, true) } as usize;
let out_count = unsafe { count_fn(self.plugin, false) } as usize;
(Some(in_count), Some(out_count))
}
fn gui_ext(&self) -> Option<&ClapPluginGui> {
let ext_id = c"clap.gui";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginGui) })
}
fn gui_timer_support_ext(&self) -> Option<&ClapPluginTimerSupport> {
let ext_id = c"clap.timer-support";
let plugin = unsafe { &*self.plugin };
let get_extension = plugin.get_extension?;
let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
if ext_ptr.is_null() {
return None;
}
Some(unsafe { &*(ext_ptr as *const ClapPluginTimerSupport) })
}
fn gui_info(&self) -> Result<ClapGuiInfo, String> {
let gui = self
.gui_ext()
.ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
let is_api_supported = gui
.is_api_supported
.ok_or_else(|| "CLAP gui.is_api_supported is unavailable".to_string())?;
for (api, supports_embedded) in [
("x11", true),
("cocoa", true),
("x11", false),
("cocoa", false),
] {
let api_c = CString::new(api).map_err(|e| e.to_string())?;
if unsafe { is_api_supported(self.plugin, api_c.as_ptr(), !supports_embedded) } {
return Ok(ClapGuiInfo {
api: api.to_string(),
supports_embedded,
});
}
}
Err("No supported CLAP GUI API found".to_string())
}
fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
let gui = self
.gui_ext()
.ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
let create = gui
.create
.ok_or_else(|| "CLAP gui.create is unavailable".to_string())?;
let api_c = CString::new(api).map_err(|e| e.to_string())?;
if unsafe { !create(self.plugin, api_c.as_ptr(), is_floating) } {
return Err("CLAP gui.create failed".to_string());
}
Ok(())
}
fn gui_get_size(&self) -> Result<(u32, u32), String> {
let gui = self
.gui_ext()
.ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
let get_size = gui
.get_size
.ok_or_else(|| "CLAP gui.get_size is unavailable".to_string())?;
let mut width = 0;
let mut height = 0;
if unsafe { !get_size(self.plugin, &mut width, &mut height) } {
return Err("CLAP gui.get_size failed".to_string());
}
Ok((width, height))
}
fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
let gui = self
.gui_ext()
.ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
let set_parent = gui
.set_parent
.ok_or_else(|| "CLAP gui.set_parent is unavailable".to_string())?;
let clap_window = ClapWindow {
api: c"x11".as_ptr(),
handle: ClapWindowHandle { x11: window },
};
if unsafe { !set_parent(self.plugin, &clap_window) } {
return Err("CLAP gui.set_parent failed".to_string());
}
Ok(())
}
fn gui_show(&self) -> Result<(), String> {
let gui = self
.gui_ext()
.ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
let show = gui
.show
.ok_or_else(|| "CLAP gui.show is unavailable".to_string())?;
if unsafe { !show(self.plugin) } {
return Err("CLAP gui.show failed".to_string());
}
Ok(())
}
fn gui_hide(&self) {
if let Some(gui) = self.gui_ext()
&& let Some(hide) = gui.hide
{
unsafe { hide(self.plugin) };
}
}
fn gui_destroy(&self) {
if let Some(gui) = self.gui_ext()
&& let Some(destroy) = gui.destroy
{
unsafe { destroy(self.plugin) };
}
}
fn gui_on_timer(&self, timer_id: u32) {
if let Some(timer_ext) = self.gui_timer_support_ext()
&& let Some(on_timer) = timer_ext.on_timer
{
unsafe { on_timer(self.plugin, timer_id) };
}
}
}
impl Drop for PluginHandle {
fn drop(&mut self) {
unsafe {
if !self.plugin.is_null() {
let plugin = &*self.plugin;
if let Some(stop_processing) = plugin.stop_processing {
stop_processing(self.plugin);
}
if let Some(deactivate) = plugin.deactivate {
deactivate(self.plugin);
}
if let Some(destroy) = plugin.destroy {
destroy(self.plugin);
}
}
if !self.entry.is_null() {
let entry = &*self.entry;
if let Some(deinit) = entry.deinit {
deinit();
}
}
}
}
}
static HOST_THREAD_CHECK_EXT: ClapHostThreadCheck = ClapHostThreadCheck {
is_main_thread: Some(host_is_main_thread),
is_audio_thread: Some(host_is_audio_thread),
};
static HOST_LATENCY_EXT: ClapHostLatency = ClapHostLatency {
changed: Some(host_latency_changed),
};
static HOST_TAIL_EXT: ClapHostTail = ClapHostTail {
changed: Some(host_tail_changed),
};
static HOST_TIMER_EXT: ClapHostTimerSupport = ClapHostTimerSupport {
register_timer: Some(host_timer_register),
unregister_timer: Some(host_timer_unregister),
};
static HOST_GUI_EXT: ClapHostGui = ClapHostGui {
resize_hints_changed: Some(host_gui_resize_hints_changed),
request_resize: Some(host_gui_request_resize),
request_show: Some(host_gui_request_show),
request_hide: Some(host_gui_request_hide),
closed: Some(host_gui_closed),
};
static HOST_PARAMS_EXT: ClapHostParams = ClapHostParams {
rescan: Some(host_params_rescan),
clear: Some(host_params_clear),
request_flush: Some(host_params_request_flush),
};
static HOST_STATE_EXT: ClapHostState = ClapHostState {
mark_dirty: Some(host_state_mark_dirty),
};
static HOST_NOTE_NAME_EXT: ClapHostNoteName = ClapHostNoteName {
changed: Some(host_note_name_changed),
};
static NEXT_TIMER_ID: AtomicU32 = AtomicU32::new(1);
fn host_runtime_state(host: *const ClapHost) -> Option<&'static HostRuntimeState> {
if host.is_null() {
return None;
}
let state_ptr = unsafe { (*host).host_data as *const HostRuntimeState };
if state_ptr.is_null() {
return None;
}
Some(unsafe { &*state_ptr })
}
unsafe extern "C" fn host_get_extension(
_host: *const ClapHost,
_extension_id: *const c_char,
) -> *const c_void {
if _extension_id.is_null() {
return std::ptr::null();
}
let id = unsafe { CStr::from_ptr(_extension_id) }.to_string_lossy();
match id.as_ref() {
"clap.host.thread-check" => {
(&HOST_THREAD_CHECK_EXT as *const ClapHostThreadCheck).cast::<c_void>()
}
"clap.host.latency" => (&HOST_LATENCY_EXT as *const ClapHostLatency).cast::<c_void>(),
"clap.host.tail" => (&HOST_TAIL_EXT as *const ClapHostTail).cast::<c_void>(),
"clap.host.timer-support" => {
(&HOST_TIMER_EXT as *const ClapHostTimerSupport).cast::<c_void>()
}
"clap.host.gui" => host_runtime_state(_host)
.filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
.map(|_| (&HOST_GUI_EXT as *const ClapHostGui).cast::<c_void>())
.unwrap_or(std::ptr::null()),
"clap.host.params" => host_runtime_state(_host)
.filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
.map(|_| (&HOST_PARAMS_EXT as *const ClapHostParams).cast::<c_void>())
.unwrap_or(std::ptr::null()),
"clap.host.state" => host_runtime_state(_host)
.filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
.map(|_| (&HOST_STATE_EXT as *const ClapHostState).cast::<c_void>())
.unwrap_or(std::ptr::null()),
"clap.host.note-name" => (&HOST_NOTE_NAME_EXT as *const ClapHostNoteName).cast::<c_void>(),
_ => std::ptr::null(),
}
}
unsafe extern "C" fn host_request_process(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.callback_flags.lock().process = true;
}
}
unsafe extern "C" fn host_request_callback(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.callback_flags.lock().callback = true;
}
}
unsafe extern "C" fn host_request_restart(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.callback_flags.lock().restart = true;
}
}
unsafe extern "C" fn host_note_name_changed(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.note_names_dirty.store(1, Ordering::Release);
}
}
unsafe extern "C" fn host_is_main_thread(_host: *const ClapHost) -> bool {
CLAP_HOST_MAIN_THREAD.with(Cell::get)
}
unsafe extern "C" fn host_is_audio_thread(_host: *const ClapHost) -> bool {
CLAP_HOST_AUDIO_THREAD.with(Cell::get)
}
unsafe extern "C" fn host_latency_changed(_host: *const ClapHost) {}
unsafe extern "C" fn host_tail_changed(_host: *const ClapHost) {}
unsafe extern "C" fn host_timer_register(
_host: *const ClapHost,
_period_ms: u32,
timer_id: *mut u32,
) -> bool {
if timer_id.is_null() {
return false;
}
let id = NEXT_TIMER_ID.fetch_add(1, Ordering::Relaxed);
if let Some(state) = host_runtime_state(_host) {
let period_ms = _period_ms.max(1);
state.timers.lock().push(HostTimer {
id,
period: Duration::from_millis(period_ms as u64),
next_tick: Instant::now() + Duration::from_millis(period_ms as u64),
});
}
unsafe {
*timer_id = id;
}
true
}
unsafe extern "C" fn host_timer_unregister(_host: *const ClapHost, _timer_id: u32) -> bool {
if let Some(state) = host_runtime_state(_host) {
state.timers.lock().retain(|timer| timer.id != _timer_id);
}
true
}
unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const ClapHost) {}
unsafe extern "C" fn host_gui_request_resize(
_host: *const ClapHost,
_width: u32,
_height: u32,
) -> bool {
true
}
unsafe extern "C" fn host_gui_request_show(_host: *const ClapHost) -> bool {
true
}
unsafe extern "C" fn host_gui_request_hide(_host: *const ClapHost) -> bool {
if let Some(state) = host_runtime_state(_host) {
if state.ui_active.load(Ordering::Acquire) != 0 {
state.ui_should_close.store(1, Ordering::Release);
}
true
} else {
false
}
}
unsafe extern "C" fn host_gui_closed(_host: *const ClapHost, _was_destroyed: bool) {
if let Some(state) = host_runtime_state(_host)
&& state.ui_active.load(Ordering::Acquire) != 0
{
state.ui_should_close.store(1, Ordering::Release);
}
}
unsafe extern "C" fn host_params_rescan(_host: *const ClapHost, _flags: u32) {}
unsafe extern "C" fn host_params_clear(_host: *const ClapHost, _param_id: u32, _flags: u32) {}
unsafe extern "C" fn host_params_request_flush(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.param_flush_requested.store(1, Ordering::Release);
state.callback_flags.lock().callback = true;
}
}
unsafe extern "C" fn host_state_mark_dirty(_host: *const ClapHost) {
if let Some(state) = host_runtime_state(_host) {
state.state_dirty_requested.store(1, Ordering::Release);
state.callback_flags.lock().callback = true;
}
}
unsafe extern "C" fn input_events_size(_list: *const ClapInputEvents) -> u32 {
if _list.is_null() {
return 0;
}
let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
if ctx.is_null() {
return 0;
}
unsafe { (*ctx).events.len() as u32 }
}
unsafe extern "C" fn input_events_get(
_list: *const ClapInputEvents,
_index: u32,
) -> *const ClapEventHeader {
if _list.is_null() {
return std::ptr::null();
}
let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
if ctx.is_null() {
return std::ptr::null();
}
let events = unsafe { &(*ctx).events };
let Some(event) = events.get(_index as usize) else {
return std::ptr::null();
};
event.header_ptr()
}
unsafe extern "C" fn output_events_try_push(
_list: *const ClapOutputEvents,
_event: *const ClapEventHeader,
) -> bool {
if _list.is_null() || _event.is_null() {
return false;
}
let ctx = unsafe { (*_list).ctx as *mut ClapOutputEventsCtx };
if ctx.is_null() {
return false;
}
let header = unsafe { &*_event };
if header.space_id != CLAP_CORE_EVENT_SPACE_ID {
return false;
}
match header.type_ {
CLAP_EVENT_MIDI => {
if (header.size as usize) < std::mem::size_of::<ClapEventMidi>() {
return false;
}
let midi = unsafe { &*(_event as *const ClapEventMidi) };
unsafe {
(*ctx).midi_events.push(ClapMidiOutputEvent {
port: midi.port_index as usize,
event: MidiEvent::new(header.time, midi.data.to_vec()),
});
}
true
}
CLAP_EVENT_PARAM_VALUE => {
if (header.size as usize) < std::mem::size_of::<ClapEventParamValue>() {
return false;
}
let param = unsafe { &*(_event as *const ClapEventParamValue) };
unsafe {
(*ctx).param_values.push(PendingParamValue {
param_id: param.param_id,
value: param.value,
});
}
true
}
CLAP_EVENT_PARAM_GESTURE_BEGIN | CLAP_EVENT_PARAM_GESTURE_END => {
if (header.size as usize) < std::mem::size_of::<ClapEventParamGesture>() {
return false;
}
let gesture = unsafe { &*(_event as *const ClapEventParamGesture) };
unsafe {
(*ctx).param_gestures.push(PendingParamGesture {
param_id: gesture.param_id,
is_begin: header.type_ == CLAP_EVENT_PARAM_GESTURE_BEGIN,
});
}
true
}
_ => false,
}
}
fn input_events_from(
midi_events: &[MidiEvent],
param_events: &[PendingParamEvent],
sample_rate: f64,
transport: ClapTransportInfo,
has_note_ports: bool,
) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
let mut events = Vec::with_capacity(midi_events.len() + param_events.len() + 1);
let bpm = transport.bpm.max(1.0);
let sample_rate = sample_rate.max(1.0);
let seconds = transport.transport_sample as f64 / sample_rate;
let song_pos_seconds = (seconds * CLAP_SECTIME_FACTOR as f64) as i64;
let beats = seconds * (bpm / 60.0);
let song_pos_beats = (beats * CLAP_BEATTIME_FACTOR as f64) as i64;
let mut flags = CLAP_TRANSPORT_HAS_TEMPO
| CLAP_TRANSPORT_HAS_BEATS_TIMELINE
| CLAP_TRANSPORT_HAS_SECONDS_TIMELINE
| CLAP_TRANSPORT_HAS_TIME_SIGNATURE;
if transport.playing {
flags |= CLAP_TRANSPORT_IS_PLAYING;
}
let (loop_start_seconds, loop_end_seconds, loop_start_beats, loop_end_beats) =
if transport.loop_enabled {
if let Some((loop_start, loop_end)) = transport.loop_range_samples {
flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE;
let ls_sec = loop_start as f64 / sample_rate;
let le_sec = loop_end as f64 / sample_rate;
let ls_beats = ls_sec * (bpm / 60.0);
let le_beats = le_sec * (bpm / 60.0);
(
(ls_sec * CLAP_SECTIME_FACTOR as f64) as i64,
(le_sec * CLAP_SECTIME_FACTOR as f64) as i64,
(ls_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
(le_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
)
} else {
(0, 0, 0, 0)
}
} else {
(0, 0, 0, 0)
};
let ts_num = transport.tsig_num.max(1);
let ts_denom = transport.tsig_denom.max(1);
let beats_per_bar = ts_num as f64 * (4.0 / ts_denom as f64);
let bar_number = if beats_per_bar > 0.0 {
(beats / beats_per_bar).floor().max(0.0) as i32
} else {
0
};
let bar_start_beats = (bar_number as f64 * beats_per_bar * CLAP_BEATTIME_FACTOR as f64) as i64;
events.push(ClapInputEvent::Transport(ClapEventTransport {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventTransport>() as u32,
time: 0,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_TRANSPORT,
flags: 0,
},
flags,
song_pos_beats,
song_pos_seconds,
tempo: bpm,
tempo_inc: 0.0,
loop_start_beats,
loop_end_beats,
loop_start_seconds,
loop_end_seconds,
bar_start: bar_start_beats,
bar_number,
tsig_num: ts_num,
tsig_denom: ts_denom,
}));
for event in midi_events {
if event.data.is_empty() {
continue;
}
let mut data = [0_u8; 3];
let bytes = event.data.len().min(3);
data[..bytes].copy_from_slice(&event.data[..bytes]);
let status = data[0];
let is_note_on = (0x90..=0x9F).contains(&status);
let is_note_off = (0x80..=0x8F).contains(&status);
if has_note_ports && (is_note_on || is_note_off) {
let channel = (status & 0x0F) as i16;
let key = data.get(1).copied().unwrap_or(0).min(127) as i16;
let velocity_byte = data.get(2).copied().unwrap_or(0);
let velocity = if is_note_on && velocity_byte == 0 {
events.push(ClapInputEvent::Note(ClapEventNote {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventNote>() as u32,
time: event.frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_NOTE_OFF,
flags: 0,
},
note_id: -1,
port_index: 0,
channel,
key,
velocity: 0.0,
}));
continue;
} else {
(velocity_byte as f64 / 127.0).clamp(0.0, 1.0)
};
events.push(ClapInputEvent::Note(ClapEventNote {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventNote>() as u32,
time: event.frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: if is_note_on {
CLAP_EVENT_NOTE_ON
} else {
CLAP_EVENT_NOTE_OFF
},
flags: 0,
},
note_id: -1,
port_index: 0,
channel,
key,
velocity,
}));
} else {
events.push(ClapInputEvent::Midi(ClapEventMidi {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventMidi>() as u32,
time: event.frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_MIDI,
flags: 0,
},
port_index: 0,
data,
}));
}
}
for param in param_events {
match *param {
PendingParamEvent::Value {
param_id,
value,
frame,
} => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamValue>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_VALUE,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
cookie: std::ptr::null_mut(),
note_id: -1,
port_index: -1,
channel: -1,
key: -1,
value,
})),
PendingParamEvent::GestureBegin { param_id, frame } => {
events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamGesture>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
}))
}
PendingParamEvent::GestureEnd { param_id, frame } => {
events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamGesture>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_GESTURE_END,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
}))
}
}
}
events.sort_by_key(|event| match event {
ClapInputEvent::Note(e) => e.header.time,
ClapInputEvent::Midi(e) => e.header.time,
ClapInputEvent::ParamValue(e) => e.header.time,
ClapInputEvent::ParamGesture(e) => e.header.time,
ClapInputEvent::Transport(e) => e.header.time,
});
let mut ctx = Box::new(ClapInputEventsCtx { events });
let list = ClapInputEvents {
ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
size: Some(input_events_size),
get: Some(input_events_get),
};
(list, ctx)
}
fn param_input_events_from(
param_events: &[PendingParamEvent],
) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
let mut events = Vec::with_capacity(param_events.len());
for param in param_events {
match *param {
PendingParamEvent::Value {
param_id,
value,
frame,
} => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamValue>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_VALUE,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
cookie: std::ptr::null_mut(),
note_id: -1,
port_index: -1,
channel: -1,
key: -1,
value,
})),
PendingParamEvent::GestureBegin { param_id, frame } => {
events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamGesture>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
}))
}
PendingParamEvent::GestureEnd { param_id, frame } => {
events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
header: ClapEventHeader {
size: std::mem::size_of::<ClapEventParamGesture>() as u32,
time: frame,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_GESTURE_END,
flags: CLAP_EVENT_IS_LIVE,
},
param_id,
}))
}
}
}
events.sort_by_key(|event| match event {
ClapInputEvent::Note(e) => e.header.time,
ClapInputEvent::Midi(e) => e.header.time,
ClapInputEvent::ParamValue(e) => e.header.time,
ClapInputEvent::ParamGesture(e) => e.header.time,
ClapInputEvent::Transport(e) => e.header.time,
});
let mut ctx = Box::new(ClapInputEventsCtx { events });
let list = ClapInputEvents {
ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
size: Some(input_events_size),
get: Some(input_events_get),
};
(list, ctx)
}
fn output_events_ctx(capacity: usize) -> (ClapOutputEvents, Box<ClapOutputEventsCtx>) {
let mut ctx = Box::new(ClapOutputEventsCtx {
midi_events: Vec::with_capacity(capacity),
param_values: Vec::with_capacity(capacity / 2),
param_gestures: Vec::with_capacity(capacity / 4),
});
let list = ClapOutputEvents {
ctx: (&mut *ctx as *mut ClapOutputEventsCtx).cast::<c_void>(),
try_push: Some(output_events_try_push),
};
(list, ctx)
}
fn c_char_buf_to_string<const N: usize>(buf: &[c_char; N]) -> String {
let bytes = buf
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect::<Vec<u8>>();
String::from_utf8_lossy(&bytes).to_string()
}
fn split_plugin_spec(spec: &str) -> (&str, Option<&str>) {
if let Some((path, id)) = spec.split_once("::")
&& !id.trim().is_empty()
{
return (path, Some(id.trim()));
}
(spec, None)
}
unsafe extern "C" fn clap_ostream_write(
stream: *const ClapOStream,
buffer: *const c_void,
size: u64,
) -> i64 {
if stream.is_null() || buffer.is_null() {
return -1;
}
let ctx = unsafe { (*stream).ctx as *mut Vec<u8> };
if ctx.is_null() {
return -1;
}
let n = (size as usize).min(isize::MAX as usize);
let src = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), n) };
unsafe {
(*ctx).extend_from_slice(src);
}
n as i64
}
unsafe extern "C" fn clap_istream_read(
stream: *const ClapIStream,
buffer: *mut c_void,
size: u64,
) -> i64 {
if stream.is_null() || buffer.is_null() {
return -1;
}
let ctx = unsafe { (*stream).ctx as *mut ClapIStreamCtx<'_> };
if ctx.is_null() {
return -1;
}
let ctx = unsafe { &mut *ctx };
let remaining = ctx.bytes.len().saturating_sub(ctx.offset);
if remaining == 0 {
return 0;
}
let n = remaining.min(size as usize);
let dst = unsafe { std::slice::from_raw_parts_mut(buffer.cast::<u8>(), n) };
dst.copy_from_slice(&ctx.bytes[ctx.offset..ctx.offset + n]);
ctx.offset += n;
n as i64
}
pub fn list_plugins() -> Vec<ClapPluginInfo> {
list_plugins_with_capabilities(false)
}
pub fn is_supported_clap_binary(path: &Path) -> bool {
path.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("clap"))
}
pub fn list_plugins_with_capabilities(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
let mut roots = default_clap_search_roots();
if let Ok(extra) = std::env::var("CLAP_PATH") {
for p in std::env::split_paths(&extra) {
if !p.as_os_str().is_empty() {
roots.push(p);
}
}
}
let mut out = Vec::new();
for root in roots {
collect_clap_plugins(&root, &mut out, scan_capabilities);
}
out.sort_by_key(|a| a.name.to_lowercase());
out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));
out
}
fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else {
continue;
};
if ft.is_dir() {
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
matches!(
name,
"deps" | "build" | "incremental" | ".fingerprint" | "examples"
)
})
{
continue;
}
collect_clap_plugins(&path, out, scan_capabilities);
continue;
}
if is_supported_clap_binary(&path) {
let infos = scan_bundle_descriptors(&path, scan_capabilities);
if infos.is_empty() {
let name = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
out.push(ClapPluginInfo {
name,
path: path.to_string_lossy().to_string(),
capabilities: None,
});
} else {
out.extend(infos);
}
}
}
}
fn scan_bundle_descriptors(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
let path_str = path.to_string_lossy().to_string();
let factory_id = c"clap.plugin-factory";
let host_runtime = match HostRuntime::new() {
Ok(runtime) => runtime,
Err(_) => return Vec::new(),
};
let library = match unsafe { Library::new(path) } {
Ok(lib) => lib,
Err(_) => return Vec::new(),
};
let entry_ptr = unsafe {
match library.get::<*const ClapPluginEntry>(b"clap_entry\0") {
Ok(sym) => *sym,
Err(_) => return Vec::new(),
}
};
if entry_ptr.is_null() {
return Vec::new();
}
let entry = unsafe { &*entry_ptr };
let Some(init) = entry.init else {
return Vec::new();
};
let host_ptr = &host_runtime.host;
if unsafe { !init(host_ptr) } {
return Vec::new();
}
let mut out = Vec::new();
if let Some(get_factory) = entry.get_factory {
let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
if !factory.is_null() {
let factory_ref = unsafe { &*factory };
if let (Some(get_count), Some(get_desc)) = (
factory_ref.get_plugin_count,
factory_ref.get_plugin_descriptor,
) {
let count = unsafe { get_count(factory) };
for i in 0..count {
let desc = unsafe { get_desc(factory, i) };
if desc.is_null() {
continue;
}
let desc = unsafe { &*desc };
if desc.id.is_null() || desc.name.is_null() {
continue;
}
let id = unsafe { CStr::from_ptr(desc.id) }
.to_string_lossy()
.to_string();
let name = unsafe { CStr::from_ptr(desc.name) }
.to_string_lossy()
.to_string();
let capabilities = if scan_capabilities {
scan_plugin_capabilities(factory_ref, factory, &host_runtime.host, &id)
} else {
None
};
out.push(ClapPluginInfo {
name,
path: format!("{path_str}::{id}"),
capabilities,
});
}
}
}
}
if let Some(deinit) = entry.deinit {
unsafe { deinit() };
}
out
}
fn scan_plugin_capabilities(
factory: &ClapPluginFactory,
factory_ptr: *const ClapPluginFactory,
host: &ClapHost,
plugin_id: &str,
) -> Option<ClapPluginCapabilities> {
let create = factory.create_plugin?;
let id_cstring = CString::new(plugin_id).ok()?;
let plugin = unsafe { create(factory_ptr, host, id_cstring.as_ptr()) };
if plugin.is_null() {
return None;
}
let plugin_ref = unsafe { &*plugin };
let plugin_init = plugin_ref.init?;
if unsafe { !plugin_init(plugin) } {
return None;
}
let mut capabilities = ClapPluginCapabilities {
has_gui: false,
gui_apis: Vec::new(),
supports_embedded: false,
supports_floating: false,
has_params: false,
has_state: false,
audio_inputs: 0,
audio_outputs: 0,
midi_inputs: 0,
midi_outputs: 0,
};
if let Some(get_extension) = plugin_ref.get_extension {
let gui_ext_id = c"clap.gui";
let gui_ptr = unsafe { get_extension(plugin, gui_ext_id.as_ptr()) };
if !gui_ptr.is_null() {
capabilities.has_gui = true;
let gui = unsafe { &*(gui_ptr as *const ClapPluginGui) };
if let Some(is_api_supported) = gui.is_api_supported {
for api in ["x11", "cocoa"] {
if let Ok(api_cstr) = CString::new(api) {
if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), false) } {
capabilities.gui_apis.push(format!("{} (embedded)", api));
capabilities.supports_embedded = true;
}
if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), true) } {
if !capabilities.supports_embedded {
capabilities.gui_apis.push(format!("{} (floating)", api));
}
capabilities.supports_floating = true;
}
}
}
}
}
let params_ext_id = c"clap.params";
let params_ptr = unsafe { get_extension(plugin, params_ext_id.as_ptr()) };
capabilities.has_params = !params_ptr.is_null();
let state_ext_id = c"clap.state";
let state_ptr = unsafe { get_extension(plugin, state_ext_id.as_ptr()) };
capabilities.has_state = !state_ptr.is_null();
let audio_ports_ext_id = c"clap.audio-ports";
let audio_ports_ptr = unsafe { get_extension(plugin, audio_ports_ext_id.as_ptr()) };
if !audio_ports_ptr.is_null() {
let audio_ports = unsafe { &*(audio_ports_ptr as *const ClapPluginAudioPorts) };
if let Some(count_fn) = audio_ports.count {
capabilities.audio_inputs = unsafe { count_fn(plugin, true) } as usize;
capabilities.audio_outputs = unsafe { count_fn(plugin, false) } as usize;
}
}
let note_ports_ext_id = c"clap.note-ports";
let note_ports_ptr = unsafe { get_extension(plugin, note_ports_ext_id.as_ptr()) };
if !note_ports_ptr.is_null() {
let note_ports = unsafe { &*(note_ports_ptr as *const ClapPluginNotePorts) };
if let Some(count_fn) = note_ports.count {
capabilities.midi_inputs = unsafe { count_fn(plugin, true) } as usize;
capabilities.midi_outputs = unsafe { count_fn(plugin, false) } as usize;
}
}
}
if let Some(destroy) = plugin_ref.destroy {
unsafe { destroy(plugin) };
}
Some(capabilities)
}
fn default_clap_search_roots() -> Vec<PathBuf> {
#[cfg(target_os = "macos")]
{
let mut roots = Vec::new();
paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
roots
}
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
{
let mut roots = Vec::new();
paths::push_unix_plugin_roots(&mut roots, "clap");
roots
}
#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd"
)))]
{
Vec::new()
}
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use super::collect_clap_plugins;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(unix)]
fn make_symlink(src: &PathBuf, dst: &PathBuf) {
std::os::unix::fs::symlink(src, dst).expect("should create symlink");
}
#[cfg(unix)]
#[test]
fn collect_clap_plugins_includes_symlinked_clap_files() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be valid")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"maolan-clap-symlink-test-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&root).expect("should create temp dir");
let target_file = root.join("librural_modeler.so");
fs::write(&target_file, b"not a real clap binary").expect("should create target file");
let clap_link = root.join("RuralModeler.clap");
make_symlink(&PathBuf::from("librural_modeler.so"), &clap_link);
let mut out = Vec::new();
collect_clap_plugins(&root, &mut out, false);
assert!(
out.iter()
.any(|info| info.path == clap_link.to_string_lossy()),
"scanner should include symlinked .clap files"
);
fs::remove_dir_all(&root).expect("should remove temp dir");
}
}