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