Skip to main content

truce_rack_clap/
lib.rs

1//! CLAP host implementation for the truce-rack framework.
2//!
3//! CLAP is the simplest format to host because the entire API is
4//! pure C with no platform-specific oddities. truce-rack-clap uses
5//! `clap-sys` for raw bindings and `libloading` to open plugin
6//! bundles at runtime — no C++ glue is involved.
7//!
8//! # Lifecycle
9//!
10//! Each `.clap` bundle ships a single `clap_entry` symbol. The
11//! scanner opens the bundle, calls `entry.init(path)`, asks the
12//! factory at `CLAP_PLUGIN_FACTORY_ID` to enumerate its plugins,
13//! converts each `clap_plugin_descriptor` into a [`PluginInfo`]
14//! and then calls `entry.deinit()` to release scanner-only state.
15//!
16//! [`ClapScanner::load`] re-opens the bundle, holds the entry +
17//! library alive for the lifetime of the returned [`ClapPlugin`],
18//! and on `Drop` calls the plugin's `destroy` followed by the
19//! entry's `deinit`.
20
21use truce_rack_core::buffer::AudioBuffer;
22use truce_rack_core::bus::BusLayout;
23use truce_rack_core::error::{Error, Result};
24use truce_rack_core::events::EventList;
25use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
26use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
27use truce_rack_core::transport::TransportInfo;
28use truce_rack_core::scanner::PluginScanner;
29use truce_rack_core::wrapper::run_audio_block_with;
30
31use clap_sys::audio_buffer::clap_audio_buffer;
32use clap_sys::entry::clap_plugin_entry;
33use clap_sys::events::{
34    CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON,
35    CLAP_EVENT_PARAM_VALUE, CLAP_EVENT_TRANSPORT, CLAP_TRANSPORT_HAS_BEATS_TIMELINE,
36    CLAP_TRANSPORT_HAS_SECONDS_TIMELINE, CLAP_TRANSPORT_HAS_TEMPO,
37    CLAP_TRANSPORT_HAS_TIME_SIGNATURE, CLAP_TRANSPORT_IS_LOOP_ACTIVE, CLAP_TRANSPORT_IS_PLAYING,
38    CLAP_TRANSPORT_IS_RECORDING, clap_event_header, clap_event_midi, clap_event_note,
39    clap_event_param_value, clap_event_transport, clap_input_events, clap_output_events,
40    clap_transport_flags,
41};
42use clap_sys::fixedpoint::{CLAP_BEATTIME_FACTOR, CLAP_SECTIME_FACTOR};
43use clap_sys::ext::gui::{
44    CLAP_EXT_GUI, CLAP_WINDOW_API_COCOA, CLAP_WINDOW_API_WIN32, CLAP_WINDOW_API_X11,
45    clap_plugin_gui, clap_window, clap_window_handle,
46};
47use clap_sys::ext::params::{
48    CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_ENUM,
49    CLAP_PARAM_IS_HIDDEN, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, clap_param_info,
50    clap_plugin_params,
51};
52use clap_sys::ext::state::{CLAP_EXT_STATE, clap_plugin_state};
53use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory};
54use clap_sys::plugin::{clap_plugin, clap_plugin_descriptor};
55use clap_sys::process::{
56    CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR,
57    CLAP_PROCESS_SLEEP, CLAP_PROCESS_TAIL, clap_process,
58};
59use clap_sys::stream::{clap_istream, clap_ostream};
60
61use std::ffi::{CStr, CString, c_char};
62use std::path::{Path, PathBuf};
63use std::ptr;
64
65/// Format identifier — used as the `format` field on
66/// [`PluginInfo`] entries this crate returns.
67pub const FORMAT: &str = "clap";
68
69/// Filename extension every CLAP plugin uses, including the
70/// leading dot.
71pub const CLAP_EXTENSION: &str = ".clap";
72
73/// Symbol name `clap_entry` plugins must export.
74const ENTRY_SYMBOL: &[u8] = b"clap_entry\0";
75
76/// CLAP scanner.
77///
78/// Walks the standard CLAP plugin directories for the current OS
79/// and returns one entry per discovered CLAP plugin. Calling
80/// [`PluginScanner::scan_path`] lets a host scan a custom
81/// directory (useful for sandboxed test fixtures or
82/// per-application bundled plugins).
83#[derive(Debug, Default)]
84pub struct ClapScanner;
85
86impl ClapScanner {
87    /// Construct a default scanner. There's no config state; the
88    /// type exists so consumers have a stable handle to scan
89    /// from.
90    #[must_use]
91    pub fn new() -> Self {
92        Self
93    }
94}
95
96impl PluginScanner for ClapScanner {
97    type Plugin = ClapPlugin;
98
99    fn scan(&self) -> Result<Vec<PluginInfo>> {
100        let mut out = Vec::new();
101        for dir in default_clap_paths() {
102            if dir.exists() {
103                scan_dir_into(&dir, &mut out);
104            }
105        }
106        Ok(out)
107    }
108
109    fn scan_path(&self, path: &Path) -> Result<Vec<PluginInfo>> {
110        let mut out = Vec::new();
111        if path.exists() {
112            scan_dir_into(path, &mut out);
113        }
114        Ok(out)
115    }
116
117    fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
118        ClapPlugin::load_from(info)
119    }
120}
121
122/// Standard locations the CLAP spec lists for each OS. Mirrors
123/// what the CLAP example host walks.
124#[must_use]
125pub fn default_clap_paths() -> Vec<PathBuf> {
126    let mut out = Vec::new();
127    if let Some(home) = std::env::var_os("HOME") {
128        let mut user = PathBuf::from(home);
129        #[cfg(target_os = "macos")]
130        user.push("Library/Audio/Plug-Ins/CLAP");
131        #[cfg(target_os = "linux")]
132        user.push(".clap");
133        #[cfg(target_os = "windows")]
134        user.push("AppData/Local/Programs/Common/CLAP");
135        out.push(user);
136    }
137    #[cfg(target_os = "macos")]
138    out.push(PathBuf::from("/Library/Audio/Plug-Ins/CLAP"));
139    #[cfg(target_os = "linux")]
140    out.push(PathBuf::from("/usr/lib/clap"));
141    #[cfg(target_os = "windows")]
142    {
143        if let Some(pf) = std::env::var_os("CommonProgramFiles") {
144            let mut p = PathBuf::from(pf);
145            p.push("CLAP");
146            out.push(p);
147        }
148    }
149    out
150}
151
152fn scan_dir_into(dir: &Path, out: &mut Vec<PluginInfo>) {
153    let Ok(entries) = std::fs::read_dir(dir) else {
154        return;
155    };
156    for entry in entries.flatten() {
157        let path = entry.path();
158        let name = match path.file_name().and_then(|n| n.to_str()) {
159            Some(n) => n.to_string(),
160            None => continue,
161        };
162        if !name.ends_with(CLAP_EXTENSION) {
163            continue;
164        }
165        // Failure to scan one bundle should not abort the whole
166        // walk — the host still wants the plugins it can see.
167        if let Err(err) = scan_bundle_into(&path, out) {
168            eprintln!("[truce-rack-clap] skipping {}: {err}", path.display());
169        }
170    }
171}
172
173fn scan_bundle_into(bundle_path: &Path, out: &mut Vec<PluginInfo>) -> Result<()> {
174    let binary = bundle_binary_path(bundle_path);
175    let handle = unsafe { LoadedLibrary::open(&binary)? };
176    let entry = handle.entry()?;
177    unsafe { entry.init(&binary)? };
178    let factory = unsafe { entry.factory() };
179    if !factory.is_null() {
180        let count = unsafe { ((*factory).get_plugin_count.unwrap_or(empty_count))(factory) };
181        for idx in 0..count {
182            let desc =
183                unsafe { ((*factory).get_plugin_descriptor.unwrap_or(empty_desc))(factory, idx) };
184            if desc.is_null() {
185                continue;
186            }
187            out.push(unsafe { descriptor_to_info(bundle_path, &*desc) });
188        }
189    }
190    unsafe { entry.deinit() };
191    Ok(())
192}
193
194unsafe extern "C" fn empty_count(_: *const clap_plugin_factory) -> u32 {
195    0
196}
197
198unsafe extern "C" fn empty_desc(
199    _: *const clap_plugin_factory,
200    _: u32,
201) -> *const clap_plugin_descriptor {
202    ptr::null()
203}
204
205/// Per-platform `.clap` bundle layout. macOS uses NSBundle-style
206/// `Contents/MacOS/<stem>`; Linux / Windows treat the `.clap`
207/// file itself as the dylib.
208fn bundle_binary_path(bundle: &Path) -> PathBuf {
209    #[cfg(target_os = "macos")]
210    {
211        let stem = bundle.file_stem().unwrap_or_default();
212        if bundle.is_dir() {
213            return bundle.join("Contents/MacOS").join(stem);
214        }
215    }
216    bundle.to_path_buf()
217}
218
219unsafe fn descriptor_to_info(bundle_path: &Path, desc: &clap_plugin_descriptor) -> PluginInfo {
220    let id = unsafe { cstr_to_string(desc.id) };
221    let name = unsafe { cstr_to_string(desc.name) };
222    let vendor = unsafe { cstr_to_string(desc.vendor) };
223    let version_str = unsafe { cstr_to_string(desc.version) };
224    let version = parse_version(&version_str);
225    let (category, accepts_midi) = unsafe { categorize(desc.features) };
226    PluginInfo {
227        name,
228        vendor,
229        version,
230        category,
231        path: bundle_path.to_path_buf(),
232        unique_id: id,
233        format: FORMAT,
234        has_editor: false, // honest default — set during load via the GUI ext.
235        accepts_midi,
236    }
237}
238
239/// CLAP feature strings live in a NULL-terminated array of C
240/// strings. We scan for instrument / note-effect / analyzer
241/// markers and the `note-input` / `note-effect` markers used to
242/// flag MIDI capability.
243unsafe fn categorize(features: *const *const c_char) -> (PluginCategory, bool) {
244    if features.is_null() {
245        return (PluginCategory::Effect, false);
246    }
247    let mut category = PluginCategory::Effect;
248    let mut accepts_midi = false;
249    let mut idx = 0usize;
250    loop {
251        let p = unsafe { *features.add(idx) };
252        if p.is_null() {
253            break;
254        }
255        let bytes = unsafe { CStr::from_ptr(p).to_bytes() };
256        match bytes {
257            b"instrument" => category = PluginCategory::Instrument,
258            b"note-effect" => {
259                category = PluginCategory::NoteEffect;
260                accepts_midi = true;
261            }
262            b"analyzer" => category = PluginCategory::Analyzer,
263            b"utility" => category = PluginCategory::Tool,
264            b"note-input" => accepts_midi = true,
265            _ => {}
266        }
267        idx += 1;
268    }
269    (category, accepts_midi)
270}
271
272/// CLAP versions are `"major.minor.patch"`. We pack the first
273/// three numeric components into a `u32` as
274/// `(major << 16) | (minor << 8) | patch`, matching what the
275/// legacy rack used.
276fn parse_version(s: &str) -> u32 {
277    let mut parts = s.split('.').map(|p| p.parse::<u32>().unwrap_or(0));
278    let major = parts.next().unwrap_or(0);
279    let minor = parts.next().unwrap_or(0);
280    let patch = parts.next().unwrap_or(0);
281    (major << 16) | (minor << 8) | patch
282}
283
284fn empty_param_info() -> clap_param_info {
285    clap_param_info {
286        id: 0,
287        flags: 0,
288        cookie: ptr::null_mut(),
289        name: [0; clap_sys::string_sizes::CLAP_NAME_SIZE],
290        module: [0; clap_sys::string_sizes::CLAP_PATH_SIZE],
291        min_value: 0.0,
292        max_value: 0.0,
293        default_value: 0.0,
294    }
295}
296
297fn clap_param_info_to_rack(info: &clap_param_info) -> ParameterInfo {
298    let name = c_buf_to_string(&info.name);
299    let mut flags = truce_rack_core::info::ParameterFlags::empty();
300    if info.flags & CLAP_PARAM_IS_BYPASS != 0 {
301        flags |= truce_rack_core::info::ParameterFlags::BYPASS;
302    }
303    if info.flags & CLAP_PARAM_IS_AUTOMATABLE != 0 {
304        flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
305    }
306    if info.flags & CLAP_PARAM_IS_HIDDEN != 0 {
307        flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
308    }
309    if info.flags & CLAP_PARAM_IS_READONLY != 0 {
310        flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
311    }
312    if info.flags & CLAP_PARAM_IS_ENUM != 0 {
313        flags |= truce_rack_core::info::ParameterFlags::ENUMERATED;
314    }
315    let step_count = if info.flags & CLAP_PARAM_IS_STEPPED != 0 {
316        // Stepped parameters have integer-valued [min, max]; the
317        // step count is the number of distinct values.
318        #[allow(clippy::cast_possible_truncation)]
319        let span = (info.max_value - info.min_value).round() as i64;
320        u32::try_from(span + 1).unwrap_or(0)
321    } else {
322        0
323    };
324    ParameterInfo {
325        id: info.id,
326        name: name.clone(),
327        short_name: name,
328        unit: String::new(), // CLAP doesn't expose a separate unit field
329        min: info.min_value,
330        max: info.max_value,
331        default: info.default_value,
332        step_count,
333        flags,
334    }
335}
336
337#[allow(clippy::cast_sign_loss)]
338fn c_buf_to_string(buf: &[c_char]) -> String {
339    // CLAP `char` is signed on Apple toolchains, unsigned elsewhere;
340    // the cast preserves bit pattern, which is what
341    // `String::from_utf8_lossy` wants.
342    let bytes: Vec<u8> = buf
343        .iter()
344        .take_while(|&&b| b != 0)
345        .map(|&b| b as u8)
346        .collect();
347    String::from_utf8_lossy(&bytes).into_owned()
348}
349
350unsafe fn cstr_to_string(p: *const c_char) -> String {
351    if p.is_null() {
352        return String::new();
353    }
354    unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
355}
356
357/// RAII wrapper around a `libloading::Library` plus the
358/// `clap_entry` symbol it exports.
359struct LoadedLibrary {
360    library: libloading::Library,
361}
362
363impl LoadedLibrary {
364    unsafe fn open(path: &Path) -> Result<Self> {
365        // SAFETY: caller guarantees `path` points at a valid
366        // CLAP plugin bundle binary; `libloading::Library::new`
367        // is itself the OS dlopen call.
368        let library = unsafe { libloading::Library::new(path) }.map_err(|e| Error::LoadFailed {
369            path: path.to_path_buf(),
370            reason: format!("dlopen failed: {e}"),
371        })?;
372        Ok(Self { library })
373    }
374
375    fn entry(&self) -> Result<EntryRef<'_>> {
376        let symbol: libloading::Symbol<'_, *const clap_plugin_entry> = unsafe {
377            self.library
378                .get(ENTRY_SYMBOL)
379                .map_err(|e| Error::LoadFailed {
380                    path: PathBuf::new(),
381                    reason: format!("missing clap_entry symbol: {e}"),
382                })?
383        };
384        let entry = *symbol;
385        if entry.is_null() {
386            return Err(Error::LoadFailed {
387                path: PathBuf::new(),
388                reason: "clap_entry symbol resolved to NULL".into(),
389            });
390        }
391        Ok(EntryRef {
392            entry,
393            _phantom: std::marker::PhantomData,
394        })
395    }
396}
397
398struct EntryRef<'a> {
399    entry: *const clap_plugin_entry,
400    // The reference lifetime ties the entry pointer to the
401    // library handle it was loaded from.
402    #[allow(dead_code)]
403    _phantom: std::marker::PhantomData<&'a LoadedLibrary>,
404}
405
406impl EntryRef<'_> {
407    unsafe fn init(&self, path: &Path) -> Result<()> {
408        let init = unsafe { (*self.entry).init };
409        if let Some(init) = init {
410            let c_path =
411                CString::new(path.to_string_lossy().as_bytes()).map_err(|e| Error::LoadFailed {
412                    path: path.to_path_buf(),
413                    reason: format!("plugin path is not a valid C string: {e}"),
414                })?;
415            if !unsafe { init(c_path.as_ptr()) } {
416                return Err(Error::LoadFailed {
417                    path: path.to_path_buf(),
418                    reason: "clap_entry::init returned false".into(),
419                });
420            }
421        }
422        Ok(())
423    }
424
425    unsafe fn deinit(&self) {
426        if let Some(deinit) = unsafe { (*self.entry).deinit } {
427            unsafe { deinit() };
428        }
429    }
430
431    unsafe fn factory(&self) -> *const clap_plugin_factory {
432        let Some(get_factory) = (unsafe { (*self.entry).get_factory }) else {
433            return ptr::null();
434        };
435        let raw = unsafe { get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) };
436        raw.cast::<clap_plugin_factory>()
437    }
438}
439
440/// One loaded CLAP plugin instance.
441///
442/// Holds the raw `*const clap_plugin` pointer plus the metadata
443/// needed for the truce-rack-core trait impls. The pointer is owned —
444/// `Drop` calls `clap_plugin::destroy` and `clap_entry::deinit`.
445pub struct ClapPlugin {
446    info: PluginInfo,
447    layouts: Vec<BusLayout>,
448    active_layout: Option<BusLayout>,
449    plugin: *const clap_plugin,
450    library: LoadedLibrary,
451    bundle_path: PathBuf,
452    started_processing: bool,
453    /// Cached `clap.params` extension. NULL if the plugin doesn't
454    /// expose any parameters.
455    params_ext: *const clap_plugin_params,
456    /// Cached `clap.state` extension. NULL if the plugin doesn't
457    /// expose state save/load.
458    state_ext: *const clap_plugin_state,
459    /// Cached `clap.gui` extension. NULL when the plugin has no
460    /// custom editor.
461    gui_ext: *const clap_plugin_gui,
462    /// Whether the editor has been `create`d but not yet
463    /// `destroy`ed — `clap.gui` separates the two lifecycles.
464    gui_open: bool,
465    /// Running `steady_time` counter — CLAP wants a monotonically
466    /// increasing sample-count across activations.
467    steady_time: i64,
468    /// Parameter changes queued from `set_parameter` while the
469    /// plugin is processing; drained into the next `process` call's
470    /// input events.
471    pending_param_changes: Vec<(u32, f64)>,
472}
473
474// SAFETY: The CLAP spec is explicit that a plugin handle may be
475// moved between threads when audio processing is stopped. Within
476// truce-rack-clap we serialize all calls through `&mut self`, so the
477// pointer is never aliased.
478unsafe impl Send for ClapPlugin {}
479
480impl ClapPlugin {
481    fn load_from(info: &PluginInfo) -> Result<Self> {
482        let bundle_path = info.path.clone();
483        let binary = bundle_binary_path(&bundle_path);
484        let library = unsafe { LoadedLibrary::open(&binary)? };
485        let entry = library.entry()?;
486        unsafe { entry.init(&binary)? };
487        let factory = unsafe { entry.factory() };
488        if factory.is_null() {
489            unsafe { entry.deinit() };
490            return Err(Error::LoadFailed {
491                path: bundle_path,
492                reason: "plugin has no clap.plugin-factory".into(),
493            });
494        }
495        let create = unsafe { (*factory).create_plugin }.ok_or_else(|| Error::LoadFailed {
496            path: bundle_path.clone(),
497            reason: "factory missing create_plugin".into(),
498        })?;
499        let id_cstring = CString::new(info.unique_id.as_str()).map_err(|e| Error::LoadFailed {
500            path: bundle_path.clone(),
501            reason: format!("plugin id is not a valid C string: {e}"),
502        })?;
503        let host = ptr::null(); // TODO(truce-rack): supply a real clap_host.
504        let plugin = unsafe { create(factory, host, id_cstring.as_ptr()) };
505        if plugin.is_null() {
506            unsafe { entry.deinit() };
507            return Err(Error::LoadFailed {
508                path: bundle_path,
509                reason: "factory.create_plugin returned NULL".into(),
510            });
511        }
512        let init = unsafe { (*plugin).init }.ok_or_else(|| Error::LoadFailed {
513            path: bundle_path.clone(),
514            reason: "plugin missing init".into(),
515        })?;
516        if !unsafe { init(plugin) } {
517            if let Some(destroy) = unsafe { (*plugin).destroy } {
518                unsafe { destroy(plugin) };
519            }
520            unsafe { entry.deinit() };
521            return Err(Error::LoadFailed {
522                path: bundle_path,
523                reason: "clap_plugin::init returned false".into(),
524            });
525        }
526        let params_ext = unsafe { lookup_extension::<clap_plugin_params>(plugin, CLAP_EXT_PARAMS) };
527        let state_ext = unsafe { lookup_extension::<clap_plugin_state>(plugin, CLAP_EXT_STATE) };
528        let gui_ext = unsafe { lookup_extension::<clap_plugin_gui>(plugin, CLAP_EXT_GUI) };
529        let mut info = info.clone();
530        if !gui_ext.is_null() && unsafe { gui_supports_current_platform(plugin, gui_ext) } {
531            info.has_editor = true;
532        }
533        Ok(Self {
534            info,
535            layouts: vec![BusLayout::stereo()],
536            active_layout: None,
537            plugin,
538            library,
539            bundle_path,
540            started_processing: false,
541            params_ext,
542            state_ext,
543            gui_ext,
544            gui_open: false,
545            steady_time: 0,
546            pending_param_changes: Vec::new(),
547        })
548    }
549}
550
551/// Look up a CLAP extension by id. Returns `null` if the plugin
552/// doesn't implement that extension. The returned pointer's
553/// lifetime is the plugin instance; callers must not outlive
554/// the plugin.
555unsafe fn lookup_extension<T>(plugin: *const clap_plugin, id: &CStr) -> *const T {
556    let Some(get_extension) = (unsafe { (*plugin).get_extension }) else {
557        return ptr::null();
558    };
559    let raw = unsafe { get_extension(plugin, id.as_ptr()) };
560    raw.cast::<T>()
561}
562
563impl Drop for ClapPlugin {
564    fn drop(&mut self) {
565        if !self.plugin.is_null() {
566            if self.gui_open
567                && let Some(destroy) = unsafe { (*self.gui_ext).destroy }
568            {
569                unsafe { destroy(self.plugin) };
570                self.gui_open = false;
571            }
572            if self.started_processing
573                && let Some(stop) = unsafe { (*self.plugin).stop_processing }
574            {
575                unsafe { stop(self.plugin) };
576            }
577            if self.active_layout.is_some()
578                && let Some(deactivate) = unsafe { (*self.plugin).deactivate }
579            {
580                unsafe { deactivate(self.plugin) };
581            }
582            if let Some(destroy) = unsafe { (*self.plugin).destroy } {
583                unsafe { destroy(self.plugin) };
584            }
585        }
586        if let Ok(entry) = self.library.entry() {
587            unsafe { entry.deinit() };
588        }
589        // `library` drops here, unloading the dylib.
590        let _ = &self.bundle_path; // keep PathBuf alive for diagnostics
591    }
592}
593
594impl PluginCore for ClapPlugin {
595    fn info(&self) -> &PluginInfo {
596        &self.info
597    }
598
599    fn active_layout(&self) -> Option<&BusLayout> {
600        self.active_layout.as_ref()
601    }
602
603    fn supported_layouts(&self) -> &[BusLayout] {
604        &self.layouts
605    }
606
607    fn parameter_count(&self) -> usize {
608        if self.params_ext.is_null() {
609            return 0;
610        }
611        let count = unsafe { (*self.params_ext).count };
612        count.map_or(0, |c| unsafe { c(self.plugin) as usize })
613    }
614
615    fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
616        if self.params_ext.is_null() {
617            return Err(Error::InvalidParameter(index));
618        }
619        let get_info =
620            unsafe { (*self.params_ext).get_info }.ok_or(Error::InvalidParameter(index))?;
621        let mut info = empty_param_info();
622        let idx_u32 = u32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
623        let ok = unsafe { get_info(self.plugin, idx_u32, &raw mut info) };
624        if !ok {
625            return Err(Error::InvalidParameter(index));
626        }
627        Ok(clap_param_info_to_rack(&info))
628    }
629
630    fn parameter_value(&self, index: usize) -> Result<f64> {
631        if self.params_ext.is_null() {
632            return Err(Error::InvalidParameter(index));
633        }
634        // Resolve index -> id via get_info, then ask get_value
635        // for the current value. CLAP's params API is id-keyed,
636        // not index-keyed.
637        let info = self.parameter_info(index)?;
638        let get_value =
639            unsafe { (*self.params_ext).get_value }.ok_or(Error::InvalidParameter(index))?;
640        let mut out = 0.0f64;
641        let ok = unsafe { get_value(self.plugin, info.id, &raw mut out) };
642        if !ok {
643            return Err(Error::InvalidParameter(index));
644        }
645        Ok(out)
646    }
647
648    fn parameter_value_string(&self, index: usize, value: f64) -> Result<String> {
649        if self.params_ext.is_null() {
650            return Err(Error::InvalidParameter(index));
651        }
652        let info = self.parameter_info(index)?;
653        let value_to_text =
654            unsafe { (*self.params_ext).value_to_text }.ok_or(Error::InvalidParameter(index))?;
655        let mut buf = [0i8; clap_sys::string_sizes::CLAP_NAME_SIZE];
656        let buf_len = u32::try_from(buf.len()).unwrap_or(u32::MAX);
657        let ok = unsafe { value_to_text(self.plugin, info.id, value, buf.as_mut_ptr(), buf_len) };
658        if !ok {
659            return Err(Error::InvalidParameter(index));
660        }
661        Ok(c_buf_to_string(&buf))
662    }
663
664    fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
665        if self.params_ext.is_null() {
666            return Err(Error::InvalidParameter(index));
667        }
668        let info = self.parameter_info(index)?;
669        // Queue the change either way; process() drains the queue
670        // every block, and a non-processing host can call flush()
671        // to apply it immediately.
672        self.pending_param_changes.push((info.id, value));
673        if !self.started_processing
674            && let Some(flush) = unsafe { (*self.params_ext).flush }
675        {
676            let events = std::mem::take(&mut self.pending_param_changes);
677            let mut converted = ConvertedInputEvents { events: Vec::new() };
678            for (param_id, value) in events {
679                converted.push_param_value(0, param_id, value);
680            }
681            let input = converted.as_clap();
682            let mut sink = OutputEventsSink::new(None);
683            let output = sink.as_clap();
684            unsafe { flush(self.plugin, &raw const input, &raw const output) };
685        }
686        Ok(())
687    }
688
689    fn preset_count(&self) -> usize {
690        0
691    }
692
693    fn preset_info(&self, index: usize) -> Result<PresetInfo> {
694        Err(Error::InvalidParameter(index))
695    }
696
697    fn load_preset(&mut self, _preset_number: i32) -> Result<()> {
698        Err(Error::Other("clap preset loading not yet wired".into()))
699    }
700
701    fn save_state(&self) -> Result<Vec<u8>> {
702        if self.state_ext.is_null() {
703            return Err(Error::Other("plugin missing clap.state extension".into()));
704        }
705        let save = unsafe { (*self.state_ext).save }
706            .ok_or_else(|| Error::Other("clap.state extension missing save fn".into()))?;
707        let mut buffer = WriteBuffer::default();
708        let stream = clap_ostream {
709            ctx: (&raw mut buffer).cast(),
710            write: Some(ostream_write),
711        };
712        let ok = unsafe { save(self.plugin, &raw const stream) };
713        if !ok {
714            return Err(Error::Other("clap state save returned false".into()));
715        }
716        Ok(buffer.bytes)
717    }
718
719    fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
720        if self.state_ext.is_null() {
721            return Err(Error::Other("plugin missing clap.state extension".into()));
722        }
723        let load = unsafe { (*self.state_ext).load }
724            .ok_or_else(|| Error::Other("clap.state extension missing load fn".into()))?;
725        let mut cursor = ReadCursor { bytes, position: 0 };
726        let stream = clap_istream {
727            ctx: (&raw mut cursor).cast(),
728            read: Some(istream_read),
729        };
730        let ok = unsafe { load(self.plugin, &raw const stream) };
731        if !ok {
732            return Err(Error::Other("clap state load returned false".into()));
733        }
734        Ok(())
735    }
736
737    fn activate(
738        &mut self,
739        layout: BusLayout,
740        sample_rate: f64,
741        max_block_size: usize,
742    ) -> Result<()> {
743        let Some(activate) = (unsafe { (*self.plugin).activate }) else {
744            return Err(Error::Other("clap plugin missing activate".into()));
745        };
746        let ok = unsafe {
747            activate(
748                self.plugin,
749                sample_rate,
750                1,
751                u32::try_from(max_block_size).unwrap_or(u32::MAX),
752            )
753        };
754        if !ok {
755            return Err(Error::Other("clap_plugin::activate returned false".into()));
756        }
757        self.active_layout = Some(layout);
758        Ok(())
759    }
760
761    fn deactivate(&mut self) {
762        if self.started_processing {
763            if let Some(stop) = unsafe { (*self.plugin).stop_processing } {
764                unsafe { stop(self.plugin) };
765            }
766            self.started_processing = false;
767        }
768        if let Some(deactivate) = unsafe { (*self.plugin).deactivate } {
769            unsafe { deactivate(self.plugin) };
770        }
771        self.active_layout = None;
772    }
773
774    fn is_active(&self) -> bool {
775        self.active_layout.is_some()
776    }
777
778    fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
779        if self.gui_ext.is_null() {
780            return None;
781        }
782        Some(self)
783    }
784}
785
786/// Platform API id we'd ask the plugin to support — one per
787/// build target.
788const fn platform_api() -> &'static CStr {
789    #[cfg(target_os = "macos")]
790    {
791        CLAP_WINDOW_API_COCOA
792    }
793    #[cfg(target_os = "windows")]
794    {
795        CLAP_WINDOW_API_WIN32
796    }
797    #[cfg(target_os = "linux")]
798    {
799        CLAP_WINDOW_API_X11
800    }
801    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
802    {
803        CLAP_WINDOW_API_COCOA
804    }
805}
806
807unsafe fn gui_supports_current_platform(
808    plugin: *const clap_plugin,
809    gui_ext: *const clap_plugin_gui,
810) -> bool {
811    let Some(is_supported) = (unsafe { (*gui_ext).is_api_supported }) else {
812        return false;
813    };
814    unsafe { is_supported(plugin, platform_api().as_ptr(), false) }
815}
816
817fn handle_to_clap_window(handle: truce_rack_core::editor::WindowHandle) -> clap_window {
818    use truce_rack_core::editor::WindowHandle;
819    let (api, specific) = match handle {
820        WindowHandle::NSView(p) => (
821            CLAP_WINDOW_API_COCOA.as_ptr(),
822            clap_window_handle { cocoa: p },
823        ),
824        WindowHandle::HWND(p) => (
825            CLAP_WINDOW_API_WIN32.as_ptr(),
826            clap_window_handle { win32: p },
827        ),
828        WindowHandle::X11(id) => (
829            CLAP_WINDOW_API_X11.as_ptr(),
830            clap_window_handle { x11: id as _ },
831        ),
832    };
833    clap_window { api, specific }
834}
835
836impl truce_rack_core::editor::PluginEditor for ClapPlugin {
837    fn open(
838        &mut self,
839        parent: truce_rack_core::editor::WindowHandle,
840        scale: f64,
841    ) -> truce_rack_core::error::Result<()> {
842        if self.gui_open {
843            return Ok(());
844        }
845        if self.gui_ext.is_null() {
846            return Err(Error::Other("clap.gui extension absent".into()));
847        }
848        let api = platform_api();
849        let create = unsafe { (*self.gui_ext).create }
850            .ok_or_else(|| Error::Other("clap.gui missing `create`".into()))?;
851        if !unsafe { create(self.plugin, api.as_ptr(), false) } {
852            return Err(Error::Other("clap.gui::create returned false".into()));
853        }
854        if let Some(set_scale) = unsafe { (*self.gui_ext).set_scale } {
855            let _ = unsafe { set_scale(self.plugin, scale) };
856        }
857        let window = handle_to_clap_window(parent);
858        if let Some(set_parent) = unsafe { (*self.gui_ext).set_parent }
859            && !unsafe { set_parent(self.plugin, &raw const window) }
860        {
861            if let Some(destroy) = unsafe { (*self.gui_ext).destroy } {
862                unsafe { destroy(self.plugin) };
863            }
864            return Err(Error::Other("clap.gui::set_parent returned false".into()));
865        }
866        if let Some(show) = unsafe { (*self.gui_ext).show } {
867            let _ = unsafe { show(self.plugin) };
868        }
869        self.gui_open = true;
870        Ok(())
871    }
872
873    fn close(&mut self) {
874        if !self.gui_open {
875            return;
876        }
877        if let Some(hide) = unsafe { (*self.gui_ext).hide } {
878            let _ = unsafe { hide(self.plugin) };
879        }
880        if let Some(destroy) = unsafe { (*self.gui_ext).destroy } {
881            unsafe { destroy(self.plugin) };
882        }
883        self.gui_open = false;
884    }
885
886    fn is_open(&self) -> bool {
887        self.gui_open
888    }
889
890    fn size(&self) -> Option<(u32, u32)> {
891        if !self.gui_open {
892            return None;
893        }
894        let get_size = unsafe { (*self.gui_ext).get_size }?;
895        let mut w: u32 = 0;
896        let mut h: u32 = 0;
897        if unsafe { get_size(self.plugin, &raw mut w, &raw mut h) } {
898            Some((w, h))
899        } else {
900            None
901        }
902    }
903
904    fn is_resizable(&self) -> bool {
905        if !self.gui_open {
906            return false;
907        }
908        unsafe { (*self.gui_ext).can_resize }.is_some_and(|f| unsafe { f(self.plugin) })
909    }
910
911    fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
912        if !self.gui_open {
913            return None;
914        }
915        let mut w = width;
916        let mut h = height;
917        if let Some(adjust) = unsafe { (*self.gui_ext).adjust_size } {
918            let _ = unsafe { adjust(self.plugin, &raw mut w, &raw mut h) };
919        }
920        let set = unsafe { (*self.gui_ext).set_size }?;
921        if unsafe { set(self.plugin, w, h) } {
922            Some((w, h))
923        } else {
924            None
925        }
926    }
927
928    fn show(&mut self) {
929        if let Some(show) = unsafe { (*self.gui_ext).show } {
930            let _ = unsafe { show(self.plugin) };
931        }
932    }
933
934    fn hide(&mut self) {
935        if let Some(hide) = unsafe { (*self.gui_ext).hide } {
936            let _ = unsafe { hide(self.plugin) };
937        }
938    }
939}
940
941impl Plugin<f32> for ClapPlugin {
942    fn process(
943        &mut self,
944        buffer: &mut AudioBuffer<'_, f32>,
945        events: &EventList,
946        context: &mut ProcessContext<'_>,
947    ) -> Result<ProcessStatus> {
948        if !self.is_active() {
949            return Err(Error::NotActivated);
950        }
951        if !self.started_processing {
952            if let Some(start) = unsafe { (*self.plugin).start_processing } {
953                let ok = unsafe { start(self.plugin) };
954                if !ok {
955                    return Err(Error::Other(
956                        "clap_plugin::start_processing returned false".into(),
957                    ));
958                }
959            }
960            self.started_processing = true;
961        }
962
963        // Translate the truce-rack event list into CLAP's typed event
964        // unions. Plus any pending parameter changes from
965        // set_parameter. The resulting Vec backs the
966        // ConvertedInputEvents callbacks for the duration of the
967        // process call.
968        // Translate the host transport snapshot up front so its
969        // backing struct lives for the whole process call.
970        let transport_event = context
971            .transport
972            .map(|t| build_clap_transport(&t, context.sample_rate));
973
974        let mut converted = ConvertedInputEvents::from_rack_events(events);
975        for (param_id, value) in self.pending_param_changes.drain(..) {
976            converted.push_param_value(0, param_id, value);
977        }
978        let input_events = converted.as_clap();
979        let mut sink = OutputEventsSink::new(Some(context.output_events));
980        let output_events = sink.as_clap();
981
982        // Build per-channel pointer arrays for clap_audio_buffer.
983        // These are constructed inline so they live for the
984        // duration of the process call.
985        let num_frames = buffer.num_frames();
986        let main_inputs = buffer.main_inputs();
987        let mut input_ptrs: Vec<*mut f32> = main_inputs
988            .iter()
989            .map(|chan| chan.as_ptr().cast_mut())
990            .collect();
991        let input_audio = clap_audio_buffer {
992            data32: input_ptrs.as_mut_ptr(),
993            data64: ptr::null_mut(),
994            channel_count: u32::try_from(input_ptrs.len()).unwrap_or(0),
995            latency: 0,
996            constant_mask: 0,
997        };
998
999        let main_outputs = buffer.main_outputs();
1000        let mut output_ptrs: Vec<*mut f32> = main_outputs
1001            .iter_mut()
1002            .map(|chan| chan.as_mut_ptr())
1003            .collect();
1004        let mut output_audio = clap_audio_buffer {
1005            data32: output_ptrs.as_mut_ptr(),
1006            data64: ptr::null_mut(),
1007            channel_count: u32::try_from(output_ptrs.len()).unwrap_or(0),
1008            latency: 0,
1009            constant_mask: 0,
1010        };
1011
1012        let process = clap_process {
1013            steady_time: self.steady_time,
1014            frames_count: u32::try_from(num_frames).unwrap_or(u32::MAX),
1015            transport: transport_event
1016                .as_ref()
1017                .map_or(ptr::null(), std::ptr::from_ref),
1018            audio_inputs: if input_ptrs.is_empty() {
1019                ptr::null()
1020            } else {
1021                &raw const input_audio
1022            },
1023            audio_outputs: if output_ptrs.is_empty() {
1024                ptr::null_mut()
1025            } else {
1026                &raw mut output_audio
1027            },
1028            audio_inputs_count: u32::from(!input_ptrs.is_empty()),
1029            audio_outputs_count: u32::from(!output_ptrs.is_empty()),
1030            in_events: &raw const input_events,
1031            out_events: &raw const output_events,
1032        };
1033
1034        let plugin = self.plugin;
1035        let process_ptr = unsafe { (*plugin).process };
1036        let status = match process_ptr {
1037            Some(process_fn) => {
1038                run_audio_block_with::<ClapPlugin, i32>(FORMAT, CLAP_PROCESS_ERROR, || unsafe {
1039                    process_fn(plugin, &raw const process)
1040                })
1041            }
1042            None => CLAP_PROCESS_ERROR,
1043        };
1044
1045        self.steady_time = self
1046            .steady_time
1047            .saturating_add(i64::try_from(num_frames).unwrap_or(0));
1048
1049        Ok(map_clap_status(status))
1050    }
1051}
1052
1053/// Translate a host [`TransportInfo`] snapshot into CLAP's
1054/// `clap_event_transport`. `sample_rate` converts the sample
1055/// position into the seconds timeline CLAP also wants.
1056///
1057/// CLAP beat / second times are 31.32 fixed-point (see
1058/// `CLAP_BEATTIME_FACTOR`); beats are quarter notes.
1059#[allow(
1060    clippy::cast_precision_loss,
1061    clippy::cast_possible_truncation,
1062    clippy::cast_possible_wrap
1063)]
1064fn build_clap_transport(t: &TransportInfo, sample_rate: f64) -> clap_event_transport {
1065    let beats_to_fixed = |b: f64| (b * CLAP_BEATTIME_FACTOR as f64).round() as i64;
1066    let secs_to_fixed = |s: f64| (s * CLAP_SECTIME_FACTOR as f64).round() as i64;
1067
1068    let mut flags: clap_transport_flags = 0;
1069    let tempo = t.tempo_bpm.unwrap_or(0.0);
1070    if t.tempo_bpm.is_some() {
1071        flags |= CLAP_TRANSPORT_HAS_TEMPO;
1072    }
1073    let song_pos_beats = match t.song_position_beats {
1074        Some(b) => {
1075            flags |= CLAP_TRANSPORT_HAS_BEATS_TIMELINE;
1076            beats_to_fixed(b)
1077        }
1078        None => 0,
1079    };
1080    let song_pos_seconds = match t.song_position_samples {
1081        Some(s) => {
1082            flags |= CLAP_TRANSPORT_HAS_SECONDS_TIMELINE;
1083            secs_to_fixed(s as f64 / sample_rate.max(1.0))
1084        }
1085        None => 0,
1086    };
1087    let (tsig_num, tsig_denom) = match t.time_signature {
1088        Some((n, d)) => {
1089            flags |= CLAP_TRANSPORT_HAS_TIME_SIGNATURE;
1090            (n as u16, d as u16)
1091        }
1092        None => (0, 0),
1093    };
1094    let bar_start = t.bar_start_beats.map_or(0, beats_to_fixed);
1095    // Bar index: bar_start (in quarter notes) divided by the bar
1096    // length the time signature implies.
1097    let bar_number = match (t.bar_start_beats, t.time_signature) {
1098        (Some(bsb), Some((n, d))) => {
1099            let beats_per_bar = f64::from(n) * 4.0 / f64::from(d.max(1));
1100            (bsb / beats_per_bar.max(f64::EPSILON)).round() as i32
1101        }
1102        _ => 0,
1103    };
1104    if t.playing {
1105        flags |= CLAP_TRANSPORT_IS_PLAYING;
1106    }
1107    if t.recording {
1108        flags |= CLAP_TRANSPORT_IS_RECORDING;
1109    }
1110    if t.loop_active {
1111        flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE;
1112    }
1113
1114    clap_event_transport {
1115        header: clap_event_header {
1116            size: u32::try_from(std::mem::size_of::<clap_event_transport>()).unwrap_or(0),
1117            time: 0,
1118            space_id: CLAP_CORE_EVENT_SPACE_ID,
1119            type_: CLAP_EVENT_TRANSPORT,
1120            flags: 0,
1121        },
1122        flags,
1123        song_pos_beats,
1124        song_pos_seconds,
1125        tempo,
1126        tempo_inc: 0.0,
1127        loop_start_beats: 0,
1128        loop_end_beats: 0,
1129        loop_start_seconds: 0,
1130        loop_end_seconds: 0,
1131        bar_start,
1132        bar_number,
1133        tsig_num,
1134        tsig_denom,
1135    }
1136}
1137
1138fn map_clap_status(status: i32) -> ProcessStatus {
1139    match status {
1140        CLAP_PROCESS_CONTINUE | CLAP_PROCESS_CONTINUE_IF_NOT_QUIET => ProcessStatus::Continue,
1141        CLAP_PROCESS_SLEEP => ProcessStatus::Sleep,
1142        CLAP_PROCESS_TAIL => ProcessStatus::Tail { tail_samples: 0 },
1143        _ => ProcessStatus::Error,
1144    }
1145}
1146
1147fn make_param_value_event(sample_offset: u32, param_id: u32, value: f64) -> clap_event_param_value {
1148    clap_event_param_value {
1149        header: clap_event_header {
1150            size: u32::try_from(std::mem::size_of::<clap_event_param_value>()).unwrap_or(0),
1151            time: sample_offset,
1152            space_id: CLAP_CORE_EVENT_SPACE_ID,
1153            type_: CLAP_EVENT_PARAM_VALUE,
1154            flags: 0,
1155        },
1156        param_id,
1157        cookie: ptr::null_mut(),
1158        note_id: -1,
1159        port_index: -1,
1160        channel: -1,
1161        key: -1,
1162        value,
1163    }
1164}
1165
1166fn make_note_event(
1167    sample_offset: u32,
1168    event_type: u16,
1169    channel: u8,
1170    key: u8,
1171    velocity: f64,
1172) -> clap_event_note {
1173    clap_event_note {
1174        header: clap_event_header {
1175            size: u32::try_from(std::mem::size_of::<clap_event_note>()).unwrap_or(0),
1176            time: sample_offset,
1177            space_id: CLAP_CORE_EVENT_SPACE_ID,
1178            type_: event_type,
1179            flags: 0,
1180        },
1181        note_id: -1,
1182        port_index: -1,
1183        channel: i16::from(channel),
1184        key: i16::from(key),
1185        velocity,
1186    }
1187}
1188
1189fn make_midi_event(sample_offset: u32, bytes: [u8; 3]) -> clap_event_midi {
1190    clap_event_midi {
1191        header: clap_event_header {
1192            size: u32::try_from(std::mem::size_of::<clap_event_midi>()).unwrap_or(0),
1193            time: sample_offset,
1194            space_id: CLAP_CORE_EVENT_SPACE_ID,
1195            type_: CLAP_EVENT_MIDI,
1196            flags: 0,
1197        },
1198        port_index: 0,
1199        data: bytes,
1200    }
1201}
1202
1203/// Sink for `clap_plugin_state::save` — the plugin pushes bytes
1204/// via the C `write` callback, we accumulate into a Vec.
1205#[derive(Default)]
1206struct WriteBuffer {
1207    bytes: Vec<u8>,
1208}
1209
1210unsafe extern "C" fn ostream_write(
1211    stream: *const clap_ostream,
1212    buffer: *const std::ffi::c_void,
1213    size: u64,
1214) -> i64 {
1215    if stream.is_null() || buffer.is_null() {
1216        return -1;
1217    }
1218    let ctx = unsafe { (*stream).ctx.cast::<WriteBuffer>() };
1219    if ctx.is_null() {
1220        return -1;
1221    }
1222    let Ok(size_usize) = usize::try_from(size) else {
1223        return -1;
1224    };
1225    let slice = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), size_usize) };
1226    unsafe { (*ctx).bytes.extend_from_slice(slice) };
1227    i64::try_from(size_usize).unwrap_or(i64::MAX)
1228}
1229
1230/// Source for `clap_plugin_state::load` — the plugin pulls bytes
1231/// via the C `read` callback, we hand back from a `&[u8]`.
1232struct ReadCursor<'a> {
1233    bytes: &'a [u8],
1234    position: usize,
1235}
1236
1237unsafe extern "C" fn istream_read(
1238    stream: *const clap_istream,
1239    buffer: *mut std::ffi::c_void,
1240    size: u64,
1241) -> i64 {
1242    if stream.is_null() || buffer.is_null() {
1243        return -1;
1244    }
1245    let ctx = unsafe { (*stream).ctx.cast::<ReadCursor<'_>>() };
1246    if ctx.is_null() {
1247        return -1;
1248    }
1249    let cursor = unsafe { &mut *ctx };
1250    let Ok(want) = usize::try_from(size) else {
1251        return -1;
1252    };
1253    let available = cursor.bytes.len().saturating_sub(cursor.position);
1254    let take = want.min(available);
1255    if take > 0 {
1256        unsafe {
1257            std::ptr::copy_nonoverlapping(
1258                cursor.bytes.as_ptr().add(cursor.position),
1259                buffer.cast::<u8>(),
1260                take,
1261            );
1262        }
1263        cursor.position += take;
1264    }
1265    i64::try_from(take).unwrap_or(i64::MAX)
1266}
1267
1268/// Owned storage for one block's CLAP-formatted input events.
1269///
1270/// We can't ship `*const clap_event_header` directly off our
1271/// `EventList` because the truce-rack event payloads have different
1272/// layouts than CLAP's. The fix is to translate up front and keep
1273/// the result alive for the `process` call.
1274struct ConvertedInputEvents {
1275    /// Owned event memory — every entry is a CLAP event struct
1276    /// the input-events vtable hands out pointers into. Boxed so
1277    /// addresses survive `Vec` reallocation.
1278    events: Vec<EventStorage>,
1279}
1280
1281#[allow(dead_code)]
1282enum EventStorage {
1283    Param(clap_event_param_value),
1284    Note(clap_event_note),
1285    Midi(clap_event_midi),
1286}
1287
1288impl EventStorage {
1289    fn header(&self) -> *const clap_event_header {
1290        match self {
1291            Self::Param(e) => &raw const e.header,
1292            Self::Note(e) => &raw const e.header,
1293            Self::Midi(e) => &raw const e.header,
1294        }
1295    }
1296}
1297
1298impl ConvertedInputEvents {
1299    fn from_rack_events(list: &EventList) -> Self {
1300        let mut out = Self { events: Vec::new() };
1301        for event in list {
1302            out.push_rack(event);
1303        }
1304        out
1305    }
1306
1307    fn push_param_value(&mut self, sample_offset: u32, param_id: u32, value: f64) {
1308        self.events.push(EventStorage::Param(make_param_value_event(
1309            sample_offset,
1310            param_id,
1311            value,
1312        )));
1313    }
1314
1315    fn push_rack(&mut self, event: &truce_rack_core::events::Event) {
1316        use truce_rack_core::events::EventBody;
1317        let offset = event.sample_offset;
1318        match event.body {
1319            EventBody::Midi(midi) => self.push_midi(offset, midi),
1320            EventBody::ParamValue { param_id, value } => {
1321                self.push_param_value(offset, param_id, value);
1322            }
1323            EventBody::ParamGesture { .. } | EventBody::TransportFlag(_) => {
1324                // ParamGesture: CLAP's begin/end events have
1325                // separate types; covered in a follow-on once
1326                // hosts start emitting them.
1327                // TransportFlag: routed through clap_event_transport
1328                // on the process struct, not via input events.
1329            }
1330        }
1331    }
1332
1333    fn push_midi(&mut self, offset: u32, midi: truce_rack_core::events::MidiData) {
1334        use truce_rack_core::events::MidiData;
1335        match midi {
1336            MidiData::NoteOn {
1337                channel,
1338                note,
1339                velocity,
1340            } => {
1341                self.events.push(EventStorage::Note(make_note_event(
1342                    offset,
1343                    CLAP_EVENT_NOTE_ON,
1344                    channel,
1345                    note,
1346                    f64::from(velocity) / 127.0,
1347                )));
1348            }
1349            MidiData::NoteOff {
1350                channel,
1351                note,
1352                velocity,
1353            } => {
1354                self.events.push(EventStorage::Note(make_note_event(
1355                    offset,
1356                    CLAP_EVENT_NOTE_OFF,
1357                    channel,
1358                    note,
1359                    f64::from(velocity) / 127.0,
1360                )));
1361            }
1362            MidiData::ControlChange {
1363                channel,
1364                controller,
1365                value,
1366            } => {
1367                let status = 0xB0 | (channel & 0x0F);
1368                self.events.push(EventStorage::Midi(make_midi_event(
1369                    offset,
1370                    [status, controller & 0x7F, value & 0x7F],
1371                )));
1372            }
1373            MidiData::ProgramChange { channel, program } => {
1374                let status = 0xC0 | (channel & 0x0F);
1375                self.events.push(EventStorage::Midi(make_midi_event(
1376                    offset,
1377                    [status, program & 0x7F, 0],
1378                )));
1379            }
1380            MidiData::PolyAftertouch {
1381                channel,
1382                note,
1383                pressure,
1384            } => {
1385                let status = 0xA0 | (channel & 0x0F);
1386                self.events.push(EventStorage::Midi(make_midi_event(
1387                    offset,
1388                    [status, note & 0x7F, pressure & 0x7F],
1389                )));
1390            }
1391            MidiData::ChannelAftertouch { channel, pressure } => {
1392                let status = 0xD0 | (channel & 0x0F);
1393                self.events.push(EventStorage::Midi(make_midi_event(
1394                    offset,
1395                    [status, pressure & 0x7F, 0],
1396                )));
1397            }
1398            MidiData::PitchBend { channel, value } => {
1399                let status = 0xE0 | (channel & 0x0F);
1400                let lsb = u8::try_from(value & 0x7F).unwrap_or(0);
1401                let msb = u8::try_from((value >> 7) & 0x7F).unwrap_or(0);
1402                self.events.push(EventStorage::Midi(make_midi_event(
1403                    offset,
1404                    [status, lsb, msb],
1405                )));
1406            }
1407            MidiData::Raw { len, data } => {
1408                if len >= 3 {
1409                    self.events.push(EventStorage::Midi(make_midi_event(
1410                        offset,
1411                        [data[0], data[1], data[2]],
1412                    )));
1413                }
1414            }
1415        }
1416    }
1417
1418    fn as_clap(&self) -> clap_input_events {
1419        clap_input_events {
1420            ctx: std::ptr::from_ref::<Self>(self)
1421                .cast::<std::ffi::c_void>()
1422                .cast_mut(),
1423            size: Some(input_events_size),
1424            get: Some(input_events_get),
1425        }
1426    }
1427}
1428
1429unsafe extern "C" fn input_events_size(list: *const clap_input_events) -> u32 {
1430    let ctx = unsafe { (*list).ctx.cast::<ConvertedInputEvents>() };
1431    if ctx.is_null() {
1432        return 0;
1433    }
1434    u32::try_from(unsafe { (*ctx).events.len() }).unwrap_or(u32::MAX)
1435}
1436
1437unsafe extern "C" fn input_events_get(
1438    list: *const clap_input_events,
1439    index: u32,
1440) -> *const clap_event_header {
1441    let ctx = unsafe { (*list).ctx.cast::<ConvertedInputEvents>() };
1442    if ctx.is_null() {
1443        return ptr::null();
1444    }
1445    let idx = index as usize;
1446    let events = unsafe { &(*ctx).events };
1447    events.get(idx).map_or(ptr::null(), EventStorage::header)
1448}
1449
1450/// Sink for events the plugin emits during `process`.
1451struct OutputEventsSink<'a> {
1452    target: Option<&'a mut EventList>,
1453}
1454
1455impl<'a> OutputEventsSink<'a> {
1456    fn new(target: Option<&'a mut EventList>) -> Self {
1457        Self { target }
1458    }
1459
1460    fn as_clap(&mut self) -> clap_output_events {
1461        clap_output_events {
1462            ctx: std::ptr::from_mut::<Self>(self).cast::<std::ffi::c_void>(),
1463            try_push: Some(output_events_try_push),
1464        }
1465    }
1466}
1467
1468unsafe extern "C" fn output_events_try_push(
1469    list: *const clap_output_events,
1470    event: *const clap_event_header,
1471) -> bool {
1472    if event.is_null() {
1473        return false;
1474    }
1475    let header = unsafe { &*event };
1476    let ctx = unsafe { (*list).ctx.cast::<OutputEventsSink<'_>>() };
1477    if ctx.is_null() {
1478        return true;
1479    }
1480    let target = unsafe { (*ctx).target.as_deref_mut() };
1481    let Some(target) = target else {
1482        // Plugin wanted to send an event; the host doesn't care
1483        // (Sink was constructed with `target: None`). Returning
1484        // true tells the plugin we accepted it.
1485        return true;
1486    };
1487    if let Some(rack_event) = unsafe { clap_event_to_rack(header) } {
1488        target.push(rack_event);
1489    }
1490    true
1491}
1492
1493unsafe fn clap_event_to_rack(header: &clap_event_header) -> Option<truce_rack_core::events::Event> {
1494    use truce_rack_core::events::{Event, EventBody, MidiData};
1495    if header.space_id != CLAP_CORE_EVENT_SPACE_ID {
1496        return None;
1497    }
1498    let offset = header.time;
1499    match header.type_ {
1500        t if t == CLAP_EVENT_PARAM_VALUE => {
1501            let e: &clap_event_param_value =
1502                unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1503            Some(Event {
1504                sample_offset: offset,
1505                body: EventBody::ParamValue {
1506                    param_id: e.param_id,
1507                    value: e.value,
1508                },
1509            })
1510        }
1511        t if t == CLAP_EVENT_NOTE_ON => {
1512            let e: &clap_event_note =
1513                unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1514            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1515            let velocity = (e.velocity * 127.0).round().clamp(0.0, 127.0) as u8;
1516            Some(Event {
1517                sample_offset: offset,
1518                body: EventBody::Midi(MidiData::NoteOn {
1519                    channel: u8::try_from(e.channel.max(0)).unwrap_or(0),
1520                    note: u8::try_from(e.key.max(0)).unwrap_or(0),
1521                    velocity,
1522                }),
1523            })
1524        }
1525        t if t == CLAP_EVENT_NOTE_OFF => {
1526            let e: &clap_event_note =
1527                unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1528            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1529            let velocity = (e.velocity * 127.0).round().clamp(0.0, 127.0) as u8;
1530            Some(Event {
1531                sample_offset: offset,
1532                body: EventBody::Midi(MidiData::NoteOff {
1533                    channel: u8::try_from(e.channel.max(0)).unwrap_or(0),
1534                    note: u8::try_from(e.key.max(0)).unwrap_or(0),
1535                    velocity,
1536                }),
1537            })
1538        }
1539        t if t == CLAP_EVENT_MIDI => {
1540            let e: &clap_event_midi =
1541                unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1542            Some(Event {
1543                sample_offset: offset,
1544                body: EventBody::Midi(MidiData::Raw {
1545                    len: 3,
1546                    data: [e.data[0], e.data[1], e.data[2], 0, 0, 0, 0, 0],
1547                }),
1548            })
1549        }
1550        _ => None,
1551    }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556    use super::*;
1557
1558    #[test]
1559    fn parse_version_components() {
1560        assert_eq!(parse_version("1.2.3"), (1 << 16) | (2 << 8) | 3);
1561        assert_eq!(parse_version("0.5"), 5 << 8);
1562        assert_eq!(parse_version(""), 0);
1563    }
1564
1565    #[test]
1566    fn bundle_binary_macos() {
1567        let p = bundle_binary_path(Path::new("/tmp/MyPlugin.clap"));
1568        #[cfg(target_os = "macos")]
1569        assert!(!p.exists() || p.starts_with("/tmp/MyPlugin.clap"));
1570        #[cfg(not(target_os = "macos"))]
1571        assert_eq!(p, Path::new("/tmp/MyPlugin.clap"));
1572    }
1573}