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