Skip to main content

maolan_engine/plugins/
clap_proc.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::{MIDIIO, MidiEvent};
3use crate::plugins::ipc;
4use crate::plugins::types::{
5    ClapMidiOutputEvent, ClapParamUpdate, ClapParameterInfo, ClapTransportInfo,
6};
7use arc_swap::{ArcSwap, ArcSwapOption};
8use maolan_plugin_protocol::events::EventPair;
9use maolan_plugin_protocol::protocol::*;
10use maolan_plugin_protocol::ringbuf::RingBuffer;
11use maolan_plugin_protocol::shm::ShmMapping;
12use std::cell::UnsafeCell;
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::process::{Child, ChildStderr};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
18use std::time::{Duration, Instant};
19
20const SHM_LATENCY_SAMPLES_OFFSET: usize = 84;
21const REQUEST_CLAP_AUDIO_PORTS: u32 = 12;
22
23unsafe fn latency_samples_atomic(ptr: *mut u8) -> &'static AtomicU32 {
24    unsafe { &*(ptr.add(SHM_LATENCY_SAMPLES_OFFSET) as *const AtomicU32) }
25}
26
27fn wait_for_host_request_complete(
28    header: &ShmHeader,
29    events: &EventPair,
30    timeout: Duration,
31) -> Result<(), String> {
32    let start = Instant::now();
33    loop {
34        // The host sets request_status before signalling and clears request_type
35        // after signalling. Check either condition so we don't miss the response
36        // if request_type is still non-zero when the wake-up arrives.
37        if header.request_type.load(Ordering::Acquire) == 0
38            || header.request_status.load(Ordering::Acquire) != 0
39        {
40            return Ok(());
41        }
42        let elapsed = start.elapsed();
43        if elapsed >= timeout {
44            return Err("Host did not respond to request".to_string());
45        }
46        if let Err(e) = events.wait_host(timeout - elapsed) {
47            return Err(format!("Host did not respond to request: {e}"));
48        }
49    }
50}
51
52pub struct ClapProcessor {
53    path: String,
54    plugin_id: String,
55    name: String,
56    buffer_size: usize,
57    audio_inputs: ArcSwap<Vec<Arc<AudioIO>>>,
58    audio_outputs: ArcSwap<Vec<Arc<AudioIO>>>,
59    main_audio_inputs: AtomicUsize,
60    main_audio_outputs: AtomicUsize,
61    midi_input_count: usize,
62    midi_output_count: usize,
63    midi_input_ports: Vec<Arc<MIDIIO>>,
64    midi_output_ports: Vec<Arc<MIDIIO>>,
65    param_infos: Vec<ClapParameterInfo>,
66    /// Current value of every known parameter, keyed by parameter id and
67    /// stored as `f64` bits. Pre-populated from `param_infos` at construction
68    /// and only ever touched through atomic loads/stores.
69    param_values: HashMap<u32, AtomicU64>,
70    bypassed: Arc<AtomicBool>,
71
72    /// Host child process handle. Interior-mutable so `process_with_audio_buffers`
73    /// (which takes `&self`) can poll it with `try_wait`.
74    ///
75    /// Invariant: at any moment there is at most one accessor — either the
76    /// audio thread running this plugin's own plan task node (exactly one per
77    /// cycle), or control code running after the last `Arc` reference to this
78    /// processor is gone (`Drop`).
79    child: UnsafeCell<Option<Child>>,
80    /// Host stderr pipe; control-side only (`take_stderr`). RCU-published so
81    /// no blocking primitive is involved.
82    stderr: ArcSwapOption<ChildStderr>,
83    mapping: Option<ShmMapping>,
84    events: Option<EventPair>,
85    shm_name: String,
86
87    crash_count: AtomicU32,
88    last_latency_samples: AtomicUsize,
89    latency_changed: AtomicBool,
90}
91
92// Safety: see the invariants on `child` and `stderr` above. Every other field
93// is either immutable after construction or synchronized on its own
94// (atomics / RCU).
95unsafe impl Sync for ClapProcessor {}
96
97pub type SharedClapProcessor = Arc<ClapProcessor>;
98
99impl ClapProcessor {
100    pub fn new(
101        _sample_rate: f64,
102        buffer_size: usize,
103        plugin_spec: &str,
104        input_count: usize,
105        output_count: usize,
106        host_binary: PathBuf,
107    ) -> Result<Self, String> {
108        let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
109
110        let instance_id = ipc::unique_instance_id("clap");
111        let plugin_spec = if plugin_id.is_empty() {
112            plugin_path.to_string()
113        } else {
114            format!("{plugin_path}::{plugin_id}")
115        };
116        let (mut child, mapping, events, shm_name, stderr) = ipc::spawn_host(ipc::HostSpawnArgs {
117            host_binary: &host_binary,
118            format: "clap",
119            plugin_spec: &plugin_spec,
120            instance_id: &instance_id,
121            extra_args: &[],
122        })?;
123
124        let header = unsafe { header_ref(mapping.as_ptr()) };
125        if !ipc::wait_for_ready(header, &mut child, Duration::from_secs(10)) {
126            let _ = child.kill();
127            return Err("host did not signal ready".to_string());
128        }
129
130        let name = unsafe {
131            maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(mapping.as_ptr())
132                .unwrap_or_else(|| plugin_id.to_string())
133        };
134
135        let (actual_audio_in, actual_audio_out, actual_midi_in, actual_midi_out) = unsafe {
136            let counts =
137                maolan_plugin_protocol::protocol::read_port_counts_from_scratch(mapping.as_ptr());
138
139            counts.unwrap_or((input_count as u32, output_count as u32, 0, 0))
140        };
141
142        let audio_inputs = (0..actual_audio_in as usize)
143            .map(|_| Arc::new(AudioIO::new(buffer_size)))
144            .collect::<Vec<_>>();
145        let audio_outputs = (0..actual_audio_out as usize)
146            .map(|_| Arc::new(AudioIO::new(buffer_size)))
147            .collect::<Vec<_>>();
148        let midi_input_ports = (0..actual_midi_in as usize)
149            .map(|_| Arc::new(MIDIIO::new()))
150            .collect::<Vec<_>>();
151        let midi_output_ports = (0..actual_midi_out as usize)
152            .map(|_| Arc::new(MIDIIO::new()))
153            .collect::<Vec<_>>();
154
155        let param_infos = Self::fetch_parameter_infos(&mapping, &events).unwrap_or_else(|e| {
156            tracing::warn!("Failed to fetch CLAP parameter infos: {e}");
157            Vec::new()
158        });
159        let param_values = param_infos
160            .iter()
161            .map(|info| (info.id, AtomicU64::new(info.default_value.to_bits())))
162            .collect();
163
164        Ok(Self {
165            path: plugin_spec.to_string(),
166            plugin_id: plugin_id.to_string(),
167            name,
168            buffer_size,
169            audio_inputs: ArcSwap::from_pointee(audio_inputs),
170            audio_outputs: ArcSwap::from_pointee(audio_outputs),
171            main_audio_inputs: AtomicUsize::new(actual_audio_in as usize),
172            main_audio_outputs: AtomicUsize::new(actual_audio_out as usize),
173            midi_input_count: actual_midi_in as usize,
174            midi_output_count: actual_midi_out as usize,
175            midi_input_ports,
176            midi_output_ports,
177            param_infos,
178            param_values,
179            bypassed: Arc::new(AtomicBool::new(false)),
180            child: UnsafeCell::new(Some(child)),
181            stderr: ArcSwapOption::from_pointee(stderr),
182            mapping: Some(mapping),
183            events: Some(events),
184            shm_name,
185            crash_count: AtomicU32::new(0),
186            last_latency_samples: AtomicUsize::new(0),
187            latency_changed: AtomicBool::new(false),
188        })
189    }
190
191    /// Access the host child process handle.
192    ///
193    /// # Safety
194    /// The caller must be the sole accessor of `child` at this time: either
195    /// the audio thread running this plugin's own plan task node (exactly one
196    /// per cycle), or control code running after the last `Arc` reference to
197    /// this processor is gone.
198    unsafe fn with_child<R>(&self, f: impl FnOnce(&mut Option<Child>) -> R) -> R {
199        f(unsafe { &mut *self.child.get() })
200    }
201
202    pub fn setup_audio_ports(&self) {
203        for port in self.audio_inputs() {
204            port.setup();
205        }
206        for port in self.audio_outputs() {
207            port.setup();
208        }
209    }
210
211    fn disconnect_all(port: &Arc<AudioIO>) {
212        let connections = port.connections();
213        for other in connections.iter() {
214            let _ = AudioIO::disconnect(other, port);
215        }
216    }
217
218    fn resize_audio_ports(ports: &mut Vec<Arc<AudioIO>>, len: usize, buffer_size: usize) {
219        if ports.len() > len {
220            for port in &ports[len..] {
221                Self::disconnect_all(port);
222            }
223            ports.truncate(len);
224        }
225        while ports.len() < len {
226            ports.push(Arc::new(AudioIO::new(buffer_size)));
227        }
228    }
229
230    fn refresh_audio_ports_from_scratch(&self, ptr: *mut u8) -> bool {
231        let Some((audio_in, audio_out, _, _)) = (unsafe { read_port_counts_from_scratch(ptr) })
232        else {
233            return false;
234        };
235        let audio_in = audio_in as usize;
236        let audio_out = audio_out as usize;
237        let mut changed = false;
238
239        let current_inputs = self.audio_inputs.load_full();
240        let current_input_len = current_inputs.len();
241        drop(current_inputs);
242        if current_input_len != audio_in
243            || self.main_audio_inputs.load(Ordering::Acquire) != audio_in
244        {
245            let mut inputs = self.audio_inputs.load_full().as_ref().clone();
246            Self::resize_audio_ports(&mut inputs, audio_in, self.buffer_size);
247            self.audio_inputs.store(Arc::new(inputs));
248            self.main_audio_inputs.store(audio_in, Ordering::Release);
249            changed = true;
250        }
251
252        let current_outputs = self.audio_outputs.load_full();
253        let current_output_len = current_outputs.len();
254        drop(current_outputs);
255        if current_output_len != audio_out
256            || self.main_audio_outputs.load(Ordering::Acquire) != audio_out
257        {
258            let mut outputs = self.audio_outputs.load_full().as_ref().clone();
259            Self::resize_audio_ports(&mut outputs, audio_out, self.buffer_size);
260            self.audio_outputs.store(Arc::new(outputs));
261            self.main_audio_outputs.store(audio_out, Ordering::Release);
262            changed = true;
263        }
264        changed
265    }
266
267    pub fn setup_midi_ports(&self) {
268        for port in &self.midi_input_ports {
269            // Safety: plan single-writer invariant — this task is the sole
270            // writer of its own ports this cycle; sources it reads were
271            // produced by earlier plan nodes (LOCKLESS.md Phase 3).
272            unsafe { port.setup() };
273        }
274        for port in &self.midi_output_ports {
275            // Safety: as above — sole writer of this port this cycle.
276            unsafe { port.setup() };
277        }
278    }
279
280    pub fn audio_inputs(&self) -> Vec<Arc<AudioIO>> {
281        self.audio_inputs.load_full().as_ref().clone()
282    }
283
284    pub fn audio_outputs(&self) -> Vec<Arc<AudioIO>> {
285        self.audio_outputs.load_full().as_ref().clone()
286    }
287
288    pub fn main_audio_input_count(&self) -> usize {
289        self.main_audio_inputs.load(Ordering::Acquire)
290    }
291
292    pub fn main_audio_output_count(&self) -> usize {
293        self.main_audio_outputs.load(Ordering::Acquire)
294    }
295
296    pub fn midi_input_count(&self) -> usize {
297        self.midi_input_count
298    }
299
300    pub fn midi_output_count(&self) -> usize {
301        self.midi_output_count
302    }
303
304    pub fn midi_input_ports(&self) -> &[Arc<MIDIIO>] {
305        &self.midi_input_ports
306    }
307
308    pub fn midi_output_ports(&self) -> &[Arc<MIDIIO>] {
309        &self.midi_output_ports
310    }
311
312    pub fn set_bypassed(&self, bypassed: bool) {
313        let previous = self.bypassed.swap(bypassed, Ordering::Relaxed);
314        if previous != bypassed {
315            self.latency_changed.store(true, Ordering::Release);
316        }
317    }
318
319    pub fn is_bypassed(&self) -> bool {
320        self.bypassed.load(Ordering::Relaxed)
321    }
322
323    pub fn latency_samples(&self) -> usize {
324        if self.bypassed.load(Ordering::Relaxed) {
325            let previous = self.last_latency_samples.swap(0, Ordering::AcqRel);
326            if previous != 0 {
327                self.latency_changed.store(true, Ordering::Release);
328            }
329            return 0;
330        }
331        let latency = self
332            .mapping
333            .as_ref()
334            .map(|mapping| unsafe {
335                latency_samples_atomic(mapping.as_ptr()).load(Ordering::Acquire) as usize
336            })
337            .unwrap_or(0);
338        let previous = self.last_latency_samples.swap(latency, Ordering::AcqRel);
339        if previous != latency {
340            self.latency_changed.store(true, Ordering::Release);
341        }
342        latency
343    }
344
345    pub fn take_latency_changed(&self) -> bool {
346        self.latency_changed.swap(false, Ordering::AcqRel)
347    }
348
349    pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
350        self.param_infos.clone()
351    }
352
353    fn fetch_parameter_infos(
354        mapping: &ShmMapping,
355        events: &EventPair,
356    ) -> Result<Vec<ClapParameterInfo>, String> {
357        let ptr = mapping.as_ptr();
358        let header = unsafe { header_mut(ptr) };
359
360        header
361            .request_type
362            .store(REQUEST_CLAP_PARAMETERS, Ordering::Release);
363        header.request_status.store(0, Ordering::Release);
364        if let Err(e) = events.signal_host() {
365            header.request_type.store(0, Ordering::Release);
366            return Err(format!("Failed to signal host for CLAP parameters: {e}"));
367        }
368
369        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
370            header.request_type.store(0, Ordering::Release);
371            return Err(format!("Host did not respond to CLAP parameters: {e}"));
372        }
373
374        let status = header.request_status.load(Ordering::Acquire);
375        let size = header.scratch_size.load(Ordering::Acquire) as usize;
376        if status != 1 {
377            header.request_type.store(0, Ordering::Release);
378            return Err("CLAP parameter enumeration failed in host".to_string());
379        }
380
381        let scratch = unsafe { scratch_ptr(ptr) };
382        let result = Self::deserialize_clap_parameters(scratch, size);
383        header.request_type.store(0, Ordering::Release);
384        result
385    }
386
387    fn deserialize_clap_parameters(
388        scratch: *const u8,
389        size: usize,
390    ) -> Result<Vec<ClapParameterInfo>, String> {
391        if size < 4 {
392            return Err("scratch too small for CLAP parameters".to_string());
393        }
394        let mut offset = 0usize;
395
396        let count = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
397        offset += 4;
398
399        let mut params = Vec::with_capacity(count);
400        for _ in 0..count {
401            if offset + 4 > size {
402                return Err("scratch underflow".to_string());
403            }
404            let id = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) };
405            offset += 4;
406
407            if offset + 4 > size {
408                return Err("scratch underflow".to_string());
409            }
410            let name_len =
411                unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
412            offset += 4;
413            if offset + name_len > size {
414                return Err("scratch underflow".to_string());
415            }
416            let mut name_bytes = vec![0u8; name_len];
417            unsafe {
418                std::ptr::copy_nonoverlapping(
419                    scratch.add(offset),
420                    name_bytes.as_mut_ptr(),
421                    name_len,
422                );
423            }
424            offset += name_len;
425            let name = String::from_utf8(name_bytes).map_err(|e| e.to_string())?;
426
427            if offset + 4 > size {
428                return Err("scratch underflow".to_string());
429            }
430            let module_len =
431                unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
432            offset += 4;
433            if offset + module_len > size {
434                return Err("scratch underflow".to_string());
435            }
436            let mut module_bytes = vec![0u8; module_len];
437            unsafe {
438                std::ptr::copy_nonoverlapping(
439                    scratch.add(offset),
440                    module_bytes.as_mut_ptr(),
441                    module_len,
442                );
443            }
444            offset += module_len;
445            let module = String::from_utf8(module_bytes).map_err(|e| e.to_string())?;
446
447            if offset + 24 > size {
448                return Err("scratch underflow".to_string());
449            }
450            let min_value = f64::from_bits(unsafe {
451                std::ptr::read_unaligned(scratch.add(offset) as *const u64)
452            });
453            let max_value = f64::from_bits(unsafe {
454                std::ptr::read_unaligned(scratch.add(offset + 8) as *const u64)
455            });
456            let default_value = f64::from_bits(unsafe {
457                std::ptr::read_unaligned(scratch.add(offset + 16) as *const u64)
458            });
459            offset += 24;
460
461            params.push(ClapParameterInfo {
462                id,
463                name,
464                module,
465                min_value,
466                max_value,
467                default_value,
468            });
469        }
470
471        Ok(params)
472    }
473
474    pub fn parameter_values(&self) -> HashMap<u32, f64> {
475        self.param_values
476            .iter()
477            .map(|(&id, value)| (id, f64::from_bits(value.load(Ordering::Relaxed))))
478            .collect()
479    }
480
481    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
482        self.set_parameter_at(param_id, value, 0)
483    }
484
485    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
486        if let Some(slot) = self.param_values.get(&param_id) {
487            slot.store(value.to_bits(), Ordering::Relaxed);
488        } else {
489            tracing::warn!("CLAP set_parameter_at: unknown parameter id {param_id}");
490        }
491
492        if let Some(ref mapping) = self.mapping {
493            let ring = unsafe {
494                let buf = param_ring_ptr(mapping.as_ptr());
495                let (w, r) = param_indices(mapping.as_ptr());
496                RingBuffer::new(buf, w, r, RING_CAPACITY)
497            };
498            let ev = ParameterEvent {
499                param_index: param_id,
500                value: value as f32,
501                sample_offset: 0,
502                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
503            };
504            if !ring.push(ev) {}
505        }
506        Ok(())
507    }
508
509    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
510        Ok(())
511    }
512
513    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
514        Ok(())
515    }
516
517    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
518        false
519    }
520
521    pub fn take_state_dirty(&self) -> bool {
522        let header = match self.mapping.as_ref() {
523            Some(m) => unsafe { header_mut(m.as_ptr()) },
524            None => return false,
525        };
526        header.state_dirty.swap(0, Ordering::Acquire) != 0
527    }
528
529    pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
530        let (mapping, events) = match (&self.mapping, &self.events) {
531            (Some(m), Some(e)) => (m, e),
532            _ => return Err("CLAP processor not initialized".to_string()),
533        };
534        let ptr = mapping.as_ptr();
535        let header = unsafe { header_mut(ptr) };
536
537        header.request_type.store(1, Ordering::Release);
538        header.request_status.store(0, Ordering::Release);
539        if let Err(e) = events.signal_host() {
540            header.request_type.store(0, Ordering::Release);
541            return Err(format!("Failed to signal host for state save: {e}"));
542        }
543
544        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
545            header.request_type.store(0, Ordering::Release);
546            return Err(format!("Host did not respond to state save: {e}"));
547        }
548
549        let status = header.request_status.load(Ordering::Acquire);
550        let size = header.scratch_size.load(Ordering::Acquire) as usize;
551        if status != 1 {
552            header.request_type.store(0, Ordering::Release);
553            return Err("State save failed in host".to_string());
554        }
555        if size > SCRATCH_SIZE {
556            header.request_type.store(0, Ordering::Release);
557            return Err(format!("Host returned invalid CLAP state size: {size}"));
558        }
559
560        let scratch = unsafe { scratch_ptr(ptr) };
561        let mut bytes = vec![0u8; size];
562        unsafe {
563            std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
564        }
565        header.request_type.store(0, Ordering::Release);
566        Ok(crate::plugins::types::ClapPluginState { bytes })
567    }
568
569    pub fn restore_state(
570        &self,
571        state: &crate::plugins::types::ClapPluginState,
572    ) -> Result<(), String> {
573        let (mapping, events) = match (&self.mapping, &self.events) {
574            (Some(m), Some(e)) => (m, e),
575            _ => return Err("CLAP processor not initialized".to_string()),
576        };
577        if state.bytes.len() > SCRATCH_SIZE {
578            return Err(format!(
579                "CLAP state is too large for scratch buffer: {} bytes",
580                state.bytes.len()
581            ));
582        }
583
584        let ptr = mapping.as_ptr();
585        let header = unsafe { header_mut(ptr) };
586        let scratch = unsafe { scratch_ptr(ptr) };
587        unsafe {
588            std::ptr::copy_nonoverlapping(state.bytes.as_ptr(), scratch, state.bytes.len());
589        }
590        header
591            .scratch_size
592            .store(state.bytes.len() as u32, Ordering::Release);
593
594        header.request_type.store(2, Ordering::Release);
595        header.request_status.store(0, Ordering::Release);
596        if let Err(e) = events.signal_host() {
597            header.request_type.store(0, Ordering::Release);
598            return Err(format!("Failed to signal host for state restore: {e}"));
599        }
600
601        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
602            header.request_type.store(0, Ordering::Release);
603            return Err(format!("Host did not respond to state restore: {e}"));
604        }
605
606        let status = header.request_status.load(Ordering::Acquire);
607        header.request_type.store(0, Ordering::Release);
608        if status != 1 {
609            return Err("State restore failed in host".to_string());
610        }
611        Ok(())
612    }
613
614    pub fn set_resource_directory(&self, dir: &std::path::Path) -> Result<(), String> {
615        let (mapping, events) = match (&self.mapping, &self.events) {
616            (Some(m), Some(e)) => (m, e),
617            _ => return Err("CLAP processor not initialized".to_string()),
618        };
619        let ptr = mapping.as_ptr();
620        let header = unsafe { header_mut(ptr) };
621        let path_str = dir.to_string_lossy().to_string();
622        unsafe {
623            write_resource_directory_to_scratch(ptr, &path_str)
624                .map_err(|e| format!("Failed to write resource directory: {e}"))?;
625        }
626        std::sync::atomic::fence(Ordering::SeqCst);
627
628        header.request_type.store(5, Ordering::Release);
629        header.request_status.store(0, Ordering::Release);
630        if let Err(e) = events.signal_host() {
631            header.request_type.store(0, Ordering::Release);
632            return Err(format!("Failed to signal host for resource directory: {e}"));
633        }
634
635        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
636            header.request_type.store(0, Ordering::Release);
637            return Err(format!("Host did not respond to resource directory: {e}"));
638        }
639
640        let status = header.request_status.load(Ordering::Acquire);
641        header.request_type.store(0, Ordering::Release);
642        if status != 1 {
643            return Err("Resource directory update failed in host".to_string());
644        }
645        Ok(())
646    }
647
648    pub fn file_references(
649        &self,
650    ) -> Result<Vec<maolan_plugin_protocol::protocol::FileReference>, String> {
651        let (mapping, events) = match (&self.mapping, &self.events) {
652            (Some(m), Some(e)) => (m, e),
653            _ => return Err("CLAP processor not initialized".to_string()),
654        };
655        let ptr = mapping.as_ptr();
656        let header = unsafe { header_mut(ptr) };
657
658        header.request_type.store(6, Ordering::Release);
659        header.request_status.store(0, Ordering::Release);
660        if let Err(e) = events.signal_host() {
661            header.request_type.store(0, Ordering::Release);
662            return Err(format!("Failed to signal host for file references: {e}"));
663        }
664
665        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
666            header.request_type.store(0, Ordering::Release);
667            return Err(format!("Host did not respond to file references: {e}"));
668        }
669
670        let status = header.request_status.load(Ordering::Acquire);
671        if status != 1 {
672            header.request_type.store(0, Ordering::Release);
673            return Err("File references enumeration failed in host".to_string());
674        }
675
676        let paths = unsafe { read_file_references_from_scratch(ptr) }
677            .ok_or("Failed to read file references from scratch")?;
678        header.request_type.store(0, Ordering::Release);
679        Ok(paths)
680    }
681
682    pub fn update_file_reference(&self, index: u32, path: &str) -> Result<(), String> {
683        let (mapping, events) = match (&self.mapping, &self.events) {
684            (Some(m), Some(e)) => (m, e),
685            _ => return Err("CLAP processor not initialized".to_string()),
686        };
687        let ptr = mapping.as_ptr();
688        let header = unsafe { header_mut(ptr) };
689        unsafe {
690            write_file_reference_update_to_scratch(ptr, index, path)
691                .map_err(|e| format!("Failed to write file-reference update: {e}"))?;
692        }
693
694        header.request_type.store(7, Ordering::Release);
695        header.request_status.store(0, Ordering::Release);
696        if let Err(e) = events.signal_host() {
697            header.request_type.store(0, Ordering::Release);
698            return Err(format!(
699                "Failed to signal host for file-reference update: {e}"
700            ));
701        }
702
703        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
704            header.request_type.store(0, Ordering::Release);
705            return Err(format!(
706                "Host did not respond to file-reference update: {e}"
707            ));
708        }
709
710        let status = header.request_status.load(Ordering::Acquire);
711        header.request_type.store(0, Ordering::Release);
712        if status != 1 {
713            return Err("File-reference update failed in host".to_string());
714        }
715        Ok(())
716    }
717
718    pub fn process_with_audio_buffers(
719        &self,
720        frames: usize,
721        midi_in: &[MidiEvent],
722        transport: ClapTransportInfo,
723        audio_inputs: &[&[f32]],
724        audio_outputs: &mut [&mut [f32]],
725    ) -> Vec<ClapMidiOutputEvent> {
726        if self.bypassed.load(Ordering::Relaxed) {
727            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
728            return Vec::new();
729        }
730
731        // Safety: the sole RT accessor of `child` is this processor's own
732        // plan task node, which runs exactly once per cycle.
733        let crashed = unsafe {
734            self.with_child(|child| {
735                if let Some(c) = child.as_mut()
736                    && let Ok(Some(status)) = c.try_wait()
737                    && !status.success()
738                {
739                    self.crash_count.fetch_add(1, Ordering::Relaxed);
740                    return true;
741                }
742                false
743            })
744        };
745        if crashed {
746            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
747            return Vec::new();
748        }
749
750        let (mapping, events) = match (&self.mapping, &self.events) {
751            (Some(m), Some(e)) => (m, e),
752            _ => {
753                ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
754                return Vec::new();
755            }
756        };
757
758        let ptr = mapping.as_ptr();
759        unsafe {
760            ipc::configure_shm_header(
761                ptr,
762                frames,
763                audio_inputs.len(),
764                audio_outputs.len(),
765                self.midi_input_ports.len(),
766                self.midi_output_ports.len(),
767            );
768            ipc::copy_input_slices_to_shm(audio_inputs, ptr, frames);
769
770            let t = transport_mut(ptr);
771            t.playhead_sample = transport.transport_sample as u64;
772            t.tempo = transport.bpm;
773            t.numerator = transport.tsig_num as u32;
774            t.denominator = transport.tsig_denom as u32;
775            t.flags = if transport.playing { 1 } else { 0 };
776
777            // Transitional: copy caller-supplied MIDI events into port 0 so
778            // existing engine scheduling keeps working until plugin MIDI
779            // connections are fully migrated to MIDIIO.
780            if let Some(port0) = self.midi_input_ports.first() {
781                // Safety: plan single-writer invariant — this task is the sole
782                // writer of its own ports this cycle; sources it reads were
783                // produced by earlier plan nodes (LOCKLESS.md Phase 3).
784                let mut buffer = port0.buffer_mut();
785                buffer.extend_from_slice(midi_in);
786                port0.mark_finished();
787            }
788
789            for (port_idx, port) in self.midi_input_ports.iter().enumerate() {
790                let midi_buf = midi_in_ring_ptr(ptr, port_idx);
791                let (midi_w, midi_r) = midi_in_indices(ptr, port_idx);
792                let midi_ring = RingBuffer::new(midi_buf, midi_w, midi_r, RING_CAPACITY);
793                // Safety: as above — sole writer this cycle; this read is of
794                // the port's own buffer, which no other node touches now.
795                let port_buffer = port.buffer();
796                for ev in port_buffer {
797                    let midi_event = maolan_plugin_protocol::protocol::MidiEvent {
798                        sample_offset: ev.frame,
799                        data: [
800                            ev.data.first().copied().unwrap_or(0),
801                            ev.data.get(1).copied().unwrap_or(0),
802                            ev.data.get(2).copied().unwrap_or(0),
803                        ],
804                        channel: ev.data.first().map(|b| b & 0x0F).unwrap_or(0),
805                        flags: 0,
806                        _pad: 0,
807                    };
808                    if !midi_ring.push(midi_event) {
809                        tracing::warn!(port = port_idx, "clap_proc MIDI ring full");
810                        break;
811                    }
812                }
813            }
814        }
815
816        if events.signal_host().is_err() {
817            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
818            return Vec::new();
819        }
820
821        let timeout = Duration::from_millis(100);
822        if events.wait_host(timeout).is_err() {
823            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
824            return Vec::new();
825        }
826
827        // Safety: same single-accessor invariant as the pre-process check.
828        let crashed = unsafe {
829            self.with_child(|child| {
830                if let Some(c) = child.as_mut()
831                    && let Ok(Some(status)) = c.try_wait()
832                    && !status.success()
833                {
834                    self.crash_count.fetch_add(1, Ordering::Relaxed);
835                    return true;
836                }
837                false
838            })
839        };
840        if crashed {
841            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
842            return Vec::new();
843        }
844
845        unsafe {
846            ipc::copy_outputs_from_shm_to_slices(audio_outputs, ptr, frames);
847        }
848
849        let mut midi_out = Vec::new();
850        unsafe {
851            for (port_idx, port) in self.midi_output_ports.iter().enumerate() {
852                // Safety: plan single-writer invariant — this task is the sole
853                // writer of its own ports this cycle (LOCKLESS.md Phase 3).
854                let mut port_buffer = port.buffer_mut();
855                port_buffer.clear();
856                let midi_out_buf = midi_out_ring_ptr(ptr, port_idx);
857                let (midi_out_w, midi_out_r) = midi_out_indices(ptr, port_idx);
858                let midi_out_ring =
859                    RingBuffer::new(midi_out_buf, midi_out_w, midi_out_r, RING_CAPACITY);
860                while let Some(ev) = midi_out_ring.pop() {
861                    let event = crate::midi::io::MidiEvent::new(ev.sample_offset, ev.data.to_vec());
862                    port_buffer.push(event.clone());
863                    midi_out.push(ClapMidiOutputEvent {
864                        port: port_idx,
865                        event,
866                    });
867                }
868                port.mark_finished();
869            }
870        }
871
872        midi_out
873    }
874
875    pub fn path(&self) -> &str {
876        &self.path
877    }
878
879    pub fn plugin_id(&self) -> &str {
880        &self.plugin_id
881    }
882
883    pub fn name(&self) -> &str {
884        &self.name
885    }
886
887    pub fn take_stderr(&self) -> Option<ChildStderr> {
888        // Control-side only: the EC is the sole accessor, so after `swap`
889        // the `Arc` is unique and `try_unwrap` cannot fail in practice.
890        self.stderr.swap(None).and_then(|s| Arc::try_unwrap(s).ok())
891    }
892
893    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
894        Ok(())
895    }
896
897    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
898        Ok(())
899    }
900
901    pub fn run_host_callbacks_main_thread(&self) {}
902
903    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
904        let Some(mapping) = self.mapping.as_ref() else {
905            return Ok(false);
906        };
907        Ok(self.refresh_audio_ports_from_scratch(mapping.as_ptr()))
908    }
909
910    pub fn refresh_audio_ports_from_host(&self) -> Result<bool, String> {
911        let (mapping, events) = match (&self.mapping, &self.events) {
912            (Some(mapping), Some(events)) => (mapping, events),
913            _ => return Ok(false),
914        };
915        let ptr = mapping.as_ptr();
916        let header = unsafe { header_mut(ptr) };
917        header
918            .request_type
919            .store(REQUEST_CLAP_AUDIO_PORTS, Ordering::Release);
920        header.request_status.store(0, Ordering::Release);
921        if let Err(e) = events.signal_host() {
922            header.request_type.store(0, Ordering::Release);
923            return Err(format!("Failed to signal host for CLAP audio ports: {e}"));
924        }
925
926        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
927            header.request_type.store(0, Ordering::Release);
928            return Err(format!("Host did not respond to CLAP audio ports: {e}"));
929        }
930
931        let status = header.request_status.load(Ordering::Acquire);
932        header.request_type.store(0, Ordering::Release);
933        if status != 1 {
934            return Err("CLAP audio port enumeration failed in host".to_string());
935        }
936        Ok(self.refresh_audio_ports_from_scratch(ptr))
937    }
938
939    pub fn ui_begin_session(&self) {}
940    pub fn ui_end_session(&self) {}
941    pub fn ui_should_close(&self) -> bool {
942        false
943    }
944    pub fn ui_take_due_timers(&self) -> Vec<u32> {
945        Vec::new()
946    }
947    pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
948        Vec::new()
949    }
950    pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
951        None
952    }
953
954    pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
955        Err("GUI not yet supported for CLAP plugins".to_string())
956    }
957
958    pub fn gui_create(&self, _api: &str, _is_floating: bool) -> Result<(), String> {
959        Err("GUI not yet supported for CLAP plugins".to_string())
960    }
961
962    pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
963        Err("GUI not yet supported for CLAP plugins".to_string())
964    }
965
966    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
967        if let Some(ref mapping) = self.mapping {
968            let header = unsafe { header_mut(mapping.as_ptr()) };
969            header.set_parent_window(window);
970            header.set_gui_parent_api(maolan_plugin_protocol::protocol::GuiParentApi::X11);
971            return Ok(());
972        }
973        Err("No active host to set parent window".to_string())
974    }
975
976    pub fn gui_set_parent_wayland(&self, window: usize) -> Result<(), String> {
977        if let Some(ref mapping) = self.mapping {
978            let header = unsafe { header_mut(mapping.as_ptr()) };
979            header.set_parent_window(window);
980            header.set_gui_parent_api(maolan_plugin_protocol::protocol::GuiParentApi::Wayland);
981            return Ok(());
982        }
983        Err("No active host to set parent window".to_string())
984    }
985
986    pub fn gui_set_floating_mode(&self, floating: bool) -> Result<(), String> {
987        if let Some(ref mapping) = self.mapping {
988            let header = unsafe { header_mut(mapping.as_ptr()) };
989            header.set_gui_mode(if floating {
990                GuiMode::Floating
991            } else {
992                GuiMode::Embedded
993            });
994            if floating {
995                header.set_parent_window(0);
996                header.set_gui_parent_api(maolan_plugin_protocol::protocol::GuiParentApi::None);
997            }
998            return Ok(());
999        }
1000        Err("No active host to set GUI mode".to_string())
1001    }
1002
1003    pub fn gui_show(&self) -> Result<(), String> {
1004        if let Some(ref mapping) = self.mapping
1005            && let Some(ref events) = self.events
1006        {
1007            let header = unsafe { header_mut(mapping.as_ptr()) };
1008            header.request_type.store(3, Ordering::Release);
1009            let _ = events.signal_host();
1010            return Ok(());
1011        }
1012        Err("No active host to show GUI".to_string())
1013    }
1014
1015    pub fn gui_hide(&self) {
1016        if let Some(ref mapping) = self.mapping
1017            && let Some(ref events) = self.events
1018        {
1019            let header = unsafe { header_mut(mapping.as_ptr()) };
1020            header.request_type.store(4, Ordering::Release);
1021            let _ = events.signal_host();
1022        }
1023    }
1024
1025    pub fn gui_destroy(&self) {}
1026
1027    pub fn gui_on_main_thread(&self) {}
1028
1029    pub fn gui_on_timer(&self, _timer_id: u32) {}
1030
1031    fn deserialize_clap_note_names(
1032        scratch: *const u8,
1033        size: usize,
1034    ) -> Result<HashMap<u8, String>, String> {
1035        if size < 4 {
1036            return Err("scratch too small for CLAP note names".to_string());
1037        }
1038        let mut offset = 0usize;
1039        let count = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
1040        offset += 4;
1041
1042        let mut note_names = HashMap::with_capacity(count);
1043        for _ in 0..count {
1044            if offset + 4 > size {
1045                return Err("scratch underflow".to_string());
1046            }
1047            let note = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) };
1048            offset += 4;
1049            if note > 127 {
1050                return Err(format!("CLAP note name key out of range: {note}"));
1051            }
1052
1053            if offset + 4 > size {
1054                return Err("scratch underflow".to_string());
1055            }
1056            let name_len =
1057                unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
1058            offset += 4;
1059            if offset + name_len > size {
1060                return Err("scratch underflow".to_string());
1061            }
1062            let mut name_bytes = vec![0u8; name_len];
1063            unsafe {
1064                std::ptr::copy_nonoverlapping(
1065                    scratch.add(offset),
1066                    name_bytes.as_mut_ptr(),
1067                    name_len,
1068                );
1069            }
1070            offset += name_len;
1071            let name = String::from_utf8(name_bytes).map_err(|e| e.to_string())?;
1072            note_names.insert(note as u8, name);
1073        }
1074
1075        Ok(note_names)
1076    }
1077
1078    pub fn note_names(&self) -> Result<HashMap<u8, String>, String> {
1079        let (mapping, events) = match (&self.mapping, &self.events) {
1080            (Some(m), Some(e)) => (m, e),
1081            _ => return Err("CLAP processor not initialized".to_string()),
1082        };
1083        let ptr = mapping.as_ptr();
1084        let header = unsafe { header_mut(ptr) };
1085
1086        header
1087            .request_type
1088            .store(REQUEST_CLAP_NOTE_NAMES, Ordering::Release);
1089        header.request_status.store(0, Ordering::Release);
1090        if let Err(e) = events.signal_host() {
1091            header.request_type.store(0, Ordering::Release);
1092            return Err(format!("Failed to signal host for CLAP note names: {e}"));
1093        }
1094
1095        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
1096            header.request_type.store(0, Ordering::Release);
1097            return Err(format!("Host did not respond to CLAP note names: {e}"));
1098        }
1099
1100        let status = header.request_status.load(Ordering::Acquire);
1101        let size = header.scratch_size.load(Ordering::Acquire) as usize;
1102        if status != 1 {
1103            header.request_type.store(0, Ordering::Release);
1104            return Err("CLAP note name enumeration failed in host".to_string());
1105        }
1106
1107        let scratch = unsafe { scratch_ptr(ptr) };
1108        let result = Self::deserialize_clap_note_names(scratch, size);
1109        header.request_type.store(0, Ordering::Release);
1110        result
1111    }
1112
1113    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
1114        let mut result = Vec::new();
1115        if let Some(ref mapping) = self.mapping {
1116            let ring = unsafe {
1117                let buf = echo_ring_ptr(mapping.as_ptr());
1118                let (w, r) = echo_indices(mapping.as_ptr());
1119                RingBuffer::new(buf, w, r, RING_CAPACITY)
1120            };
1121            while let Some(ev) = ring.pop() {
1122                result.push(ev);
1123            }
1124        }
1125        result
1126    }
1127
1128    pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
1129        let mut result = Vec::new();
1130        if let Some(ref mapping) = self.mapping {
1131            let ring = unsafe {
1132                let buf = midi_out_ring_ptr(mapping.as_ptr(), 0);
1133                let (w, r) = midi_out_indices(mapping.as_ptr(), 0);
1134                RingBuffer::new(buf, w, r, RING_CAPACITY)
1135            };
1136            while let Some(ev) = ring.pop() {
1137                result.push(crate::midi::io::MidiEvent {
1138                    frame: ev.sample_offset,
1139                    data: ev.data.to_vec(),
1140                });
1141            }
1142        }
1143        result
1144    }
1145}
1146
1147impl Drop for ClapProcessor {
1148    fn drop(&mut self) {
1149        let mapping = self.mapping.take();
1150        let events = self.events.take();
1151        let child = self.child.get_mut().take();
1152        let shm_name = std::mem::take(&mut self.shm_name);
1153        ipc::drop_host(mapping, events, child, shm_name);
1154    }
1155}
1156
1157fn split_plugin_spec(spec: &str) -> (&str, &str) {
1158    if let Some(pos) = spec.rfind("::") {
1159        (&spec[..pos], &spec[pos + 2..])
1160    } else if let Some(pos) = spec.rfind('#') {
1161        (&spec[..pos], &spec[pos + 1..])
1162    } else {
1163        (spec, "")
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170    use std::sync::Arc;
1171
1172    fn find_host_binary() -> PathBuf {
1173        let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
1174        let workspace_root = std::path::Path::new(&manifest)
1175            .parent()
1176            .unwrap()
1177            .join("maolan");
1178        workspace_root
1179            .join("target")
1180            .join("debug")
1181            .join("maolan-plugin-host")
1182    }
1183
1184    #[test]
1185    fn resize_audio_ports_disconnects_truncated_connected_ports() {
1186        let first = Arc::new(AudioIO::new(256));
1187        let second = Arc::new(AudioIO::new(256));
1188        let target = Arc::new(AudioIO::new(256));
1189        AudioIO::connect(&second, &target);
1190        let mut ports = vec![first, second.clone()];
1191
1192        ClapProcessor::resize_audio_ports(&mut ports, 1, 256);
1193
1194        assert_eq!(ports.len(), 1);
1195        assert!(second.connections().is_empty());
1196        assert!(target.connections().is_empty());
1197    }
1198
1199    #[cfg_attr(
1200        all(miri, target_os = "freebsd"),
1201        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1202    )]
1203    #[test]
1204    fn clap_processor_processes_audio() {
1205        let host_bin = find_host_binary();
1206        if !host_bin.exists() {
1207            return;
1208        }
1209
1210        let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1211            .parent()
1212            .unwrap()
1213            .join("daw")
1214            .join("plugin-host")
1215            .join("tests")
1216            .join("test_passthrough.clap");
1217
1218        if !plugin_path.exists() {
1219            return;
1220        }
1221
1222        let processor = ClapProcessor::new(
1223            48000.0,
1224            256,
1225            &format!("{}#com.maolan.test.passthrough", plugin_path.display()),
1226            2,
1227            2,
1228            host_bin,
1229        )
1230        .expect("should create processor");
1231
1232        processor.setup_audio_ports();
1233
1234        let input_buffers = (0..processor.audio_inputs().len())
1235            .map(|i| (0..256).map(|j| (i * 1000 + j) as f32).collect::<Vec<_>>())
1236            .collect::<Vec<_>>();
1237        let mut output_buffers = vec![vec![0.0; 256]; processor.audio_outputs().len()];
1238        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1239        let mut outputs = output_buffers
1240            .iter_mut()
1241            .map(Vec::as_mut_slice)
1242            .collect::<Vec<_>>();
1243        processor.process_with_audio_buffers(
1244            256,
1245            &[],
1246            ClapTransportInfo::default(),
1247            &inputs,
1248            &mut outputs,
1249        );
1250
1251        for output in output_buffers.iter() {
1252            assert!(
1253                output.iter().any(|&s| s != 0.0),
1254                "output buffer should contain non-zero samples"
1255            );
1256        }
1257    }
1258
1259    #[cfg_attr(
1260        all(miri, target_os = "freebsd"),
1261        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1262    )]
1263    #[test]
1264    fn clap_processor_crash_bypass() {
1265        let host_bin = find_host_binary();
1266        if !host_bin.exists() {
1267            return;
1268        }
1269
1270        let processor = ClapProcessor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
1271            .expect("should create processor for crash test");
1272
1273        processor.setup_audio_ports();
1274
1275        let input_buffers = [vec![1.0; 256]];
1276        let mut output_buffers = [vec![0.0; 256]];
1277        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1278        let mut outputs = output_buffers
1279            .iter_mut()
1280            .map(Vec::as_mut_slice)
1281            .collect::<Vec<_>>();
1282
1283        // Give the aborted host a moment to be reaped so the crash is visible.
1284        std::thread::sleep(std::time::Duration::from_millis(50));
1285
1286        processor.process_with_audio_buffers(
1287            256,
1288            &[],
1289            ClapTransportInfo::default(),
1290            &inputs,
1291            &mut outputs,
1292        );
1293
1294        assert!(
1295            output_buffers[0].iter().all(|&s| s == 1.0),
1296            "after crash, output should be bypass copy of input"
1297        );
1298    }
1299
1300    #[cfg_attr(
1301        all(miri, target_os = "freebsd"),
1302        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1303    )]
1304    #[test]
1305    fn clap_bypass_reports_zero_latency() {
1306        let host_bin = find_host_binary();
1307        if !host_bin.exists() {
1308            return;
1309        }
1310        let Ok(processor) = ClapProcessor::new(48000.0, 256, "__test__", 1, 1, host_bin) else {
1311            return;
1312        };
1313        let mapping = processor.mapping.as_ref().expect("mapping exists");
1314        unsafe {
1315            latency_samples_atomic(mapping.as_ptr()).store(128, Ordering::Release);
1316        }
1317
1318        assert_eq!(processor.latency_samples(), 128);
1319        processor.set_bypassed(true);
1320        assert_eq!(processor.latency_samples(), 0);
1321        assert!(processor.take_latency_changed());
1322    }
1323
1324    #[cfg_attr(
1325        all(miri, target_os = "freebsd"),
1326        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1327    )]
1328    #[test]
1329    fn clap_track_integration() {
1330        use crate::track::Track;
1331
1332        let host_bin = find_host_binary();
1333        if !host_bin.exists() {
1334            return;
1335        }
1336
1337        let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1338            .parent()
1339            .unwrap()
1340            .join("daw")
1341            .join("plugin-host")
1342            .join("tests")
1343            .join("test_passthrough.clap");
1344
1345        if !plugin_path.exists() {
1346            return;
1347        }
1348
1349        let mut track = Track::new("test-track".to_string(), 2, 2, 0, 0, 256, 48000.0);
1350
1351        track
1352            .load_clap_plugin(
1353                &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
1354                None,
1355            )
1356            .expect("should load CLAP plugin on track");
1357
1358        assert_eq!(track.clap_plugins.len(), 1);
1359
1360        let processor = track.clap_plugins[0].processor.clone();
1361        processor.setup_audio_ports();
1362
1363        let input_buffers = (0..processor.audio_inputs().len())
1364            .map(|i| (0..256).map(|j| (i * 1000 + j) as f32).collect::<Vec<_>>())
1365            .collect::<Vec<_>>();
1366        let mut output_buffers = vec![vec![0.0; 256]; processor.audio_outputs().len()];
1367        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1368        let mut outputs = output_buffers
1369            .iter_mut()
1370            .map(Vec::as_mut_slice)
1371            .collect::<Vec<_>>();
1372        processor.process_with_audio_buffers(
1373            256,
1374            &[],
1375            ClapTransportInfo::default(),
1376            &inputs,
1377            &mut outputs,
1378        );
1379
1380        for (ch, output) in output_buffers.iter().enumerate() {
1381            assert!(
1382                output.iter().any(|&s| s != 0.0),
1383                "plugin output ch={ch} should contain non-zero samples after CLAP processing"
1384            );
1385        }
1386    }
1387
1388    #[cfg_attr(
1389        all(miri, target_os = "freebsd"),
1390        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1391    )]
1392    #[test]
1393    fn clap_processor_forwards_midi_input_port_events_to_synth() {
1394        let host_bin = find_host_binary();
1395        if !host_bin.exists() {
1396            return;
1397        }
1398
1399        let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
1400        let plugin_path = std::path::Path::new(&manifest)
1401            .parent()
1402            .unwrap()
1403            .join("plugins")
1404            .join("target")
1405            .join("release")
1406            .join("libmaolan_plugins.so");
1407
1408        if !plugin_path.exists() {
1409            return;
1410        }
1411
1412        let processor = ClapProcessor::new(
1413            48000.0,
1414            256,
1415            &format!("{}::rs.maolan.synth", plugin_path.display()),
1416            0,
1417            2,
1418            host_bin,
1419        )
1420        .expect("should create Maolan Synth processor");
1421
1422        processor.setup_audio_ports();
1423
1424        let port0 = processor
1425            .midi_input_ports()
1426            .first()
1427            .expect("Maolan Synth should expose a MIDI input port");
1428        unsafe {
1429            let mut buffer = port0.buffer_mut();
1430            buffer.push(MidiEvent::new(0, vec![0x90, 48, 100]));
1431        }
1432
1433        let input_buffers: Vec<Vec<f32>> = vec![];
1434        let mut output_buffers = vec![vec![0.0; 256]; processor.audio_outputs().len()];
1435        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1436        let mut outputs = output_buffers
1437            .iter_mut()
1438            .map(Vec::as_mut_slice)
1439            .collect::<Vec<_>>();
1440
1441        processor.process_with_audio_buffers(
1442            256,
1443            &[],
1444            ClapTransportInfo::default(),
1445            &inputs,
1446            &mut outputs,
1447        );
1448
1449        let peak = output_buffers
1450            .iter()
1451            .flat_map(|ch| ch.iter().map(|&s| s.abs()))
1452            .fold(0.0f32, f32::max);
1453        assert!(
1454            peak > 0.001,
1455            "MIDI note-on did not reach the synth; output is silent (peak={})",
1456            peak
1457        );
1458    }
1459}