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