Skip to main content

maolan_engine/plugins/
clap.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::MidiEvent;
3use crate::mutex::UnsafeMutex;
4#[cfg(any(
5    target_os = "macos",
6    target_os = "linux",
7    target_os = "freebsd",
8    target_os = "openbsd"
9))]
10use crate::plugins::paths;
11use libloading::Library;
12use serde::{Deserialize, Serialize};
13use std::cell::Cell;
14use std::collections::HashMap;
15use std::ffi::{CStr, CString, c_char, c_void};
16use std::fmt;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU32, Ordering};
20use std::time::{Duration, Instant};
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct ClapParameterInfo {
24    pub id: u32,
25    pub name: String,
26    pub module: String,
27    pub min_value: f64,
28    pub max_value: f64,
29    pub default_value: f64,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ClapPluginState {
34    pub bytes: Vec<u8>,
35}
36
37type AudioPortLayout = (Vec<usize>, usize);
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ClapMidiOutputEvent {
41    pub port: usize,
42    pub event: MidiEvent,
43}
44
45#[derive(Clone, Copy, Debug, Default)]
46pub struct ClapTransportInfo {
47    pub transport_sample: usize,
48    pub playing: bool,
49    pub loop_enabled: bool,
50    pub loop_range_samples: Option<(usize, usize)>,
51    pub bpm: f64,
52    pub tsig_num: u16,
53    pub tsig_denom: u16,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct ClapGuiInfo {
58    pub api: String,
59    pub supports_embedded: bool,
60}
61
62#[derive(Clone, Copy, Debug)]
63struct PendingParamValue {
64    param_id: u32,
65    value: f64,
66}
67
68#[derive(Clone, Copy, Debug)]
69pub struct ClapParamUpdate {
70    pub param_id: u32,
71    pub value: f64,
72}
73
74#[derive(Clone, Copy, Debug)]
75enum PendingParamEvent {
76    Value {
77        param_id: u32,
78        value: f64,
79        frame: u32,
80    },
81    GestureBegin {
82        param_id: u32,
83        frame: u32,
84    },
85    GestureEnd {
86        param_id: u32,
87        frame: u32,
88    },
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ClapPluginInfo {
93    pub name: String,
94    pub path: String,
95    pub capabilities: Option<ClapPluginCapabilities>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct ClapPluginCapabilities {
100    pub has_gui: bool,
101    pub gui_apis: Vec<String>,
102    pub supports_embedded: bool,
103    pub supports_floating: bool,
104    pub has_params: bool,
105    pub has_state: bool,
106    pub audio_inputs: usize,
107    pub audio_outputs: usize,
108    pub midi_inputs: usize,
109    pub midi_outputs: usize,
110}
111
112#[derive(Clone)]
113pub struct ClapProcessor {
114    path: String,
115    plugin_id: String,
116    name: String,
117    sample_rate: f64,
118    audio_inputs: Vec<Arc<AudioIO>>,
119    audio_outputs: Vec<Arc<AudioIO>>,
120    input_port_channels: Vec<usize>,
121    output_port_channels: Vec<usize>,
122    midi_input_ports: usize,
123    midi_output_ports: usize,
124    main_audio_inputs: usize,
125    main_audio_outputs: usize,
126    host_runtime: Arc<HostRuntime>,
127    plugin_handle: Arc<PluginHandle>,
128    param_infos: Arc<Vec<ClapParameterInfo>>,
129    param_values: Arc<UnsafeMutex<HashMap<u32, f64>>>,
130    pending_param_events: Arc<UnsafeMutex<Vec<PendingParamEvent>>>,
131    pending_param_events_ui: Arc<UnsafeMutex<Vec<PendingParamEvent>>>,
132    process_lock: Arc<UnsafeMutex<()>>,
133}
134
135impl fmt::Debug for ClapProcessor {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("ClapProcessor")
138            .field("path", &self.path)
139            .field("plugin_id", &self.plugin_id)
140            .field("name", &self.name)
141            .field("audio_inputs", &self.audio_inputs.len())
142            .field("audio_outputs", &self.audio_outputs.len())
143            .field("input_port_channels", &self.input_port_channels)
144            .field("output_port_channels", &self.output_port_channels)
145            .field("midi_input_ports", &self.midi_input_ports)
146            .field("midi_output_ports", &self.midi_output_ports)
147            .field("main_audio_inputs", &self.main_audio_inputs)
148            .field("main_audio_outputs", &self.main_audio_outputs)
149            .finish()
150    }
151}
152
153impl ClapProcessor {
154    pub fn new(
155        sample_rate: f64,
156        buffer_size: usize,
157        plugin_spec: &str,
158        input_count: usize,
159        output_count: usize,
160    ) -> Result<Self, String> {
161        let _thread_scope = HostThreadScope::enter_main();
162        let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
163        let host_runtime = Arc::new(HostRuntime::new()?);
164        let plugin_handle = Arc::new(PluginHandle::load(
165            plugin_path,
166            plugin_id,
167            host_runtime.clone(),
168            sample_rate,
169            buffer_size as u32,
170        )?);
171        let (input_layout_opt, output_layout_opt) = plugin_handle.audio_port_channels();
172        let input_port_channels_opt = input_layout_opt.as_ref().map(|(c, _)| c.clone());
173        let output_port_channels_opt = output_layout_opt.as_ref().map(|(c, _)| c.clone());
174        let discovered_inputs = input_layout_opt.as_ref().map(|(c, _)| c.len());
175        let discovered_outputs = output_layout_opt.as_ref().map(|(c, _)| c.len());
176        let (discovered_midi_inputs, discovered_midi_outputs) = plugin_handle.note_port_layout();
177        let resolved_inputs = discovered_inputs.unwrap_or(input_count);
178        let resolved_outputs = discovered_outputs.unwrap_or(output_count);
179        let main_audio_inputs = input_layout_opt
180            .as_ref()
181            .map(|(_, main)| *main)
182            .unwrap_or(input_count);
183        let main_audio_outputs = output_layout_opt
184            .as_ref()
185            .map(|(_, main)| *main)
186            .unwrap_or(output_count);
187        let audio_inputs = (0..resolved_inputs)
188            .map(|_| Arc::new(AudioIO::new(buffer_size)))
189            .collect();
190        let audio_outputs = (0..resolved_outputs)
191            .map(|_| Arc::new(AudioIO::new(buffer_size)))
192            .collect();
193        let param_infos = Arc::new(plugin_handle.parameter_infos());
194        let param_values = Arc::new(UnsafeMutex::new(
195            plugin_handle.parameter_values(&param_infos),
196        ));
197        Ok(Self {
198            path: plugin_spec.to_string(),
199            plugin_id: plugin_handle.plugin_id().to_string(),
200            name: plugin_handle.plugin_name().to_string(),
201            sample_rate,
202            audio_inputs,
203            audio_outputs,
204            input_port_channels: input_port_channels_opt
205                .unwrap_or_else(|| vec![1; resolved_inputs]),
206            output_port_channels: output_port_channels_opt
207                .unwrap_or_else(|| vec![1; resolved_outputs]),
208            midi_input_ports: discovered_midi_inputs.unwrap_or(0),
209            midi_output_ports: discovered_midi_outputs.unwrap_or(0),
210            main_audio_inputs,
211            main_audio_outputs,
212            host_runtime,
213            plugin_handle,
214            param_infos,
215            param_values,
216            pending_param_events: Arc::new(UnsafeMutex::new(Vec::new())),
217            pending_param_events_ui: Arc::new(UnsafeMutex::new(Vec::new())),
218            process_lock: Arc::new(UnsafeMutex::new(())),
219        })
220    }
221
222    pub fn setup_audio_ports(&self) {
223        for port in &self.audio_inputs {
224            port.setup();
225        }
226        for port in &self.audio_outputs {
227            port.setup();
228        }
229    }
230
231    pub fn process_with_audio_io(&self, frames: usize) {
232        let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
233    }
234
235    pub fn process_with_midi(
236        &self,
237        frames: usize,
238        midi_in: &[MidiEvent],
239        transport: ClapTransportInfo,
240    ) -> Vec<ClapMidiOutputEvent> {
241        // CLAP processors are not guaranteed to be re-entrant. Serialize
242        // processing per instance to avoid concurrent mutation of plugin state.
243        let _process_guard = self.process_lock.lock();
244        let started = Instant::now();
245        for port in &self.audio_inputs {
246            if port.ready() {
247                port.process();
248            }
249        }
250        let (processed, processed_midi) = match self.process_native(frames, midi_in, transport) {
251            Ok(ok) => ok,
252            Err(err) => {
253                tracing::warn!(
254                    "CLAP process failed for '{}' ({}): {}",
255                    self.name,
256                    self.path,
257                    err
258                );
259                (false, Vec::new())
260            }
261        };
262        let elapsed = started.elapsed();
263        if elapsed > Duration::from_millis(20) {
264            tracing::warn!(
265                "Slow CLAP process '{}' ({}) took {:.3} ms for {} frames",
266                self.name,
267                self.path,
268                elapsed.as_secs_f64() * 1000.0,
269                frames
270            );
271        }
272        if !processed {
273            for out in &self.audio_outputs {
274                let out_buf = out.buffer.lock();
275                out_buf.fill(0.0);
276                *out.finished.lock() = true;
277            }
278        }
279        processed_midi
280    }
281
282    pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
283        self.param_infos.as_ref().clone()
284    }
285
286    pub fn parameter_values(&self) -> HashMap<u32, f64> {
287        self.param_values.lock().clone()
288    }
289
290    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
291        self.set_parameter_at(param_id, value, 0)
292    }
293
294    pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
295        let _thread_scope = HostThreadScope::enter_main();
296        let Some(info) = self.param_infos.iter().find(|p| p.id == param_id) else {
297            return Err(format!("Unknown CLAP parameter id: {param_id}"));
298        };
299        let clamped = value.clamp(info.min_value, info.max_value);
300        self.pending_param_events
301            .lock()
302            .push(PendingParamEvent::Value {
303                param_id,
304                value: clamped,
305                frame,
306            });
307        self.pending_param_events_ui
308            .lock()
309            .push(PendingParamEvent::Value {
310                param_id,
311                value: clamped,
312                frame,
313            });
314        self.param_values.lock().insert(param_id, clamped);
315        Ok(())
316    }
317
318    pub fn begin_parameter_edit(&self, param_id: u32) -> Result<(), String> {
319        self.begin_parameter_edit_at(param_id, 0)
320    }
321
322    pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
323        let _thread_scope = HostThreadScope::enter_main();
324        if !self.param_infos.iter().any(|p| p.id == param_id) {
325            return Err(format!("Unknown CLAP parameter id: {param_id}"));
326        }
327        self.pending_param_events
328            .lock()
329            .push(PendingParamEvent::GestureBegin { param_id, frame });
330        self.pending_param_events_ui
331            .lock()
332            .push(PendingParamEvent::GestureBegin { param_id, frame });
333        Ok(())
334    }
335
336    pub fn end_parameter_edit(&self, param_id: u32) -> Result<(), String> {
337        self.end_parameter_edit_at(param_id, 0)
338    }
339
340    pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
341        let _thread_scope = HostThreadScope::enter_main();
342        if !self.param_infos.iter().any(|p| p.id == param_id) {
343            return Err(format!("Unknown CLAP parameter id: {param_id}"));
344        }
345        self.pending_param_events
346            .lock()
347            .push(PendingParamEvent::GestureEnd { param_id, frame });
348        self.pending_param_events_ui
349            .lock()
350            .push(PendingParamEvent::GestureEnd { param_id, frame });
351        Ok(())
352    }
353
354    pub fn snapshot_state(&self) -> Result<ClapPluginState, String> {
355        let _thread_scope = HostThreadScope::enter_main();
356        self.plugin_handle.snapshot_state()
357    }
358
359    pub fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
360        let _thread_scope = HostThreadScope::enter_main();
361        self.plugin_handle.restore_state(state)
362    }
363
364    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
365        &self.audio_inputs
366    }
367
368    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
369        &self.audio_outputs
370    }
371
372    pub fn main_audio_input_count(&self) -> usize {
373        self.main_audio_inputs
374    }
375
376    pub fn main_audio_output_count(&self) -> usize {
377        self.main_audio_outputs
378    }
379
380    pub fn midi_input_count(&self) -> usize {
381        self.midi_input_ports
382    }
383
384    pub fn midi_output_count(&self) -> usize {
385        self.midi_output_ports
386    }
387
388    pub fn path(&self) -> &str {
389        &self.path
390    }
391
392    pub fn plugin_id(&self) -> &str {
393        &self.plugin_id
394    }
395
396    pub fn name(&self) -> &str {
397        &self.name
398    }
399
400    pub fn ui_begin_session(&self) {
401        self.host_runtime.begin_ui_session();
402    }
403
404    pub fn ui_end_session(&self) {
405        self.host_runtime.end_ui_session();
406    }
407
408    pub fn ui_should_close(&self) -> bool {
409        self.host_runtime.ui_should_close()
410    }
411
412    pub fn ui_take_due_timers(&self) -> Vec<u32> {
413        self.host_runtime.ui_take_due_timers()
414    }
415
416    pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
417        let pending_ui_events = std::mem::take(self.pending_param_events_ui.lock());
418        if pending_ui_events.is_empty() && !self.host_runtime.ui_take_param_flush_requested() {
419            return Vec::new();
420        }
421        let _thread_scope = HostThreadScope::enter_main();
422        let updates = self.plugin_handle.flush_params(&pending_ui_events);
423        if updates.is_empty() {
424            return Vec::new();
425        }
426        let values = &mut *self.param_values.lock();
427        let mut out = Vec::with_capacity(updates.len());
428        for update in updates {
429            values.insert(update.param_id, update.value);
430            out.push(ClapParamUpdate {
431                param_id: update.param_id,
432                value: update.value,
433            });
434        }
435        out
436    }
437
438    pub fn ui_take_state_update(&self) -> Option<ClapPluginState> {
439        if !self.host_runtime.ui_take_state_dirty_requested() {
440            return None;
441        }
442        let _thread_scope = HostThreadScope::enter_main();
443        self.plugin_handle.snapshot_state().ok()
444    }
445
446    pub fn gui_info(&self) -> Result<ClapGuiInfo, String> {
447        let _thread_scope = HostThreadScope::enter_main();
448        self.plugin_handle.gui_info()
449    }
450
451    pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
452        let _thread_scope = HostThreadScope::enter_main();
453        self.plugin_handle.gui_create(api, is_floating)
454    }
455
456    pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
457        let _thread_scope = HostThreadScope::enter_main();
458        self.plugin_handle.gui_get_size()
459    }
460
461    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
462        let _thread_scope = HostThreadScope::enter_main();
463        self.plugin_handle.gui_set_parent_x11(window)
464    }
465
466    pub fn gui_show(&self) -> Result<(), String> {
467        let _thread_scope = HostThreadScope::enter_main();
468        self.plugin_handle.gui_show()
469    }
470
471    pub fn gui_hide(&self) {
472        let _thread_scope = HostThreadScope::enter_main();
473        self.plugin_handle.gui_hide();
474    }
475
476    pub fn gui_destroy(&self) {
477        let _thread_scope = HostThreadScope::enter_main();
478        self.plugin_handle.gui_destroy();
479    }
480
481    pub fn gui_on_main_thread(&self) {
482        let _thread_scope = HostThreadScope::enter_main();
483        self.plugin_handle.on_main_thread();
484    }
485
486    pub fn gui_on_timer(&self, timer_id: u32) {
487        let _thread_scope = HostThreadScope::enter_main();
488        self.plugin_handle.gui_on_timer(timer_id);
489    }
490
491    pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
492        self.plugin_handle.get_note_names()
493    }
494
495    pub fn run_host_callbacks_main_thread(&self) {
496        let host_flags = self.host_runtime.take_callback_flags();
497        if host_flags.restart {
498            let _thread_scope = HostThreadScope::enter_main();
499            self.plugin_handle.reset();
500        }
501        if host_flags.callback {
502            let _thread_scope = HostThreadScope::enter_main();
503            self.plugin_handle.on_main_thread();
504        }
505        if host_flags.process {
506            // Host already continuously schedules process blocks.
507        }
508    }
509
510    fn process_native(
511        &self,
512        frames: usize,
513        midi_in: &[MidiEvent],
514        transport: ClapTransportInfo,
515    ) -> Result<(bool, Vec<ClapMidiOutputEvent>), String> {
516        if frames == 0 {
517            return Ok((true, Vec::new()));
518        }
519
520        let mut in_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_inputs.len());
521        let mut out_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_outputs.len());
522        let mut in_channel_scratch: Vec<Vec<f32>> = Vec::new();
523        let mut out_channel_scratch: Vec<Vec<f32>> = Vec::new();
524        let mut out_channel_scratch_ranges: Vec<(usize, usize)> =
525            Vec::with_capacity(self.audio_outputs.len());
526        let mut in_buffers = Vec::with_capacity(self.audio_inputs.len());
527        let mut out_buffers = Vec::with_capacity(self.audio_outputs.len());
528
529        for (port_idx, input) in self.audio_inputs.iter().enumerate() {
530            let buf = input.buffer.lock();
531            let channel_count = self
532                .input_port_channels
533                .get(port_idx)
534                .copied()
535                .unwrap_or(1)
536                .max(1);
537            let mut ptrs = Vec::with_capacity(channel_count);
538            ptrs.push(buf.as_ptr() as *mut f32);
539            for _ in 1..channel_count {
540                in_channel_scratch.push(buf.to_vec());
541                let idx = in_channel_scratch.len().saturating_sub(1);
542                ptrs.push(in_channel_scratch[idx].as_mut_ptr());
543            }
544            in_channel_ptrs.push(ptrs);
545            in_buffers.push(buf);
546        }
547        for (port_idx, output) in self.audio_outputs.iter().enumerate() {
548            let buf = output.buffer.lock();
549            let channel_count = self
550                .output_port_channels
551                .get(port_idx)
552                .copied()
553                .unwrap_or(1)
554                .max(1);
555            let mut ptrs = Vec::with_capacity(channel_count);
556            ptrs.push(buf.as_mut_ptr());
557            let scratch_start = out_channel_scratch.len();
558            for _ in 1..channel_count {
559                out_channel_scratch.push(vec![0.0; frames]);
560                let idx = out_channel_scratch.len().saturating_sub(1);
561                ptrs.push(out_channel_scratch[idx].as_mut_ptr());
562            }
563            let scratch_end = out_channel_scratch.len();
564            out_channel_scratch_ranges.push((scratch_start, scratch_end));
565            out_channel_ptrs.push(ptrs);
566            out_buffers.push(buf);
567        }
568
569        let mut in_audio = Vec::with_capacity(self.audio_inputs.len());
570        let mut out_audio = Vec::with_capacity(self.audio_outputs.len());
571
572        for ptrs in &mut in_channel_ptrs {
573            in_audio.push(ClapAudioBuffer {
574                data32: ptrs.as_mut_ptr(),
575                data64: std::ptr::null_mut(),
576                channel_count: ptrs.len() as u32,
577                latency: 0,
578                constant_mask: 0,
579            });
580        }
581        for ptrs in &mut out_channel_ptrs {
582            out_audio.push(ClapAudioBuffer {
583                data32: ptrs.as_mut_ptr(),
584                data64: std::ptr::null_mut(),
585                channel_count: ptrs.len() as u32,
586                latency: 0,
587                constant_mask: 0,
588            });
589        }
590
591        let pending_params = std::mem::take(self.pending_param_events.lock());
592        let (in_events, in_ctx) = input_events_from(
593            midi_in,
594            &pending_params,
595            self.sample_rate,
596            transport,
597            self.midi_input_ports > 0,
598        );
599        let out_cap = midi_in
600            .len()
601            .saturating_add(self.midi_output_ports.saturating_mul(64));
602        let (mut out_events, mut out_ctx) = output_events_ctx(out_cap);
603
604        let mut process = ClapProcess {
605            steady_time: -1,
606            frames_count: frames as u32,
607            transport: std::ptr::null(),
608            audio_inputs: in_audio.as_mut_ptr(),
609            audio_outputs: out_audio.as_mut_ptr(),
610            audio_inputs_count: in_audio.len() as u32,
611            audio_outputs_count: out_audio.len() as u32,
612            in_events: &in_events,
613            out_events: &mut out_events,
614        };
615
616        let _thread_scope = HostThreadScope::enter_audio();
617        let result = self.plugin_handle.process(&mut process);
618        drop(in_ctx);
619        for output in &self.audio_outputs {
620            *output.finished.lock() = true;
621        }
622        let processed = result?;
623        if processed {
624            // Downmix multi-channel CLAP output ports into track output buffers.
625            for (port_idx, out_buf) in out_buffers.iter_mut().enumerate() {
626                let Some((scratch_start, scratch_end)) = out_channel_scratch_ranges.get(port_idx)
627                else {
628                    continue;
629                };
630                let scratch_count = scratch_end.saturating_sub(*scratch_start);
631                if scratch_count == 0 {
632                    continue;
633                }
634                let ch_count = scratch_count + 1;
635                for scratch in &out_channel_scratch[*scratch_start..*scratch_end] {
636                    for (dst, src) in out_buf.iter_mut().zip(scratch.iter().take(frames)) {
637                        *dst += *src;
638                    }
639                }
640                let inv = 1.0_f32 / ch_count as f32;
641                for sample in out_buf.iter_mut().take(frames) {
642                    *sample *= inv;
643                }
644            }
645            for update in &out_ctx.param_values {
646                self.param_values
647                    .lock()
648                    .insert(update.param_id, update.value);
649            }
650            Ok((true, std::mem::take(&mut out_ctx.midi_events)))
651        } else {
652            Ok((false, Vec::new()))
653        }
654    }
655}
656
657#[repr(C)]
658#[derive(Clone, Copy)]
659struct ClapVersion {
660    major: u32,
661    minor: u32,
662    revision: u32,
663}
664
665const CLAP_VERSION: ClapVersion = ClapVersion {
666    major: 1,
667    minor: 2,
668    revision: 0,
669};
670
671#[repr(C)]
672struct ClapHost {
673    clap_version: ClapVersion,
674    host_data: *mut c_void,
675    name: *const c_char,
676    vendor: *const c_char,
677    url: *const c_char,
678    version: *const c_char,
679    get_extension: Option<unsafe extern "C" fn(*const ClapHost, *const c_char) -> *const c_void>,
680    request_restart: Option<unsafe extern "C" fn(*const ClapHost)>,
681    request_process: Option<unsafe extern "C" fn(*const ClapHost)>,
682    request_callback: Option<unsafe extern "C" fn(*const ClapHost)>,
683}
684
685#[repr(C)]
686struct ClapPluginEntry {
687    clap_version: ClapVersion,
688    init: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
689    deinit: Option<unsafe extern "C" fn()>,
690    get_factory: Option<unsafe extern "C" fn(*const c_char) -> *const c_void>,
691}
692
693#[repr(C)]
694struct ClapPluginFactory {
695    get_plugin_count: Option<unsafe extern "C" fn(*const ClapPluginFactory) -> u32>,
696    get_plugin_descriptor:
697        Option<unsafe extern "C" fn(*const ClapPluginFactory, u32) -> *const ClapPluginDescriptor>,
698    create_plugin: Option<
699        unsafe extern "C" fn(
700            *const ClapPluginFactory,
701            *const ClapHost,
702            *const c_char,
703        ) -> *const ClapPlugin,
704    >,
705}
706
707#[repr(C)]
708struct ClapPluginDescriptor {
709    clap_version: ClapVersion,
710    id: *const c_char,
711    name: *const c_char,
712    vendor: *const c_char,
713    url: *const c_char,
714    manual_url: *const c_char,
715    support_url: *const c_char,
716    version: *const c_char,
717    description: *const c_char,
718    features: *const *const c_char,
719}
720
721#[repr(C)]
722struct ClapPlugin {
723    desc: *const ClapPluginDescriptor,
724    plugin_data: *mut c_void,
725    init: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
726    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
727    activate: Option<unsafe extern "C" fn(*const ClapPlugin, f64, u32, u32) -> bool>,
728    deactivate: Option<unsafe extern "C" fn(*const ClapPlugin)>,
729    start_processing: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
730    stop_processing: Option<unsafe extern "C" fn(*const ClapPlugin)>,
731    reset: Option<unsafe extern "C" fn(*const ClapPlugin)>,
732    process: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapProcess) -> i32>,
733    get_extension: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char) -> *const c_void>,
734    on_main_thread: Option<unsafe extern "C" fn(*const ClapPlugin)>,
735}
736
737#[repr(C)]
738struct ClapInputEvents {
739    ctx: *const c_void,
740    size: Option<unsafe extern "C" fn(*const ClapInputEvents) -> u32>,
741    get: Option<unsafe extern "C" fn(*const ClapInputEvents, u32) -> *const ClapEventHeader>,
742}
743
744#[repr(C)]
745struct ClapOutputEvents {
746    ctx: *mut c_void,
747    try_push: Option<unsafe extern "C" fn(*const ClapOutputEvents, *const ClapEventHeader) -> bool>,
748}
749
750#[repr(C)]
751struct ClapEventHeader {
752    size: u32,
753    time: u32,
754    space_id: u16,
755    type_: u16,
756    flags: u32,
757}
758
759const CLAP_CORE_EVENT_SPACE_ID: u16 = 0;
760const CLAP_EVENT_NOTE_ON: u16 = 0;
761const CLAP_EVENT_NOTE_OFF: u16 = 1;
762const CLAP_EVENT_MIDI: u16 = 10;
763const CLAP_EVENT_PARAM_VALUE: u16 = 5;
764const CLAP_EVENT_PARAM_GESTURE_BEGIN: u16 = 6;
765const CLAP_EVENT_PARAM_GESTURE_END: u16 = 7;
766const CLAP_EVENT_TRANSPORT: u16 = 9;
767const CLAP_TRANSPORT_HAS_TEMPO: u32 = 1 << 0;
768const CLAP_TRANSPORT_HAS_BEATS_TIMELINE: u32 = 1 << 1;
769const CLAP_TRANSPORT_HAS_SECONDS_TIMELINE: u32 = 1 << 2;
770const CLAP_TRANSPORT_HAS_TIME_SIGNATURE: u32 = 1 << 3;
771const CLAP_TRANSPORT_IS_PLAYING: u32 = 1 << 4;
772const CLAP_TRANSPORT_IS_LOOP_ACTIVE: u32 = 1 << 6;
773const CLAP_BEATTIME_FACTOR: i64 = 1_i64 << 31;
774const CLAP_SECTIME_FACTOR: i64 = 1_i64 << 31;
775
776#[repr(C)]
777struct ClapEventMidi {
778    header: ClapEventHeader,
779    port_index: u16,
780    data: [u8; 3],
781}
782
783#[repr(C)]
784struct ClapEventNote {
785    header: ClapEventHeader,
786    note_id: i32,
787    port_index: i16,
788    channel: i16,
789    key: i16,
790    velocity: f64,
791}
792
793#[repr(C)]
794struct ClapEventParamValue {
795    header: ClapEventHeader,
796    param_id: u32,
797    cookie: *mut c_void,
798    note_id: i32,
799    port_index: i16,
800    channel: i16,
801    key: i16,
802    value: f64,
803}
804
805#[repr(C)]
806struct ClapEventParamGesture {
807    header: ClapEventHeader,
808    param_id: u32,
809}
810
811#[repr(C)]
812struct ClapEventTransport {
813    header: ClapEventHeader,
814    flags: u32,
815    song_pos_beats: i64,
816    song_pos_seconds: i64,
817    tempo: f64,
818    tempo_inc: f64,
819    loop_start_beats: i64,
820    loop_end_beats: i64,
821    loop_start_seconds: i64,
822    loop_end_seconds: i64,
823    bar_start: i64,
824    bar_number: i32,
825    tsig_num: u16,
826    tsig_denom: u16,
827}
828
829#[repr(C)]
830struct ClapParamInfoRaw {
831    id: u32,
832    flags: u32,
833    cookie: *mut c_void,
834    name: [c_char; 256],
835    module: [c_char; 1024],
836    min_value: f64,
837    max_value: f64,
838    default_value: f64,
839}
840
841#[repr(C)]
842struct ClapPluginParams {
843    count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
844    get_info: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapParamInfoRaw) -> bool>,
845    get_value: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut f64) -> bool>,
846    value_to_text:
847        Option<unsafe extern "C" fn(*const ClapPlugin, u32, f64, *mut c_char, u32) -> bool>,
848    text_to_value:
849        Option<unsafe extern "C" fn(*const ClapPlugin, u32, *const c_char, *mut f64) -> bool>,
850    flush: Option<
851        unsafe extern "C" fn(*const ClapPlugin, *const ClapInputEvents, *const ClapOutputEvents),
852    >,
853}
854
855#[repr(C)]
856struct ClapPluginStateExt {
857    save: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapOStream) -> bool>,
858    load: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapIStream) -> bool>,
859}
860
861#[repr(C)]
862struct ClapAudioPortInfoRaw {
863    id: u32,
864    name: [c_char; 256],
865    flags: u32,
866    channel_count: u32,
867    port_type: *const c_char,
868    in_place_pair: u32,
869}
870
871#[repr(C)]
872struct ClapPluginAudioPorts {
873    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
874    get: Option<
875        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapAudioPortInfoRaw) -> bool,
876    >,
877}
878
879#[repr(C)]
880struct ClapNotePortInfoRaw {
881    id: u16,
882    supported_dialects: u32,
883    preferred_dialect: u32,
884    name: [c_char; 256],
885}
886
887#[repr(C)]
888struct ClapPluginNotePorts {
889    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
890    get: Option<
891        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapNotePortInfoRaw) -> bool,
892    >,
893}
894
895#[repr(C)]
896struct ClapPluginGui {
897    is_api_supported: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
898    get_preferred_api:
899        Option<unsafe extern "C" fn(*const ClapPlugin, *mut *const c_char, *mut bool) -> bool>,
900    create: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
901    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
902    set_scale: Option<unsafe extern "C" fn(*const ClapPlugin, f64) -> bool>,
903    get_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
904    can_resize: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
905    get_resize_hints: Option<unsafe extern "C" fn(*const ClapPlugin, *mut c_void) -> bool>,
906    adjust_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
907    set_size: Option<unsafe extern "C" fn(*const ClapPlugin, u32, u32) -> bool>,
908    set_parent: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
909    set_transient: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
910    suggest_title: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char)>,
911    show: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
912    hide: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
913}
914
915#[repr(C)]
916union ClapWindowHandle {
917    x11: usize,
918    native: *mut c_void,
919    cocoa: *mut c_void,
920}
921
922#[repr(C)]
923struct ClapWindow {
924    api: *const c_char,
925    handle: ClapWindowHandle,
926}
927
928#[repr(C)]
929struct ClapPluginTimerSupport {
930    on_timer: Option<unsafe extern "C" fn(*const ClapPlugin, u32)>,
931}
932
933#[repr(C)]
934struct ClapHostThreadCheck {
935    is_main_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
936    is_audio_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
937}
938
939#[repr(C)]
940struct ClapHostLatency {
941    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
942}
943
944#[repr(C)]
945struct ClapHostTail {
946    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
947}
948
949#[repr(C)]
950struct ClapHostTimerSupport {
951    register_timer: Option<unsafe extern "C" fn(*const ClapHost, u32, *mut u32) -> bool>,
952    unregister_timer: Option<unsafe extern "C" fn(*const ClapHost, u32) -> bool>,
953}
954
955#[repr(C)]
956struct ClapHostGui {
957    resize_hints_changed: Option<unsafe extern "C" fn(*const ClapHost)>,
958    request_resize: Option<unsafe extern "C" fn(*const ClapHost, u32, u32) -> bool>,
959    request_show: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
960    request_hide: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
961    closed: Option<unsafe extern "C" fn(*const ClapHost, bool)>,
962}
963
964#[repr(C)]
965struct ClapHostParams {
966    rescan: Option<unsafe extern "C" fn(*const ClapHost, u32)>,
967    clear: Option<unsafe extern "C" fn(*const ClapHost, u32, u32)>,
968    request_flush: Option<unsafe extern "C" fn(*const ClapHost)>,
969}
970
971#[repr(C)]
972struct ClapHostState {
973    mark_dirty: Option<unsafe extern "C" fn(*const ClapHost)>,
974}
975
976#[repr(C)]
977struct ClapNoteName {
978    name: [c_char; 256],
979    port: i16,
980    key: i16,
981    channel: i16,
982}
983
984#[repr(C)]
985struct ClapPluginNoteName {
986    count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
987    get: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapNoteName) -> bool>,
988}
989
990#[repr(C)]
991struct ClapHostNoteName {
992    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
993}
994
995#[repr(C)]
996struct ClapOStream {
997    ctx: *mut c_void,
998    write: Option<unsafe extern "C" fn(*const ClapOStream, *const c_void, u64) -> i64>,
999}
1000
1001#[repr(C)]
1002struct ClapIStream {
1003    ctx: *mut c_void,
1004    read: Option<unsafe extern "C" fn(*const ClapIStream, *mut c_void, u64) -> i64>,
1005}
1006
1007#[repr(C)]
1008struct ClapAudioBuffer {
1009    data32: *mut *mut f32,
1010    data64: *mut *mut f64,
1011    channel_count: u32,
1012    latency: u32,
1013    constant_mask: u64,
1014}
1015
1016#[repr(C)]
1017struct ClapProcess {
1018    steady_time: i64,
1019    frames_count: u32,
1020    transport: *const c_void,
1021    audio_inputs: *mut ClapAudioBuffer,
1022    audio_outputs: *mut ClapAudioBuffer,
1023    audio_inputs_count: u32,
1024    audio_outputs_count: u32,
1025    in_events: *const ClapInputEvents,
1026    out_events: *mut ClapOutputEvents,
1027}
1028
1029enum ClapInputEvent {
1030    Note(ClapEventNote),
1031    Midi(ClapEventMidi),
1032    ParamValue(ClapEventParamValue),
1033    ParamGesture(ClapEventParamGesture),
1034    Transport(ClapEventTransport),
1035}
1036
1037impl ClapInputEvent {
1038    fn header_ptr(&self) -> *const ClapEventHeader {
1039        match self {
1040            Self::Note(e) => &e.header as *const ClapEventHeader,
1041            Self::Midi(e) => &e.header as *const ClapEventHeader,
1042            Self::ParamValue(e) => &e.header as *const ClapEventHeader,
1043            Self::ParamGesture(e) => &e.header as *const ClapEventHeader,
1044            Self::Transport(e) => &e.header as *const ClapEventHeader,
1045        }
1046    }
1047}
1048
1049struct ClapInputEventsCtx {
1050    events: Vec<ClapInputEvent>,
1051}
1052
1053struct ClapOutputEventsCtx {
1054    midi_events: Vec<ClapMidiOutputEvent>,
1055    param_values: Vec<PendingParamValue>,
1056}
1057
1058struct ClapIStreamCtx<'a> {
1059    bytes: &'a [u8],
1060    offset: usize,
1061}
1062
1063#[derive(Default, Clone, Copy)]
1064struct HostCallbackFlags {
1065    restart: bool,
1066    process: bool,
1067    callback: bool,
1068}
1069
1070#[derive(Clone, Copy)]
1071struct HostTimer {
1072    id: u32,
1073    period: Duration,
1074    next_tick: Instant,
1075}
1076
1077struct HostRuntimeState {
1078    callback_flags: UnsafeMutex<HostCallbackFlags>,
1079    timers: UnsafeMutex<Vec<HostTimer>>,
1080    ui_should_close: AtomicU32,
1081    ui_active: AtomicU32,
1082    param_flush_requested: AtomicU32,
1083    state_dirty_requested: AtomicU32,
1084    note_names_dirty: AtomicU32,
1085}
1086
1087thread_local! {
1088    static CLAP_HOST_MAIN_THREAD: Cell<bool> = const { Cell::new(true) };
1089    static CLAP_HOST_AUDIO_THREAD: Cell<bool> = const { Cell::new(false) };
1090}
1091
1092struct HostThreadScope {
1093    main: bool,
1094    prev: bool,
1095}
1096
1097impl HostThreadScope {
1098    fn enter_main() -> Self {
1099        let prev = CLAP_HOST_MAIN_THREAD.with(|flag| {
1100            let prev = flag.get();
1101            flag.set(true);
1102            prev
1103        });
1104        Self { main: true, prev }
1105    }
1106
1107    fn enter_audio() -> Self {
1108        let prev = CLAP_HOST_AUDIO_THREAD.with(|flag| {
1109            let prev = flag.get();
1110            flag.set(true);
1111            prev
1112        });
1113        Self { main: false, prev }
1114    }
1115}
1116
1117impl Drop for HostThreadScope {
1118    fn drop(&mut self) {
1119        if self.main {
1120            CLAP_HOST_MAIN_THREAD.with(|flag| flag.set(self.prev));
1121        } else {
1122            CLAP_HOST_AUDIO_THREAD.with(|flag| flag.set(self.prev));
1123        }
1124    }
1125}
1126
1127struct HostRuntime {
1128    state: Box<HostRuntimeState>,
1129    host: ClapHost,
1130}
1131
1132impl HostRuntime {
1133    fn new() -> Result<Self, String> {
1134        let mut state = Box::new(HostRuntimeState {
1135            callback_flags: UnsafeMutex::new(HostCallbackFlags::default()),
1136            timers: UnsafeMutex::new(Vec::new()),
1137            ui_should_close: AtomicU32::new(0),
1138            ui_active: AtomicU32::new(0),
1139            param_flush_requested: AtomicU32::new(0),
1140            state_dirty_requested: AtomicU32::new(0),
1141            note_names_dirty: AtomicU32::new(0),
1142        });
1143        let host = ClapHost {
1144            clap_version: CLAP_VERSION,
1145            host_data: (&mut *state as *mut HostRuntimeState).cast::<c_void>(),
1146            name: c"Maolan".as_ptr(),
1147            vendor: c"Maolan".as_ptr(),
1148            url: c"https://example.invalid".as_ptr(),
1149            version: c"0.0.1".as_ptr(),
1150            get_extension: Some(host_get_extension),
1151            request_restart: Some(host_request_restart),
1152            request_process: Some(host_request_process),
1153            request_callback: Some(host_request_callback),
1154        };
1155        Ok(Self { state, host })
1156    }
1157
1158    fn take_callback_flags(&self) -> HostCallbackFlags {
1159        let flags = self.state.callback_flags.lock();
1160        let out = *flags;
1161        *flags = HostCallbackFlags::default();
1162        out
1163    }
1164
1165    fn begin_ui_session(&self) {
1166        self.state.ui_should_close.store(0, Ordering::Release);
1167        self.state.ui_active.store(1, Ordering::Release);
1168        self.state.param_flush_requested.store(0, Ordering::Release);
1169        self.state.state_dirty_requested.store(0, Ordering::Release);
1170        self.state.timers.lock().clear();
1171    }
1172
1173    fn end_ui_session(&self) {
1174        self.state.ui_active.store(0, Ordering::Release);
1175        self.state.ui_should_close.store(0, Ordering::Release);
1176        self.state.param_flush_requested.store(0, Ordering::Release);
1177        self.state.state_dirty_requested.store(0, Ordering::Release);
1178        self.state.timers.lock().clear();
1179    }
1180
1181    fn ui_should_close(&self) -> bool {
1182        self.state.ui_should_close.load(Ordering::Acquire) != 0
1183    }
1184
1185    fn ui_take_due_timers(&self) -> Vec<u32> {
1186        let now = Instant::now();
1187        let timers = &mut *self.state.timers.lock();
1188        let mut due = Vec::new();
1189        for timer in timers.iter_mut() {
1190            if now >= timer.next_tick {
1191                due.push(timer.id);
1192                timer.next_tick = now + timer.period;
1193            }
1194        }
1195        due
1196    }
1197
1198    fn ui_take_param_flush_requested(&self) -> bool {
1199        self.state.param_flush_requested.swap(0, Ordering::AcqRel) != 0
1200    }
1201
1202    fn ui_take_state_dirty_requested(&self) -> bool {
1203        self.state.state_dirty_requested.swap(0, Ordering::AcqRel) != 0
1204    }
1205}
1206
1207// SAFETY: HostRuntime owns stable CString storage and a CLAP host struct that
1208// contains raw pointers into that owned storage. The data is immutable after
1209// construction and safe to share/move across threads.
1210unsafe impl Send for HostRuntime {}
1211// SAFETY: See Send rationale above; HostRuntime has no interior mutation.
1212unsafe impl Sync for HostRuntime {}
1213
1214struct PluginHandle {
1215    _library: Library,
1216    entry: *const ClapPluginEntry,
1217    plugin: *const ClapPlugin,
1218    plugin_id: String,
1219    plugin_name: String,
1220}
1221
1222// SAFETY: PluginHandle only stores pointers/libraries managed by the CLAP ABI.
1223// Access to plugin processing is synchronized by the engine track scheduling.
1224unsafe impl Send for PluginHandle {}
1225// SAFETY: Shared references do not mutate PluginHandle fields directly.
1226unsafe impl Sync for PluginHandle {}
1227
1228impl PluginHandle {
1229    fn load(
1230        plugin_path: &str,
1231        plugin_id: Option<&str>,
1232        host_runtime: Arc<HostRuntime>,
1233        sample_rate: f64,
1234        frames: u32,
1235    ) -> Result<Self, String> {
1236        let factory_id = c"clap.plugin-factory";
1237
1238        // SAFETY: We keep `library` alive for at least as long as plugin and entry pointers.
1239        let library = unsafe { Library::new(plugin_path) }.map_err(|e| e.to_string())?;
1240        // SAFETY: Symbol name and type follow CLAP ABI (`clap_entry` global variable).
1241        let entry_ptr = unsafe {
1242            let sym = library
1243                .get::<*const ClapPluginEntry>(b"clap_entry\0")
1244                .map_err(|e| e.to_string())?;
1245            *sym
1246        };
1247        if entry_ptr.is_null() {
1248            return Err("CLAP entry symbol is null".to_string());
1249        }
1250        // SAFETY: entry pointer comes from validated CLAP symbol.
1251        let entry = unsafe { &*entry_ptr };
1252        let init = entry
1253            .init
1254            .ok_or_else(|| "CLAP entry missing init()".to_string())?;
1255        let host_ptr = &host_runtime.host as *const ClapHost;
1256        // SAFETY: Valid host pointer for plugin bundle.
1257        if unsafe { !init(host_ptr) } {
1258            return Err(format!("CLAP entry init failed for {plugin_path}"));
1259        }
1260        let get_factory = entry
1261            .get_factory
1262            .ok_or_else(|| "CLAP entry missing get_factory()".to_string())?;
1263        // SAFETY: Factory id is a static NUL-terminated C string.
1264        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
1265        if factory.is_null() {
1266            return Err("CLAP plugin factory not found".to_string());
1267        }
1268        // SAFETY: factory pointer was validated above.
1269        let factory_ref = unsafe { &*factory };
1270        let get_count = factory_ref
1271            .get_plugin_count
1272            .ok_or_else(|| "CLAP factory missing get_plugin_count()".to_string())?;
1273        let get_desc = factory_ref
1274            .get_plugin_descriptor
1275            .ok_or_else(|| "CLAP factory missing get_plugin_descriptor()".to_string())?;
1276        let create = factory_ref
1277            .create_plugin
1278            .ok_or_else(|| "CLAP factory missing create_plugin()".to_string())?;
1279
1280        // SAFETY: factory function pointers are valid CLAP ABI function pointers.
1281        let count = unsafe { get_count(factory) };
1282        if count == 0 {
1283            return Err("CLAP factory returned zero plugins".to_string());
1284        }
1285        let mut selected_id = None::<CString>;
1286        let mut selected_name = None::<String>;
1287        for i in 0..count {
1288            // SAFETY: i < count.
1289            let desc = unsafe { get_desc(factory, i) };
1290            if desc.is_null() {
1291                continue;
1292            }
1293            // SAFETY: descriptor pointer comes from factory.
1294            let desc = unsafe { &*desc };
1295            if desc.id.is_null() {
1296                continue;
1297            }
1298            // SAFETY: descriptor id is NUL-terminated per CLAP ABI.
1299            let id = unsafe { CStr::from_ptr(desc.id) };
1300            let id_str = id.to_string_lossy();
1301            let name_str = if desc.name.is_null() {
1302                String::new()
1303            } else {
1304                // SAFETY: descriptor name is NUL-terminated per CLAP ABI.
1305                unsafe { CStr::from_ptr(desc.name) }
1306                    .to_string_lossy()
1307                    .into_owned()
1308            };
1309            if plugin_id.is_none() || plugin_id == Some(id_str.as_ref()) {
1310                selected_id = Some(
1311                    CString::new(id_str.as_ref()).map_err(|e| format!("Invalid plugin id: {e}"))?,
1312                );
1313                selected_name = Some(name_str);
1314                break;
1315            }
1316        }
1317        let selected_id = selected_id.ok_or_else(|| {
1318            if let Some(id) = plugin_id {
1319                format!("CLAP descriptor id not found in bundle: {id}")
1320            } else {
1321                "CLAP descriptor not found".to_string()
1322            }
1323        })?;
1324        let plugin_name = selected_name.unwrap_or_else(|| {
1325            Path::new(plugin_path)
1326                .file_stem()
1327                .map(|s| s.to_string_lossy().to_string())
1328                .unwrap_or_else(|| plugin_path.to_string())
1329        });
1330        // SAFETY: valid host pointer and plugin id.
1331        let plugin = unsafe { create(factory, &host_runtime.host, selected_id.as_ptr()) };
1332        if plugin.is_null() {
1333            return Err("CLAP factory create_plugin failed".to_string());
1334        }
1335        // SAFETY: plugin pointer validated above.
1336        let plugin_ref = unsafe { &*plugin };
1337        let plugin_init = plugin_ref
1338            .init
1339            .ok_or_else(|| "CLAP plugin missing init()".to_string())?;
1340        // SAFETY: plugin pointer and function pointer follow CLAP ABI.
1341        if unsafe { !plugin_init(plugin) } {
1342            return Err("CLAP plugin init() failed".to_string());
1343        }
1344        if let Some(activate) = plugin_ref.activate {
1345            // SAFETY: plugin pointer and arguments are valid for current engine buffer config.
1346            if unsafe { !activate(plugin, sample_rate, frames.max(1), frames.max(1)) } {
1347                return Err("CLAP plugin activate() failed".to_string());
1348            }
1349        }
1350        if let Some(start_processing) = plugin_ref.start_processing {
1351            // SAFETY: plugin activated above.
1352            if unsafe { !start_processing(plugin) } {
1353                return Err("CLAP plugin start_processing() failed".to_string());
1354            }
1355        }
1356        let plugin_id_str = selected_id.to_string_lossy().into_owned();
1357        Ok(Self {
1358            _library: library,
1359            entry: entry_ptr,
1360            plugin,
1361            plugin_id: plugin_id_str,
1362            plugin_name,
1363        })
1364    }
1365
1366    fn plugin_id(&self) -> &str {
1367        &self.plugin_id
1368    }
1369
1370    fn plugin_name(&self) -> &str {
1371        &self.plugin_name
1372    }
1373
1374    fn process(&self, process: &mut ClapProcess) -> Result<bool, String> {
1375        // SAFETY: plugin pointer is valid for lifetime of self.
1376        let plugin = unsafe { &*self.plugin };
1377        let Some(process_fn) = plugin.process else {
1378            return Ok(false);
1379        };
1380        // SAFETY: process struct references live buffers for the duration of call.
1381        let _status = unsafe { process_fn(self.plugin, process as *const _) };
1382        Ok(true)
1383    }
1384
1385    fn reset(&self) {
1386        // SAFETY: plugin pointer valid during self lifetime.
1387        let plugin = unsafe { &*self.plugin };
1388        if let Some(reset) = plugin.reset {
1389            // SAFETY: function pointer follows CLAP ABI.
1390            unsafe { reset(self.plugin) };
1391        }
1392    }
1393
1394    fn on_main_thread(&self) {
1395        // SAFETY: plugin pointer valid during self lifetime.
1396        let plugin = unsafe { &*self.plugin };
1397        if let Some(on_main_thread) = plugin.on_main_thread {
1398            // SAFETY: function pointer follows CLAP ABI.
1399            unsafe { on_main_thread(self.plugin) };
1400        }
1401    }
1402
1403    fn params_ext(&self) -> Option<&ClapPluginParams> {
1404        let ext_id = c"clap.params";
1405        // SAFETY: plugin pointer is valid while self is alive.
1406        let plugin = unsafe { &*self.plugin };
1407        let get_extension = plugin.get_extension?;
1408        // SAFETY: extension id is a valid static C string.
1409        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1410        if ext_ptr.is_null() {
1411            return None;
1412        }
1413        // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
1414        Some(unsafe { &*(ext_ptr as *const ClapPluginParams) })
1415    }
1416
1417    fn state_ext(&self) -> Option<&ClapPluginStateExt> {
1418        let ext_id = c"clap.state";
1419        // SAFETY: plugin pointer is valid while self is alive.
1420        let plugin = unsafe { &*self.plugin };
1421        let get_extension = plugin.get_extension?;
1422        // SAFETY: extension id is valid static C string.
1423        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1424        if ext_ptr.is_null() {
1425            return None;
1426        }
1427        // SAFETY: extension pointer layout follows clap.state ABI.
1428        Some(unsafe { &*(ext_ptr as *const ClapPluginStateExt) })
1429    }
1430
1431    fn audio_ports_ext(&self) -> Option<&ClapPluginAudioPorts> {
1432        let ext_id = c"clap.audio-ports";
1433        // SAFETY: plugin pointer is valid while self is alive.
1434        let plugin = unsafe { &*self.plugin };
1435        let get_extension = plugin.get_extension?;
1436        // SAFETY: extension id is valid static C string.
1437        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1438        if ext_ptr.is_null() {
1439            return None;
1440        }
1441        // SAFETY: extension pointer layout follows clap.audio-ports ABI.
1442        Some(unsafe { &*(ext_ptr as *const ClapPluginAudioPorts) })
1443    }
1444
1445    fn note_ports_ext(&self) -> Option<&ClapPluginNotePorts> {
1446        let ext_id = c"clap.note-ports";
1447        // SAFETY: plugin pointer is valid while self is alive.
1448        let plugin = unsafe { &*self.plugin };
1449        let get_extension = plugin.get_extension?;
1450        // SAFETY: extension id is valid static C string.
1451        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1452        if ext_ptr.is_null() {
1453            return None;
1454        }
1455        // SAFETY: extension pointer layout follows clap.note-ports ABI.
1456        Some(unsafe { &*(ext_ptr as *const ClapPluginNotePorts) })
1457    }
1458
1459    fn note_name_ext(&self) -> Option<&ClapPluginNoteName> {
1460        let ext_id = c"clap.note-name";
1461        let plugin = unsafe { &*self.plugin };
1462        let get_extension = plugin.get_extension?;
1463        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1464        if ext_ptr.is_null() {
1465            return None;
1466        }
1467        Some(unsafe { &*(ext_ptr as *const ClapPluginNoteName) })
1468    }
1469
1470    fn get_note_names(&self) -> std::collections::HashMap<u8, String> {
1471        let mut result = std::collections::HashMap::new();
1472        let Some(ext) = self.note_name_ext() else {
1473            return result;
1474        };
1475        let Some(count_fn) = ext.count else {
1476            return result;
1477        };
1478        let Some(get_fn) = ext.get else {
1479            return result;
1480        };
1481        let count = unsafe { count_fn(self.plugin) };
1482        for i in 0..count {
1483            let mut nn = ClapNoteName {
1484                name: [0; 256],
1485                port: -1,
1486                key: -1,
1487                channel: -1,
1488            };
1489            if unsafe { get_fn(self.plugin, i, &mut nn) } {
1490                let name = unsafe {
1491                    std::ffi::CStr::from_ptr(nn.name.as_ptr())
1492                        .to_string_lossy()
1493                        .into_owned()
1494                };
1495                if nn.key >= 0 && nn.key <= 127 && !name.is_empty() {
1496                    result.insert(nn.key as u8, name);
1497                }
1498            }
1499        }
1500        result
1501    }
1502
1503    fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
1504        let Some(params) = self.params_ext() else {
1505            return Vec::new();
1506        };
1507        let Some(count_fn) = params.count else {
1508            return Vec::new();
1509        };
1510        let Some(get_info_fn) = params.get_info else {
1511            return Vec::new();
1512        };
1513        // SAFETY: function pointers come from plugin extension table.
1514        let count = unsafe { count_fn(self.plugin) };
1515        let mut out = Vec::with_capacity(count as usize);
1516        for idx in 0..count {
1517            let mut info = ClapParamInfoRaw {
1518                id: 0,
1519                flags: 0,
1520                cookie: std::ptr::null_mut(),
1521                name: [0; 256],
1522                module: [0; 1024],
1523                min_value: 0.0,
1524                max_value: 1.0,
1525                default_value: 0.0,
1526            };
1527            // SAFETY: info points to valid writable struct.
1528            if unsafe { !get_info_fn(self.plugin, idx, &mut info as *mut _) } {
1529                continue;
1530            }
1531            out.push(ClapParameterInfo {
1532                id: info.id,
1533                name: c_char_buf_to_string(&info.name),
1534                module: c_char_buf_to_string(&info.module),
1535                min_value: info.min_value,
1536                max_value: info.max_value,
1537                default_value: info.default_value,
1538            });
1539        }
1540        out
1541    }
1542
1543    fn parameter_values(&self, infos: &[ClapParameterInfo]) -> HashMap<u32, f64> {
1544        let mut out = HashMap::new();
1545        let Some(params) = self.params_ext() else {
1546            for info in infos {
1547                out.insert(info.id, info.default_value);
1548            }
1549            return out;
1550        };
1551        let Some(get_value_fn) = params.get_value else {
1552            for info in infos {
1553                out.insert(info.id, info.default_value);
1554            }
1555            return out;
1556        };
1557        for info in infos {
1558            let mut value = info.default_value;
1559            // SAFETY: pointer to stack `value` is valid and param id belongs to plugin metadata.
1560            if unsafe { !get_value_fn(self.plugin, info.id, &mut value as *mut _) } {
1561                value = info.default_value;
1562            }
1563            out.insert(info.id, value);
1564        }
1565        out
1566    }
1567
1568    fn flush_params(&self, param_events: &[PendingParamEvent]) -> Vec<PendingParamValue> {
1569        let Some(params) = self.params_ext() else {
1570            return Vec::new();
1571        };
1572        let Some(flush_fn) = params.flush else {
1573            return Vec::new();
1574        };
1575        let (in_events, _in_ctx) = param_input_events_from(param_events);
1576        let out_cap = param_events.len().max(32);
1577        let (out_events, mut out_ctx) = output_events_ctx(out_cap);
1578        // SAFETY: input/output event wrappers stay valid for duration of flush callback.
1579        unsafe {
1580            flush_fn(self.plugin, &in_events, &out_events);
1581        }
1582        std::mem::take(&mut out_ctx.param_values)
1583    }
1584
1585    fn snapshot_state(&self) -> Result<ClapPluginState, String> {
1586        let Some(state_ext) = self.state_ext() else {
1587            return Ok(ClapPluginState { bytes: Vec::new() });
1588        };
1589        let Some(save_fn) = state_ext.save else {
1590            return Ok(ClapPluginState { bytes: Vec::new() });
1591        };
1592        let mut bytes = Vec::<u8>::new();
1593        let mut stream = ClapOStream {
1594            ctx: (&mut bytes as *mut Vec<u8>).cast::<c_void>(),
1595            write: Some(clap_ostream_write),
1596        };
1597        // SAFETY: stream callbacks reference `bytes` for duration of call.
1598        if unsafe {
1599            !save_fn(
1600                self.plugin,
1601                &mut stream as *mut ClapOStream as *const ClapOStream,
1602            )
1603        } {
1604            return Err("CLAP state save failed".to_string());
1605        }
1606        Ok(ClapPluginState { bytes })
1607    }
1608
1609    fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
1610        let Some(state_ext) = self.state_ext() else {
1611            return Ok(());
1612        };
1613        let Some(load_fn) = state_ext.load else {
1614            return Ok(());
1615        };
1616        let mut ctx = ClapIStreamCtx {
1617            bytes: &state.bytes,
1618            offset: 0,
1619        };
1620        let mut stream = ClapIStream {
1621            ctx: (&mut ctx as *mut ClapIStreamCtx).cast::<c_void>(),
1622            read: Some(clap_istream_read),
1623        };
1624        // SAFETY: stream callbacks reference `ctx` for duration of call.
1625        if unsafe {
1626            !load_fn(
1627                self.plugin,
1628                &mut stream as *mut ClapIStream as *const ClapIStream,
1629            )
1630        } {
1631            return Err("CLAP state load failed".to_string());
1632        }
1633        Ok(())
1634    }
1635
1636    const CLAP_AUDIO_PORT_IS_MAIN: u32 = 1;
1637
1638    fn audio_port_channels(&self) -> (Option<AudioPortLayout>, Option<AudioPortLayout>) {
1639        let Some(ext) = self.audio_ports_ext() else {
1640            return (None, None);
1641        };
1642        let Some(count_fn) = ext.count else {
1643            return (None, None);
1644        };
1645        let Some(get_fn) = ext.get else {
1646            return (None, None);
1647        };
1648
1649        let read_ports = |is_input: bool| -> AudioPortLayout {
1650            let mut channels = Vec::new();
1651            let mut main_count = 0;
1652            let count = unsafe { count_fn(self.plugin, is_input) } as usize;
1653            channels.reserve(count);
1654            for idx in 0..count {
1655                let mut info = ClapAudioPortInfoRaw {
1656                    id: 0,
1657                    name: [0; 256],
1658                    flags: 0,
1659                    channel_count: 1,
1660                    port_type: std::ptr::null(),
1661                    in_place_pair: u32::MAX,
1662                };
1663                if unsafe { get_fn(self.plugin, idx as u32, is_input, &mut info as *mut _) } {
1664                    channels.push((info.channel_count as usize).max(1));
1665                    if info.flags & Self::CLAP_AUDIO_PORT_IS_MAIN != 0 {
1666                        main_count += 1;
1667                    }
1668                }
1669            }
1670            (channels, main_count)
1671        };
1672        (Some(read_ports(true)), Some(read_ports(false)))
1673    }
1674
1675    fn note_port_layout(&self) -> (Option<usize>, Option<usize>) {
1676        let Some(ext) = self.note_ports_ext() else {
1677            return (None, None);
1678        };
1679        let Some(count_fn) = ext.count else {
1680            return (None, None);
1681        };
1682        // SAFETY: function pointer comes from plugin extension table.
1683        let in_count = unsafe { count_fn(self.plugin, true) } as usize;
1684        // SAFETY: function pointer comes from plugin extension table.
1685        let out_count = unsafe { count_fn(self.plugin, false) } as usize;
1686        (Some(in_count), Some(out_count))
1687    }
1688
1689    fn gui_ext(&self) -> Option<&ClapPluginGui> {
1690        let ext_id = c"clap.gui";
1691        let plugin = unsafe { &*self.plugin };
1692        let get_extension = plugin.get_extension?;
1693        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1694        if ext_ptr.is_null() {
1695            return None;
1696        }
1697        Some(unsafe { &*(ext_ptr as *const ClapPluginGui) })
1698    }
1699
1700    fn gui_timer_support_ext(&self) -> Option<&ClapPluginTimerSupport> {
1701        let ext_id = c"clap.timer-support";
1702        let plugin = unsafe { &*self.plugin };
1703        let get_extension = plugin.get_extension?;
1704        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1705        if ext_ptr.is_null() {
1706            return None;
1707        }
1708        Some(unsafe { &*(ext_ptr as *const ClapPluginTimerSupport) })
1709    }
1710
1711    fn gui_info(&self) -> Result<ClapGuiInfo, String> {
1712        let gui = self
1713            .gui_ext()
1714            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1715        let is_api_supported = gui
1716            .is_api_supported
1717            .ok_or_else(|| "CLAP gui.is_api_supported is unavailable".to_string())?;
1718        for (api, supports_embedded) in [
1719            ("x11", true),
1720            ("cocoa", true),
1721            ("x11", false),
1722            ("cocoa", false),
1723        ] {
1724            let api_c = CString::new(api).map_err(|e| e.to_string())?;
1725            if unsafe { is_api_supported(self.plugin, api_c.as_ptr(), !supports_embedded) } {
1726                return Ok(ClapGuiInfo {
1727                    api: api.to_string(),
1728                    supports_embedded,
1729                });
1730            }
1731        }
1732        Err("No supported CLAP GUI API found".to_string())
1733    }
1734
1735    fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
1736        let gui = self
1737            .gui_ext()
1738            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1739        let create = gui
1740            .create
1741            .ok_or_else(|| "CLAP gui.create is unavailable".to_string())?;
1742        let api_c = CString::new(api).map_err(|e| e.to_string())?;
1743        if unsafe { !create(self.plugin, api_c.as_ptr(), is_floating) } {
1744            return Err("CLAP gui.create failed".to_string());
1745        }
1746        Ok(())
1747    }
1748
1749    fn gui_get_size(&self) -> Result<(u32, u32), String> {
1750        let gui = self
1751            .gui_ext()
1752            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1753        let get_size = gui
1754            .get_size
1755            .ok_or_else(|| "CLAP gui.get_size is unavailable".to_string())?;
1756        let mut width = 0;
1757        let mut height = 0;
1758        if unsafe { !get_size(self.plugin, &mut width, &mut height) } {
1759            return Err("CLAP gui.get_size failed".to_string());
1760        }
1761        Ok((width, height))
1762    }
1763
1764    fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
1765        let gui = self
1766            .gui_ext()
1767            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1768        let set_parent = gui
1769            .set_parent
1770            .ok_or_else(|| "CLAP gui.set_parent is unavailable".to_string())?;
1771        let clap_window = ClapWindow {
1772            api: c"x11".as_ptr(),
1773            handle: ClapWindowHandle { x11: window },
1774        };
1775        if unsafe { !set_parent(self.plugin, &clap_window) } {
1776            return Err("CLAP gui.set_parent failed".to_string());
1777        }
1778        Ok(())
1779    }
1780
1781    fn gui_show(&self) -> Result<(), String> {
1782        let gui = self
1783            .gui_ext()
1784            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1785        let show = gui
1786            .show
1787            .ok_or_else(|| "CLAP gui.show is unavailable".to_string())?;
1788        if unsafe { !show(self.plugin) } {
1789            return Err("CLAP gui.show failed".to_string());
1790        }
1791        Ok(())
1792    }
1793
1794    fn gui_hide(&self) {
1795        if let Some(gui) = self.gui_ext()
1796            && let Some(hide) = gui.hide
1797        {
1798            unsafe { hide(self.plugin) };
1799        }
1800    }
1801
1802    fn gui_destroy(&self) {
1803        if let Some(gui) = self.gui_ext()
1804            && let Some(destroy) = gui.destroy
1805        {
1806            unsafe { destroy(self.plugin) };
1807        }
1808    }
1809
1810    fn gui_on_timer(&self, timer_id: u32) {
1811        if let Some(timer_ext) = self.gui_timer_support_ext()
1812            && let Some(on_timer) = timer_ext.on_timer
1813        {
1814            unsafe { on_timer(self.plugin, timer_id) };
1815        }
1816    }
1817}
1818
1819impl Drop for PluginHandle {
1820    fn drop(&mut self) {
1821        // SAFETY: pointers were obtained from valid CLAP entry and plugin factory.
1822        unsafe {
1823            if !self.plugin.is_null() {
1824                let plugin = &*self.plugin;
1825                if let Some(stop_processing) = plugin.stop_processing {
1826                    stop_processing(self.plugin);
1827                }
1828                if let Some(deactivate) = plugin.deactivate {
1829                    deactivate(self.plugin);
1830                }
1831                if let Some(destroy) = plugin.destroy {
1832                    destroy(self.plugin);
1833                }
1834            }
1835            if !self.entry.is_null() {
1836                let entry = &*self.entry;
1837                if let Some(deinit) = entry.deinit {
1838                    deinit();
1839                }
1840            }
1841        }
1842    }
1843}
1844
1845static HOST_THREAD_CHECK_EXT: ClapHostThreadCheck = ClapHostThreadCheck {
1846    is_main_thread: Some(host_is_main_thread),
1847    is_audio_thread: Some(host_is_audio_thread),
1848};
1849static HOST_LATENCY_EXT: ClapHostLatency = ClapHostLatency {
1850    changed: Some(host_latency_changed),
1851};
1852static HOST_TAIL_EXT: ClapHostTail = ClapHostTail {
1853    changed: Some(host_tail_changed),
1854};
1855static HOST_TIMER_EXT: ClapHostTimerSupport = ClapHostTimerSupport {
1856    register_timer: Some(host_timer_register),
1857    unregister_timer: Some(host_timer_unregister),
1858};
1859static HOST_GUI_EXT: ClapHostGui = ClapHostGui {
1860    resize_hints_changed: Some(host_gui_resize_hints_changed),
1861    request_resize: Some(host_gui_request_resize),
1862    request_show: Some(host_gui_request_show),
1863    request_hide: Some(host_gui_request_hide),
1864    closed: Some(host_gui_closed),
1865};
1866static HOST_PARAMS_EXT: ClapHostParams = ClapHostParams {
1867    rescan: Some(host_params_rescan),
1868    clear: Some(host_params_clear),
1869    request_flush: Some(host_params_request_flush),
1870};
1871static HOST_STATE_EXT: ClapHostState = ClapHostState {
1872    mark_dirty: Some(host_state_mark_dirty),
1873};
1874static HOST_NOTE_NAME_EXT: ClapHostNoteName = ClapHostNoteName {
1875    changed: Some(host_note_name_changed),
1876};
1877static NEXT_TIMER_ID: AtomicU32 = AtomicU32::new(1);
1878
1879fn host_runtime_state(host: *const ClapHost) -> Option<&'static HostRuntimeState> {
1880    if host.is_null() {
1881        return None;
1882    }
1883    let state_ptr = unsafe { (*host).host_data as *const HostRuntimeState };
1884    if state_ptr.is_null() {
1885        return None;
1886    }
1887    Some(unsafe { &*state_ptr })
1888}
1889
1890unsafe extern "C" fn host_get_extension(
1891    _host: *const ClapHost,
1892    _extension_id: *const c_char,
1893) -> *const c_void {
1894    if _extension_id.is_null() {
1895        return std::ptr::null();
1896    }
1897    // SAFETY: extension id is expected to be a valid NUL-terminated string.
1898    let id = unsafe { CStr::from_ptr(_extension_id) }.to_string_lossy();
1899    match id.as_ref() {
1900        "clap.host.thread-check" => {
1901            (&HOST_THREAD_CHECK_EXT as *const ClapHostThreadCheck).cast::<c_void>()
1902        }
1903        "clap.host.latency" => (&HOST_LATENCY_EXT as *const ClapHostLatency).cast::<c_void>(),
1904        "clap.host.tail" => (&HOST_TAIL_EXT as *const ClapHostTail).cast::<c_void>(),
1905        "clap.host.timer-support" => {
1906            (&HOST_TIMER_EXT as *const ClapHostTimerSupport).cast::<c_void>()
1907        }
1908        "clap.host.gui" => host_runtime_state(_host)
1909            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1910            .map(|_| (&HOST_GUI_EXT as *const ClapHostGui).cast::<c_void>())
1911            .unwrap_or(std::ptr::null()),
1912        "clap.host.params" => host_runtime_state(_host)
1913            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1914            .map(|_| (&HOST_PARAMS_EXT as *const ClapHostParams).cast::<c_void>())
1915            .unwrap_or(std::ptr::null()),
1916        "clap.host.state" => host_runtime_state(_host)
1917            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1918            .map(|_| (&HOST_STATE_EXT as *const ClapHostState).cast::<c_void>())
1919            .unwrap_or(std::ptr::null()),
1920        "clap.host.note-name" => (&HOST_NOTE_NAME_EXT as *const ClapHostNoteName).cast::<c_void>(),
1921        _ => std::ptr::null(),
1922    }
1923}
1924
1925unsafe extern "C" fn host_request_process(_host: *const ClapHost) {
1926    if let Some(state) = host_runtime_state(_host) {
1927        state.callback_flags.lock().process = true;
1928    }
1929}
1930
1931unsafe extern "C" fn host_request_callback(_host: *const ClapHost) {
1932    if let Some(state) = host_runtime_state(_host) {
1933        state.callback_flags.lock().callback = true;
1934    }
1935}
1936
1937unsafe extern "C" fn host_request_restart(_host: *const ClapHost) {
1938    if let Some(state) = host_runtime_state(_host) {
1939        state.callback_flags.lock().restart = true;
1940    }
1941}
1942
1943unsafe extern "C" fn host_note_name_changed(_host: *const ClapHost) {
1944    if let Some(state) = host_runtime_state(_host) {
1945        state.note_names_dirty.store(1, Ordering::Release);
1946    }
1947}
1948
1949unsafe extern "C" fn host_is_main_thread(_host: *const ClapHost) -> bool {
1950    CLAP_HOST_MAIN_THREAD.with(Cell::get)
1951}
1952
1953unsafe extern "C" fn host_is_audio_thread(_host: *const ClapHost) -> bool {
1954    CLAP_HOST_AUDIO_THREAD.with(Cell::get)
1955}
1956
1957unsafe extern "C" fn host_latency_changed(_host: *const ClapHost) {}
1958
1959unsafe extern "C" fn host_tail_changed(_host: *const ClapHost) {}
1960
1961unsafe extern "C" fn host_timer_register(
1962    _host: *const ClapHost,
1963    _period_ms: u32,
1964    timer_id: *mut u32,
1965) -> bool {
1966    if timer_id.is_null() {
1967        return false;
1968    }
1969    let id = NEXT_TIMER_ID.fetch_add(1, Ordering::Relaxed);
1970    if let Some(state) = host_runtime_state(_host) {
1971        let period_ms = _period_ms.max(1);
1972        state.timers.lock().push(HostTimer {
1973            id,
1974            period: Duration::from_millis(period_ms as u64),
1975            next_tick: Instant::now() + Duration::from_millis(period_ms as u64),
1976        });
1977    }
1978    // SAFETY: timer_id points to writable u32 provided by plugin.
1979    unsafe {
1980        *timer_id = id;
1981    }
1982    true
1983}
1984
1985unsafe extern "C" fn host_timer_unregister(_host: *const ClapHost, _timer_id: u32) -> bool {
1986    if let Some(state) = host_runtime_state(_host) {
1987        state.timers.lock().retain(|timer| timer.id != _timer_id);
1988    }
1989    true
1990}
1991
1992unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const ClapHost) {}
1993
1994unsafe extern "C" fn host_gui_request_resize(
1995    _host: *const ClapHost,
1996    _width: u32,
1997    _height: u32,
1998) -> bool {
1999    true
2000}
2001
2002unsafe extern "C" fn host_gui_request_show(_host: *const ClapHost) -> bool {
2003    true
2004}
2005
2006unsafe extern "C" fn host_gui_request_hide(_host: *const ClapHost) -> bool {
2007    if let Some(state) = host_runtime_state(_host) {
2008        if state.ui_active.load(Ordering::Acquire) != 0 {
2009            state.ui_should_close.store(1, Ordering::Release);
2010        }
2011        true
2012    } else {
2013        false
2014    }
2015}
2016
2017unsafe extern "C" fn host_gui_closed(_host: *const ClapHost, _was_destroyed: bool) {
2018    if let Some(state) = host_runtime_state(_host)
2019        && state.ui_active.load(Ordering::Acquire) != 0
2020    {
2021        state.ui_should_close.store(1, Ordering::Release);
2022    }
2023}
2024
2025unsafe extern "C" fn host_params_rescan(_host: *const ClapHost, _flags: u32) {}
2026
2027unsafe extern "C" fn host_params_clear(_host: *const ClapHost, _param_id: u32, _flags: u32) {}
2028
2029unsafe extern "C" fn host_params_request_flush(_host: *const ClapHost) {
2030    if let Some(state) = host_runtime_state(_host) {
2031        state.param_flush_requested.store(1, Ordering::Release);
2032        state.callback_flags.lock().callback = true;
2033    }
2034}
2035
2036unsafe extern "C" fn host_state_mark_dirty(_host: *const ClapHost) {
2037    if let Some(state) = host_runtime_state(_host) {
2038        state.state_dirty_requested.store(1, Ordering::Release);
2039        state.callback_flags.lock().callback = true;
2040    }
2041}
2042
2043unsafe extern "C" fn input_events_size(_list: *const ClapInputEvents) -> u32 {
2044    if _list.is_null() {
2045        return 0;
2046    }
2047    // SAFETY: ctx points to ClapInputEventsCtx owned by process_native.
2048    let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
2049    if ctx.is_null() {
2050        return 0;
2051    }
2052    // SAFETY: ctx is valid during process callback lifetime.
2053    unsafe { (*ctx).events.len() as u32 }
2054}
2055
2056unsafe extern "C" fn input_events_get(
2057    _list: *const ClapInputEvents,
2058    _index: u32,
2059) -> *const ClapEventHeader {
2060    if _list.is_null() {
2061        return std::ptr::null();
2062    }
2063    // SAFETY: ctx points to ClapInputEventsCtx owned by process_native.
2064    let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
2065    if ctx.is_null() {
2066        return std::ptr::null();
2067    }
2068    // SAFETY: ctx is valid during process callback lifetime.
2069    let events = unsafe { &(*ctx).events };
2070    let Some(event) = events.get(_index as usize) else {
2071        return std::ptr::null();
2072    };
2073    event.header_ptr()
2074}
2075
2076unsafe extern "C" fn output_events_try_push(
2077    _list: *const ClapOutputEvents,
2078    _event: *const ClapEventHeader,
2079) -> bool {
2080    if _list.is_null() || _event.is_null() {
2081        return false;
2082    }
2083    // SAFETY: ctx points to ClapOutputEventsCtx owned by process_native.
2084    let ctx = unsafe { (*_list).ctx as *mut ClapOutputEventsCtx };
2085    if ctx.is_null() {
2086        return false;
2087    }
2088    // SAFETY: event pointer is valid for callback lifetime.
2089    let header = unsafe { &*_event };
2090    if header.space_id != CLAP_CORE_EVENT_SPACE_ID {
2091        return false;
2092    }
2093    match header.type_ {
2094        CLAP_EVENT_MIDI => {
2095            if (header.size as usize) < std::mem::size_of::<ClapEventMidi>() {
2096                return false;
2097            }
2098            // SAFETY: validated type/size above.
2099            let midi = unsafe { &*(_event as *const ClapEventMidi) };
2100            // SAFETY: ctx pointer is valid and uniquely owned during processing.
2101            unsafe {
2102                (*ctx).midi_events.push(ClapMidiOutputEvent {
2103                    port: midi.port_index as usize,
2104                    event: MidiEvent::new(header.time, midi.data.to_vec()),
2105                });
2106            }
2107            true
2108        }
2109        CLAP_EVENT_PARAM_VALUE => {
2110            if (header.size as usize) < std::mem::size_of::<ClapEventParamValue>() {
2111                return false;
2112            }
2113            // SAFETY: validated type/size above.
2114            let param = unsafe { &*(_event as *const ClapEventParamValue) };
2115            // SAFETY: ctx pointer is valid and uniquely owned during processing.
2116            unsafe {
2117                (*ctx).param_values.push(PendingParamValue {
2118                    param_id: param.param_id,
2119                    value: param.value,
2120                });
2121            }
2122            true
2123        }
2124        _ => false,
2125    }
2126}
2127
2128fn input_events_from(
2129    midi_events: &[MidiEvent],
2130    param_events: &[PendingParamEvent],
2131    sample_rate: f64,
2132    transport: ClapTransportInfo,
2133    has_note_ports: bool,
2134) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
2135    let mut events = Vec::with_capacity(midi_events.len() + param_events.len() + 1);
2136    let bpm = transport.bpm.max(1.0);
2137    let sample_rate = sample_rate.max(1.0);
2138    let seconds = transport.transport_sample as f64 / sample_rate;
2139    let song_pos_seconds = (seconds * CLAP_SECTIME_FACTOR as f64) as i64;
2140    let beats = seconds * (bpm / 60.0);
2141    let song_pos_beats = (beats * CLAP_BEATTIME_FACTOR as f64) as i64;
2142    let mut flags = CLAP_TRANSPORT_HAS_TEMPO
2143        | CLAP_TRANSPORT_HAS_BEATS_TIMELINE
2144        | CLAP_TRANSPORT_HAS_SECONDS_TIMELINE
2145        | CLAP_TRANSPORT_HAS_TIME_SIGNATURE;
2146    if transport.playing {
2147        flags |= CLAP_TRANSPORT_IS_PLAYING;
2148    }
2149    let (loop_start_seconds, loop_end_seconds, loop_start_beats, loop_end_beats) =
2150        if transport.loop_enabled {
2151            if let Some((loop_start, loop_end)) = transport.loop_range_samples {
2152                flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE;
2153                let ls_sec = loop_start as f64 / sample_rate;
2154                let le_sec = loop_end as f64 / sample_rate;
2155                let ls_beats = ls_sec * (bpm / 60.0);
2156                let le_beats = le_sec * (bpm / 60.0);
2157                (
2158                    (ls_sec * CLAP_SECTIME_FACTOR as f64) as i64,
2159                    (le_sec * CLAP_SECTIME_FACTOR as f64) as i64,
2160                    (ls_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
2161                    (le_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
2162                )
2163            } else {
2164                (0, 0, 0, 0)
2165            }
2166        } else {
2167            (0, 0, 0, 0)
2168        };
2169    let ts_num = transport.tsig_num.max(1);
2170    let ts_denom = transport.tsig_denom.max(1);
2171    let beats_per_bar = ts_num as f64 * (4.0 / ts_denom as f64);
2172    let bar_number = if beats_per_bar > 0.0 {
2173        (beats / beats_per_bar).floor().max(0.0) as i32
2174    } else {
2175        0
2176    };
2177    let bar_start_beats = (bar_number as f64 * beats_per_bar * CLAP_BEATTIME_FACTOR as f64) as i64;
2178    events.push(ClapInputEvent::Transport(ClapEventTransport {
2179        header: ClapEventHeader {
2180            size: std::mem::size_of::<ClapEventTransport>() as u32,
2181            time: 0,
2182            space_id: CLAP_CORE_EVENT_SPACE_ID,
2183            type_: CLAP_EVENT_TRANSPORT,
2184            flags: 0,
2185        },
2186        flags,
2187        song_pos_beats,
2188        song_pos_seconds,
2189        tempo: bpm,
2190        tempo_inc: 0.0,
2191        loop_start_beats,
2192        loop_end_beats,
2193        loop_start_seconds,
2194        loop_end_seconds,
2195        bar_start: bar_start_beats,
2196        bar_number,
2197        tsig_num: ts_num,
2198        tsig_denom: ts_denom,
2199    }));
2200    for event in midi_events {
2201        if event.data.is_empty() {
2202            continue;
2203        }
2204        let mut data = [0_u8; 3];
2205        let bytes = event.data.len().min(3);
2206        data[..bytes].copy_from_slice(&event.data[..bytes]);
2207        let status = data[0];
2208        let is_note_on = (0x90..=0x9F).contains(&status);
2209        let is_note_off = (0x80..=0x8F).contains(&status);
2210        if has_note_ports && (is_note_on || is_note_off) {
2211            let channel = (status & 0x0F) as i16;
2212            let key = data.get(1).copied().unwrap_or(0).min(127) as i16;
2213            let velocity_byte = data.get(2).copied().unwrap_or(0);
2214            let velocity = if is_note_on && velocity_byte == 0 {
2215                // Note-on with velocity 0 is conventionally note-off.
2216                events.push(ClapInputEvent::Note(ClapEventNote {
2217                    header: ClapEventHeader {
2218                        size: std::mem::size_of::<ClapEventNote>() as u32,
2219                        time: event.frame,
2220                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2221                        type_: CLAP_EVENT_NOTE_OFF,
2222                        flags: 0,
2223                    },
2224                    note_id: -1,
2225                    port_index: 0,
2226                    channel,
2227                    key,
2228                    velocity: 0.0,
2229                }));
2230                continue;
2231            } else {
2232                (velocity_byte as f64 / 127.0).clamp(0.0, 1.0)
2233            };
2234            events.push(ClapInputEvent::Note(ClapEventNote {
2235                header: ClapEventHeader {
2236                    size: std::mem::size_of::<ClapEventNote>() as u32,
2237                    time: event.frame,
2238                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2239                    type_: if is_note_on {
2240                        CLAP_EVENT_NOTE_ON
2241                    } else {
2242                        CLAP_EVENT_NOTE_OFF
2243                    },
2244                    flags: 0,
2245                },
2246                note_id: -1,
2247                port_index: 0,
2248                channel,
2249                key,
2250                velocity,
2251            }));
2252        } else {
2253            events.push(ClapInputEvent::Midi(ClapEventMidi {
2254                header: ClapEventHeader {
2255                    size: std::mem::size_of::<ClapEventMidi>() as u32,
2256                    time: event.frame,
2257                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2258                    type_: CLAP_EVENT_MIDI,
2259                    flags: 0,
2260                },
2261                port_index: 0,
2262                data,
2263            }));
2264        }
2265    }
2266    for param in param_events {
2267        match *param {
2268            PendingParamEvent::Value {
2269                param_id,
2270                value,
2271                frame,
2272            } => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
2273                header: ClapEventHeader {
2274                    size: std::mem::size_of::<ClapEventParamValue>() as u32,
2275                    time: frame,
2276                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2277                    type_: CLAP_EVENT_PARAM_VALUE,
2278                    flags: 0,
2279                },
2280                param_id,
2281                cookie: std::ptr::null_mut(),
2282                note_id: -1,
2283                port_index: -1,
2284                channel: -1,
2285                key: -1,
2286                value,
2287            })),
2288            PendingParamEvent::GestureBegin { param_id, frame } => {
2289                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2290                    header: ClapEventHeader {
2291                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2292                        time: frame,
2293                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2294                        type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
2295                        flags: 0,
2296                    },
2297                    param_id,
2298                }))
2299            }
2300            PendingParamEvent::GestureEnd { param_id, frame } => {
2301                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2302                    header: ClapEventHeader {
2303                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2304                        time: frame,
2305                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2306                        type_: CLAP_EVENT_PARAM_GESTURE_END,
2307                        flags: 0,
2308                    },
2309                    param_id,
2310                }))
2311            }
2312        }
2313    }
2314    events.sort_by_key(|event| match event {
2315        ClapInputEvent::Note(e) => e.header.time,
2316        ClapInputEvent::Midi(e) => e.header.time,
2317        ClapInputEvent::ParamValue(e) => e.header.time,
2318        ClapInputEvent::ParamGesture(e) => e.header.time,
2319        ClapInputEvent::Transport(e) => e.header.time,
2320    });
2321    let mut ctx = Box::new(ClapInputEventsCtx { events });
2322    let list = ClapInputEvents {
2323        ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
2324        size: Some(input_events_size),
2325        get: Some(input_events_get),
2326    };
2327    (list, ctx)
2328}
2329
2330fn param_input_events_from(
2331    param_events: &[PendingParamEvent],
2332) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
2333    let mut events = Vec::with_capacity(param_events.len());
2334    for param in param_events {
2335        match *param {
2336            PendingParamEvent::Value {
2337                param_id,
2338                value,
2339                frame,
2340            } => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
2341                header: ClapEventHeader {
2342                    size: std::mem::size_of::<ClapEventParamValue>() as u32,
2343                    time: frame,
2344                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2345                    type_: CLAP_EVENT_PARAM_VALUE,
2346                    flags: 0,
2347                },
2348                param_id,
2349                cookie: std::ptr::null_mut(),
2350                note_id: -1,
2351                port_index: -1,
2352                channel: -1,
2353                key: -1,
2354                value,
2355            })),
2356            PendingParamEvent::GestureBegin { param_id, frame } => {
2357                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2358                    header: ClapEventHeader {
2359                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2360                        time: frame,
2361                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2362                        type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
2363                        flags: 0,
2364                    },
2365                    param_id,
2366                }))
2367            }
2368            PendingParamEvent::GestureEnd { param_id, frame } => {
2369                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2370                    header: ClapEventHeader {
2371                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2372                        time: frame,
2373                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2374                        type_: CLAP_EVENT_PARAM_GESTURE_END,
2375                        flags: 0,
2376                    },
2377                    param_id,
2378                }))
2379            }
2380        }
2381    }
2382    events.sort_by_key(|event| match event {
2383        ClapInputEvent::Note(e) => e.header.time,
2384        ClapInputEvent::Midi(e) => e.header.time,
2385        ClapInputEvent::ParamValue(e) => e.header.time,
2386        ClapInputEvent::ParamGesture(e) => e.header.time,
2387        ClapInputEvent::Transport(e) => e.header.time,
2388    });
2389    let mut ctx = Box::new(ClapInputEventsCtx { events });
2390    let list = ClapInputEvents {
2391        ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
2392        size: Some(input_events_size),
2393        get: Some(input_events_get),
2394    };
2395    (list, ctx)
2396}
2397
2398fn output_events_ctx(capacity: usize) -> (ClapOutputEvents, Box<ClapOutputEventsCtx>) {
2399    let mut ctx = Box::new(ClapOutputEventsCtx {
2400        midi_events: Vec::with_capacity(capacity),
2401        param_values: Vec::with_capacity(capacity / 2),
2402    });
2403    let list = ClapOutputEvents {
2404        ctx: (&mut *ctx as *mut ClapOutputEventsCtx).cast::<c_void>(),
2405        try_push: Some(output_events_try_push),
2406    };
2407    (list, ctx)
2408}
2409
2410fn c_char_buf_to_string<const N: usize>(buf: &[c_char; N]) -> String {
2411    let bytes = buf
2412        .iter()
2413        .take_while(|&&b| b != 0)
2414        .map(|&b| b as u8)
2415        .collect::<Vec<u8>>();
2416    String::from_utf8_lossy(&bytes).to_string()
2417}
2418
2419fn split_plugin_spec(spec: &str) -> (&str, Option<&str>) {
2420    if let Some((path, id)) = spec.split_once("::")
2421        && !id.trim().is_empty()
2422    {
2423        return (path, Some(id.trim()));
2424    }
2425    (spec, None)
2426}
2427
2428unsafe extern "C" fn clap_ostream_write(
2429    stream: *const ClapOStream,
2430    buffer: *const c_void,
2431    size: u64,
2432) -> i64 {
2433    if stream.is_null() || buffer.is_null() {
2434        return -1;
2435    }
2436    // SAFETY: ctx is initialized by snapshot_state and valid during callback.
2437    let ctx = unsafe { (*stream).ctx as *mut Vec<u8> };
2438    if ctx.is_null() {
2439        return -1;
2440    }
2441    let n = (size as usize).min(isize::MAX as usize);
2442    // SAFETY: source pointer is valid for `n` bytes per caller contract.
2443    let src = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), n) };
2444    // SAFETY: ctx points to writable Vec<u8>.
2445    unsafe {
2446        (*ctx).extend_from_slice(src);
2447    }
2448    n as i64
2449}
2450
2451unsafe extern "C" fn clap_istream_read(
2452    stream: *const ClapIStream,
2453    buffer: *mut c_void,
2454    size: u64,
2455) -> i64 {
2456    if stream.is_null() || buffer.is_null() {
2457        return -1;
2458    }
2459    // SAFETY: ctx is initialized by restore_state and valid during callback.
2460    let ctx = unsafe { (*stream).ctx as *mut ClapIStreamCtx<'_> };
2461    if ctx.is_null() {
2462        return -1;
2463    }
2464    // SAFETY: ctx points to valid read context.
2465    let ctx = unsafe { &mut *ctx };
2466    let remaining = ctx.bytes.len().saturating_sub(ctx.offset);
2467    if remaining == 0 {
2468        return 0;
2469    }
2470    let n = remaining.min(size as usize);
2471    // SAFETY: destination pointer is valid for `n` bytes per caller contract.
2472    let dst = unsafe { std::slice::from_raw_parts_mut(buffer.cast::<u8>(), n) };
2473    dst.copy_from_slice(&ctx.bytes[ctx.offset..ctx.offset + n]);
2474    ctx.offset += n;
2475    n as i64
2476}
2477
2478pub fn list_plugins() -> Vec<ClapPluginInfo> {
2479    list_plugins_with_capabilities(false)
2480}
2481
2482pub fn is_supported_clap_binary(path: &Path) -> bool {
2483    path.extension().is_some_and(|ext| {
2484        ext.eq_ignore_ascii_case("clap")
2485            || ext.eq_ignore_ascii_case("so")
2486            || ext.eq_ignore_ascii_case("dylib")
2487            || ext.eq_ignore_ascii_case("dll")
2488    })
2489}
2490
2491pub fn list_plugins_with_capabilities(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
2492    let mut roots = default_clap_search_roots();
2493
2494    if let Ok(extra) = std::env::var("CLAP_PATH") {
2495        for p in std::env::split_paths(&extra) {
2496            if !p.as_os_str().is_empty() {
2497                roots.push(p);
2498            }
2499        }
2500    }
2501
2502    let mut out = Vec::new();
2503    for root in roots {
2504        collect_clap_plugins(&root, &mut out, scan_capabilities);
2505    }
2506
2507    out.sort_by_key(|a| a.name.to_lowercase());
2508    out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));
2509    out
2510}
2511
2512fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
2513    let Ok(entries) = std::fs::read_dir(root) else {
2514        return;
2515    };
2516    for entry in entries.flatten() {
2517        let path = entry.path();
2518        let Ok(ft) = entry.file_type() else {
2519            continue;
2520        };
2521        if ft.is_dir() {
2522            if path
2523                .file_name()
2524                .and_then(|name| name.to_str())
2525                .is_some_and(|name| {
2526                    matches!(
2527                        name,
2528                        "deps" | "build" | "incremental" | ".fingerprint" | "examples"
2529                    )
2530                })
2531            {
2532                continue;
2533            }
2534            collect_clap_plugins(&path, out, scan_capabilities);
2535            continue;
2536        }
2537
2538        if is_supported_clap_binary(&path) {
2539            let infos = scan_bundle_descriptors(&path, scan_capabilities);
2540            if infos.is_empty() {
2541                let name = path
2542                    .file_stem()
2543                    .map(|s| s.to_string_lossy().to_string())
2544                    .unwrap_or_else(|| path.to_string_lossy().to_string());
2545                out.push(ClapPluginInfo {
2546                    name,
2547                    path: path.to_string_lossy().to_string(),
2548                    capabilities: None,
2549                });
2550            } else {
2551                out.extend(infos);
2552            }
2553        }
2554    }
2555}
2556
2557fn scan_bundle_descriptors(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
2558    let path_str = path.to_string_lossy().to_string();
2559    let factory_id = c"clap.plugin-factory";
2560    let host_runtime = match HostRuntime::new() {
2561        Ok(runtime) => runtime,
2562        Err(_) => return Vec::new(),
2563    };
2564    // SAFETY: path points to plugin module file.
2565    let library = match unsafe { Library::new(path) } {
2566        Ok(lib) => lib,
2567        Err(_) => return Vec::new(),
2568    };
2569    // SAFETY: symbol is CLAP entry pointer.
2570    let entry_ptr = unsafe {
2571        match library.get::<*const ClapPluginEntry>(b"clap_entry\0") {
2572            Ok(sym) => *sym,
2573            Err(_) => return Vec::new(),
2574        }
2575    };
2576    if entry_ptr.is_null() {
2577        return Vec::new();
2578    }
2579    // SAFETY: entry pointer validated above.
2580    let entry = unsafe { &*entry_ptr };
2581    let Some(init) = entry.init else {
2582        return Vec::new();
2583    };
2584    let host_ptr = &host_runtime.host;
2585    // SAFETY: valid host pointer.
2586    if unsafe { !init(host_ptr) } {
2587        return Vec::new();
2588    }
2589    let mut out = Vec::new();
2590    if let Some(get_factory) = entry.get_factory {
2591        // SAFETY: static factory id.
2592        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
2593        if !factory.is_null() {
2594            // SAFETY: factory pointer validated above.
2595            let factory_ref = unsafe { &*factory };
2596            if let (Some(get_count), Some(get_desc)) = (
2597                factory_ref.get_plugin_count,
2598                factory_ref.get_plugin_descriptor,
2599            ) {
2600                // SAFETY: function pointer from plugin.
2601                let count = unsafe { get_count(factory) };
2602                for i in 0..count {
2603                    // SAFETY: i < count.
2604                    let desc = unsafe { get_desc(factory, i) };
2605                    if desc.is_null() {
2606                        continue;
2607                    }
2608                    // SAFETY: descriptor pointer from plugin factory.
2609                    let desc = unsafe { &*desc };
2610                    if desc.id.is_null() || desc.name.is_null() {
2611                        continue;
2612                    }
2613                    // SAFETY: CLAP descriptor strings are NUL-terminated.
2614                    let id = unsafe { CStr::from_ptr(desc.id) }
2615                        .to_string_lossy()
2616                        .to_string();
2617                    // SAFETY: CLAP descriptor strings are NUL-terminated.
2618                    let name = unsafe { CStr::from_ptr(desc.name) }
2619                        .to_string_lossy()
2620                        .to_string();
2621
2622                    let capabilities = if scan_capabilities {
2623                        scan_plugin_capabilities(factory_ref, factory, &host_runtime.host, &id)
2624                    } else {
2625                        None
2626                    };
2627
2628                    out.push(ClapPluginInfo {
2629                        name,
2630                        path: format!("{path_str}::{id}"),
2631                        capabilities,
2632                    });
2633                }
2634            }
2635        }
2636    }
2637    // SAFETY: deinit belongs to entry and is valid after init.
2638    if let Some(deinit) = entry.deinit {
2639        unsafe { deinit() };
2640    }
2641    out
2642}
2643
2644fn scan_plugin_capabilities(
2645    factory: &ClapPluginFactory,
2646    factory_ptr: *const ClapPluginFactory,
2647    host: &ClapHost,
2648    plugin_id: &str,
2649) -> Option<ClapPluginCapabilities> {
2650    let create = factory.create_plugin?;
2651
2652    let id_cstring = CString::new(plugin_id).ok()?;
2653    // SAFETY: valid factory, host, and id pointers.
2654    let plugin = unsafe { create(factory_ptr, host, id_cstring.as_ptr()) };
2655    if plugin.is_null() {
2656        return None;
2657    }
2658
2659    // SAFETY: plugin pointer validated above.
2660    let plugin_ref = unsafe { &*plugin };
2661    let plugin_init = plugin_ref.init?;
2662
2663    // SAFETY: plugin pointer and function pointer follow CLAP ABI.
2664    if unsafe { !plugin_init(plugin) } {
2665        return None;
2666    }
2667
2668    let mut capabilities = ClapPluginCapabilities {
2669        has_gui: false,
2670        gui_apis: Vec::new(),
2671        supports_embedded: false,
2672        supports_floating: false,
2673        has_params: false,
2674        has_state: false,
2675        audio_inputs: 0,
2676        audio_outputs: 0,
2677        midi_inputs: 0,
2678        midi_outputs: 0,
2679    };
2680
2681    // Check for extensions
2682    if let Some(get_extension) = plugin_ref.get_extension {
2683        // Check GUI extension
2684        let gui_ext_id = c"clap.gui";
2685        // SAFETY: extension id is valid static C string.
2686        let gui_ptr = unsafe { get_extension(plugin, gui_ext_id.as_ptr()) };
2687        if !gui_ptr.is_null() {
2688            capabilities.has_gui = true;
2689            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2690            let gui = unsafe { &*(gui_ptr as *const ClapPluginGui) };
2691
2692            // Check which GUI APIs are supported
2693            if let Some(is_api_supported) = gui.is_api_supported {
2694                for api in ["x11", "cocoa"] {
2695                    if let Ok(api_cstr) = CString::new(api) {
2696                        // Check embedded mode
2697                        // SAFETY: valid plugin and API string pointers.
2698                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), false) } {
2699                            capabilities.gui_apis.push(format!("{} (embedded)", api));
2700                            capabilities.supports_embedded = true;
2701                        }
2702                        // Check floating mode
2703                        // SAFETY: valid plugin and API string pointers.
2704                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), true) } {
2705                            if !capabilities.supports_embedded {
2706                                capabilities.gui_apis.push(format!("{} (floating)", api));
2707                            }
2708                            capabilities.supports_floating = true;
2709                        }
2710                    }
2711                }
2712            }
2713        }
2714
2715        // Check params extension
2716        let params_ext_id = c"clap.params";
2717        // SAFETY: extension id is valid static C string.
2718        let params_ptr = unsafe { get_extension(plugin, params_ext_id.as_ptr()) };
2719        capabilities.has_params = !params_ptr.is_null();
2720
2721        // Check state extension
2722        let state_ext_id = c"clap.state";
2723        // SAFETY: extension id is valid static C string.
2724        let state_ptr = unsafe { get_extension(plugin, state_ext_id.as_ptr()) };
2725        capabilities.has_state = !state_ptr.is_null();
2726
2727        // Check audio-ports extension
2728        let audio_ports_ext_id = c"clap.audio-ports";
2729        // SAFETY: extension id is valid static C string.
2730        let audio_ports_ptr = unsafe { get_extension(plugin, audio_ports_ext_id.as_ptr()) };
2731        if !audio_ports_ptr.is_null() {
2732            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2733            let audio_ports = unsafe { &*(audio_ports_ptr as *const ClapPluginAudioPorts) };
2734            if let Some(count_fn) = audio_ports.count {
2735                // SAFETY: function pointer comes from plugin extension table.
2736                capabilities.audio_inputs = unsafe { count_fn(plugin, true) } as usize;
2737                // SAFETY: function pointer comes from plugin extension table.
2738                capabilities.audio_outputs = unsafe { count_fn(plugin, false) } as usize;
2739            }
2740        }
2741
2742        // Check note-ports extension
2743        let note_ports_ext_id = c"clap.note-ports";
2744        // SAFETY: extension id is valid static C string.
2745        let note_ports_ptr = unsafe { get_extension(plugin, note_ports_ext_id.as_ptr()) };
2746        if !note_ports_ptr.is_null() {
2747            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2748            let note_ports = unsafe { &*(note_ports_ptr as *const ClapPluginNotePorts) };
2749            if let Some(count_fn) = note_ports.count {
2750                // SAFETY: function pointer comes from plugin extension table.
2751                capabilities.midi_inputs = unsafe { count_fn(plugin, true) } as usize;
2752                // SAFETY: function pointer comes from plugin extension table.
2753                capabilities.midi_outputs = unsafe { count_fn(plugin, false) } as usize;
2754            }
2755        }
2756    }
2757
2758    // Clean up plugin instance
2759    if let Some(destroy) = plugin_ref.destroy {
2760        // SAFETY: plugin pointer is valid.
2761        unsafe { destroy(plugin) };
2762    }
2763
2764    Some(capabilities)
2765}
2766
2767fn default_clap_search_roots() -> Vec<PathBuf> {
2768    let mut roots = Vec::new();
2769
2770    #[cfg(target_os = "macos")]
2771    {
2772        paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
2773    }
2774
2775    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
2776    {
2777        paths::push_unix_plugin_roots(&mut roots, "clap");
2778    }
2779
2780    roots
2781}
2782
2783#[cfg(test)]
2784mod tests {
2785    use super::collect_clap_plugins;
2786    use std::fs;
2787    use std::path::PathBuf;
2788    use std::time::{SystemTime, UNIX_EPOCH};
2789
2790    #[cfg(unix)]
2791    fn make_symlink(src: &PathBuf, dst: &PathBuf) {
2792        std::os::unix::fs::symlink(src, dst).expect("should create symlink");
2793    }
2794
2795    #[cfg(unix)]
2796    #[test]
2797    fn collect_clap_plugins_includes_symlinked_clap_files() {
2798        let nanos = SystemTime::now()
2799            .duration_since(UNIX_EPOCH)
2800            .expect("time should be valid")
2801            .as_nanos();
2802        let root = std::env::temp_dir().join(format!(
2803            "maolan-clap-symlink-test-{}-{nanos}",
2804            std::process::id()
2805        ));
2806        fs::create_dir_all(&root).expect("should create temp dir");
2807
2808        let target_file = root.join("librural_modeler.so");
2809        fs::write(&target_file, b"not a real clap binary").expect("should create target file");
2810        let clap_link = root.join("RuralModeler.clap");
2811        make_symlink(&PathBuf::from("librural_modeler.so"), &clap_link);
2812
2813        let mut out = Vec::new();
2814        collect_clap_plugins(&root, &mut out, false);
2815
2816        assert!(
2817            out.iter()
2818                .any(|info| info.path == clap_link.to_string_lossy()),
2819            "scanner should include symlinked .clap files"
2820        );
2821
2822        fs::remove_dir_all(&root).expect("should remove temp dir");
2823    }
2824}