Skip to main content

maolan_engine/plugins/
clap.rs

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