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