Skip to main content

maolan_engine/plugins/
clap.rs

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