Skip to main content

maolan_engine/plugins/
clap_proc.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::MidiEvent;
3use crate::mutex::UnsafeMutex;
4use crate::plugins::ipc;
5use crate::plugins::types::{
6    ClapMidiOutputEvent, ClapParamUpdate, ClapParameterInfo, ClapTransportInfo,
7};
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::collections::HashMap;
13use std::path::PathBuf;
14use std::process::{Child, ChildStderr};
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, atomic::AtomicU32};
17use std::time::{Duration, Instant};
18
19fn wait_for_host_request_complete(
20    header: &ShmHeader,
21    events: &EventPair,
22    timeout: Duration,
23) -> Result<(), String> {
24    let start = Instant::now();
25    loop {
26        // The host sets request_status before signalling and clears request_type
27        // after signalling. Check either condition so we don't miss the response
28        // if request_type is still non-zero when the wake-up arrives.
29        if header.request_type.load(Ordering::Acquire) == 0
30            || header.request_status.load(Ordering::Acquire) != 0
31        {
32            return Ok(());
33        }
34        let elapsed = start.elapsed();
35        if elapsed >= timeout {
36            return Err("Host did not respond to request".to_string());
37        }
38        if let Err(e) = events.wait_host(timeout - elapsed) {
39            return Err(format!("Host did not respond to request: {e}"));
40        }
41    }
42}
43
44pub struct ClapProcessor {
45    path: String,
46    plugin_id: String,
47    name: String,
48    audio_inputs: Vec<Arc<AudioIO>>,
49    audio_outputs: Vec<Arc<AudioIO>>,
50    main_audio_inputs: usize,
51    main_audio_outputs: usize,
52    midi_inputs: usize,
53    midi_outputs: usize,
54    param_infos: Vec<ClapParameterInfo>,
55    param_values: UnsafeMutex<HashMap<u32, f64>>,
56    bypassed: Arc<AtomicBool>,
57
58    child: UnsafeMutex<Option<Child>>,
59    stderr: UnsafeMutex<Option<ChildStderr>>,
60    mapping: Option<ShmMapping>,
61    events: Option<EventPair>,
62    shm_name: String,
63
64    crash_count: AtomicU32,
65    last_process_time: UnsafeMutex<Instant>,
66}
67
68pub type SharedClapProcessor = Arc<UnsafeMutex<ClapProcessor>>;
69
70impl ClapProcessor {
71    pub fn new(
72        _sample_rate: f64,
73        buffer_size: usize,
74        plugin_spec: &str,
75        input_count: usize,
76        output_count: usize,
77        host_binary: PathBuf,
78    ) -> Result<Self, String> {
79        let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
80
81        let instance_id = ipc::unique_instance_id("clap");
82        let plugin_spec = if plugin_id.is_empty() {
83            plugin_path.to_string()
84        } else {
85            format!("{plugin_path}::{plugin_id}")
86        };
87        let (mut child, mapping, events, shm_name, stderr) = ipc::spawn_host(ipc::HostSpawnArgs {
88            host_binary: &host_binary,
89            format: "clap",
90            plugin_spec: &plugin_spec,
91            instance_id: &instance_id,
92            extra_args: &[],
93        })?;
94
95        let header = unsafe { header_ref(mapping.as_ptr()) };
96        if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
97            let _ = child.kill();
98            return Err("host did not signal ready".to_string());
99        }
100
101        let name = unsafe {
102            maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(mapping.as_ptr())
103                .unwrap_or_else(|| plugin_id.to_string())
104        };
105
106        let (actual_audio_in, actual_audio_out, actual_midi_in, actual_midi_out) = unsafe {
107            let counts =
108                maolan_plugin_protocol::protocol::read_port_counts_from_scratch(mapping.as_ptr());
109
110            counts.unwrap_or((input_count as u32, output_count as u32, 0, 0))
111        };
112
113        let audio_inputs = (0..actual_audio_in as usize)
114            .map(|_| Arc::new(AudioIO::new(buffer_size)))
115            .collect::<Vec<_>>();
116        let audio_outputs = (0..actual_audio_out as usize)
117            .map(|_| Arc::new(AudioIO::new(buffer_size)))
118            .collect::<Vec<_>>();
119
120        let param_infos = Vec::new();
121
122        Ok(Self {
123            path: plugin_spec.to_string(),
124            plugin_id: plugin_id.to_string(),
125            name,
126            audio_inputs,
127            audio_outputs,
128            main_audio_inputs: actual_audio_in as usize,
129            main_audio_outputs: actual_audio_out as usize,
130            midi_inputs: actual_midi_in as usize,
131            midi_outputs: actual_midi_out as usize,
132            param_infos,
133            param_values: UnsafeMutex::new(HashMap::new()),
134            bypassed: Arc::new(AtomicBool::new(false)),
135            child: UnsafeMutex::new(Some(child)),
136            stderr: UnsafeMutex::new(stderr),
137            mapping: Some(mapping),
138            events: Some(events),
139            shm_name,
140            crash_count: AtomicU32::new(0),
141            last_process_time: UnsafeMutex::new(Instant::now()),
142        })
143    }
144
145    pub fn setup_audio_ports(&self) {
146        for port in &self.audio_inputs {
147            port.setup();
148        }
149        for port in &self.audio_outputs {
150            port.setup();
151        }
152    }
153
154    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
155        &self.audio_inputs
156    }
157
158    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
159        &self.audio_outputs
160    }
161
162    pub fn main_audio_input_count(&self) -> usize {
163        self.main_audio_inputs
164    }
165
166    pub fn main_audio_output_count(&self) -> usize {
167        self.main_audio_outputs
168    }
169
170    pub fn midi_input_count(&self) -> usize {
171        self.midi_inputs
172    }
173
174    pub fn midi_output_count(&self) -> usize {
175        self.midi_outputs
176    }
177
178    pub fn set_bypassed(&self, bypassed: bool) {
179        self.bypassed.store(bypassed, Ordering::Relaxed);
180    }
181
182    pub fn is_bypassed(&self) -> bool {
183        self.bypassed.load(Ordering::Relaxed)
184    }
185
186    pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
187        self.param_infos.clone()
188    }
189
190    pub fn parameter_values(&self) -> HashMap<u32, f64> {
191        self.param_values.lock().clone()
192    }
193
194    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
195        self.set_parameter_at(param_id, value, 0)
196    }
197
198    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
199        self.param_values.lock().insert(param_id, value);
200
201        if let Some(ref mapping) = self.mapping {
202            let ring = unsafe {
203                let buf = param_ring_ptr(mapping.as_ptr());
204                let (w, r) = param_indices(mapping.as_ptr());
205                RingBuffer::new(buf, w, r, RING_CAPACITY)
206            };
207            let ev = ParameterEvent {
208                param_index: param_id,
209                value: value as f32,
210                sample_offset: 0,
211                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
212            };
213            if !ring.push(ev) {}
214        }
215        Ok(())
216    }
217
218    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
219        Ok(())
220    }
221
222    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
223        Ok(())
224    }
225
226    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
227        false
228    }
229
230    pub fn take_state_dirty(&self) -> bool {
231        let header = match self.mapping.as_ref() {
232            Some(m) => unsafe { header_mut(m.as_ptr()) },
233            None => return false,
234        };
235        header.state_dirty.swap(0, Ordering::Acquire) != 0
236    }
237
238    pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
239        let (mapping, events) = match (&self.mapping, &self.events) {
240            (Some(m), Some(e)) => (m, e),
241            _ => return Err("CLAP processor not initialized".to_string()),
242        };
243        let ptr = mapping.as_ptr();
244        let header = unsafe { header_mut(ptr) };
245
246        header.request_type.store(1, Ordering::Release);
247        header.request_status.store(0, Ordering::Release);
248        if let Err(e) = events.signal_host() {
249            header.request_type.store(0, Ordering::Release);
250            return Err(format!("Failed to signal host for state save: {e}"));
251        }
252
253        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
254            header.request_type.store(0, Ordering::Release);
255            return Err(format!("Host did not respond to state save: {e}"));
256        }
257
258        let status = header.request_status.load(Ordering::Acquire);
259        let size = header.scratch_size.load(Ordering::Acquire) as usize;
260        if status != 1 {
261            header.request_type.store(0, Ordering::Release);
262            return Err("State save failed in host".to_string());
263        }
264        if size > SCRATCH_SIZE {
265            header.request_type.store(0, Ordering::Release);
266            return Err(format!("Host returned invalid CLAP state size: {size}"));
267        }
268
269        let scratch = unsafe { scratch_ptr(ptr) };
270        let mut bytes = vec![0u8; size];
271        unsafe {
272            std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
273        }
274        header.request_type.store(0, Ordering::Release);
275        Ok(crate::plugins::types::ClapPluginState { bytes })
276    }
277
278    pub fn restore_state(
279        &self,
280        state: &crate::plugins::types::ClapPluginState,
281    ) -> Result<(), String> {
282        let (mapping, events) = match (&self.mapping, &self.events) {
283            (Some(m), Some(e)) => (m, e),
284            _ => return Err("CLAP processor not initialized".to_string()),
285        };
286        if state.bytes.len() > SCRATCH_SIZE {
287            return Err(format!(
288                "CLAP state is too large for scratch buffer: {} bytes",
289                state.bytes.len()
290            ));
291        }
292
293        let ptr = mapping.as_ptr();
294        let header = unsafe { header_mut(ptr) };
295        let scratch = unsafe { scratch_ptr(ptr) };
296        unsafe {
297            std::ptr::copy_nonoverlapping(state.bytes.as_ptr(), scratch, state.bytes.len());
298        }
299        header
300            .scratch_size
301            .store(state.bytes.len() as u32, Ordering::Release);
302
303        header.request_type.store(2, Ordering::Release);
304        header.request_status.store(0, Ordering::Release);
305        if let Err(e) = events.signal_host() {
306            header.request_type.store(0, Ordering::Release);
307            return Err(format!("Failed to signal host for state restore: {e}"));
308        }
309
310        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
311            header.request_type.store(0, Ordering::Release);
312            return Err(format!("Host did not respond to state restore: {e}"));
313        }
314
315        let status = header.request_status.load(Ordering::Acquire);
316        header.request_type.store(0, Ordering::Release);
317        if status != 1 {
318            return Err("State restore failed in host".to_string());
319        }
320        Ok(())
321    }
322
323    pub fn set_resource_directory(&self, dir: &std::path::Path) -> Result<(), String> {
324        let (mapping, events) = match (&self.mapping, &self.events) {
325            (Some(m), Some(e)) => (m, e),
326            _ => return Err("CLAP processor not initialized".to_string()),
327        };
328        let ptr = mapping.as_ptr();
329        let header = unsafe { header_mut(ptr) };
330        let path_str = dir.to_string_lossy().to_string();
331        unsafe {
332            write_resource_directory_to_scratch(ptr, &path_str)
333                .map_err(|e| format!("Failed to write resource directory: {e}"))?;
334        }
335        std::sync::atomic::fence(Ordering::SeqCst);
336
337        header.request_type.store(5, Ordering::Release);
338        header.request_status.store(0, Ordering::Release);
339        if let Err(e) = events.signal_host() {
340            header.request_type.store(0, Ordering::Release);
341            return Err(format!("Failed to signal host for resource directory: {e}"));
342        }
343
344        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
345            header.request_type.store(0, Ordering::Release);
346            return Err(format!("Host did not respond to resource directory: {e}"));
347        }
348
349        let status = header.request_status.load(Ordering::Acquire);
350        header.request_type.store(0, Ordering::Release);
351        if status != 1 {
352            return Err("Resource directory update failed in host".to_string());
353        }
354        Ok(())
355    }
356
357    pub fn file_references(
358        &self,
359    ) -> Result<Vec<maolan_plugin_protocol::protocol::FileReference>, String> {
360        let (mapping, events) = match (&self.mapping, &self.events) {
361            (Some(m), Some(e)) => (m, e),
362            _ => return Err("CLAP processor not initialized".to_string()),
363        };
364        let ptr = mapping.as_ptr();
365        let header = unsafe { header_mut(ptr) };
366
367        header.request_type.store(6, Ordering::Release);
368        header.request_status.store(0, Ordering::Release);
369        if let Err(e) = events.signal_host() {
370            header.request_type.store(0, Ordering::Release);
371            return Err(format!("Failed to signal host for file references: {e}"));
372        }
373
374        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
375            header.request_type.store(0, Ordering::Release);
376            return Err(format!("Host did not respond to file references: {e}"));
377        }
378
379        let status = header.request_status.load(Ordering::Acquire);
380        if status != 1 {
381            header.request_type.store(0, Ordering::Release);
382            return Err("File references enumeration failed in host".to_string());
383        }
384
385        let paths = unsafe { read_file_references_from_scratch(ptr) }
386            .ok_or("Failed to read file references from scratch")?;
387        header.request_type.store(0, Ordering::Release);
388        Ok(paths)
389    }
390
391    pub fn update_file_reference(&self, index: u32, path: &str) -> Result<(), String> {
392        let (mapping, events) = match (&self.mapping, &self.events) {
393            (Some(m), Some(e)) => (m, e),
394            _ => return Err("CLAP processor not initialized".to_string()),
395        };
396        let ptr = mapping.as_ptr();
397        let header = unsafe { header_mut(ptr) };
398        unsafe {
399            write_file_reference_update_to_scratch(ptr, index, path)
400                .map_err(|e| format!("Failed to write file-reference update: {e}"))?;
401        }
402
403        header.request_type.store(7, Ordering::Release);
404        header.request_status.store(0, Ordering::Release);
405        if let Err(e) = events.signal_host() {
406            header.request_type.store(0, Ordering::Release);
407            return Err(format!(
408                "Failed to signal host for file-reference update: {e}"
409            ));
410        }
411
412        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
413            header.request_type.store(0, Ordering::Release);
414            return Err(format!(
415                "Host did not respond to file-reference update: {e}"
416            ));
417        }
418
419        let status = header.request_status.load(Ordering::Acquire);
420        header.request_type.store(0, Ordering::Release);
421        if status != 1 {
422            return Err("File-reference update failed in host".to_string());
423        }
424        Ok(())
425    }
426
427    pub fn process_with_audio_io(&self, frames: usize) {
428        let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
429    }
430
431    pub fn process_with_midi(
432        &self,
433        frames: usize,
434        midi_in: &[MidiEvent],
435        transport: ClapTransportInfo,
436    ) -> Vec<ClapMidiOutputEvent> {
437        if self.bypassed.load(Ordering::Relaxed) {
438            ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
439            return Vec::new();
440        }
441
442        {
443            let child = self.child.lock();
444            if let Some(ref mut c) = child.as_mut() {
445                match c.try_wait() {
446                    Ok(Some(status)) if !status.success() => {
447                        self.crash_count.fetch_add(1, Ordering::Relaxed);
448                        ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
449                        return Vec::new();
450                    }
451                    _ => {}
452                }
453            }
454        }
455
456        let (mapping, events) = match (&self.mapping, &self.events) {
457            (Some(m), Some(e)) => (m, e),
458            _ => {
459                ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
460                return Vec::new();
461            }
462        };
463
464        let ptr = mapping.as_ptr();
465        unsafe {
466            ipc::configure_shm_header(
467                ptr,
468                frames,
469                self.audio_inputs.len(),
470                self.audio_outputs.len(),
471            );
472            ipc::copy_inputs_to_shm(&self.audio_inputs, ptr, frames);
473
474            let t = transport_mut(ptr);
475            t.playhead_sample = transport.transport_sample as u64;
476            t.tempo = transport.bpm;
477            t.numerator = transport.tsig_num as u32;
478            t.denominator = transport.tsig_denom as u32;
479            t.flags = if transport.playing { 1 } else { 0 };
480
481            let midi_buf = midi_ring_ptr(ptr);
482            let (midi_w, midi_r) = midi_indices(ptr);
483            let midi_ring = RingBuffer::new(midi_buf, midi_w, midi_r, RING_CAPACITY);
484            for ev in midi_in {
485                let midi_event = maolan_plugin_protocol::protocol::MidiEvent {
486                    sample_offset: ev.frame,
487                    data: [
488                        ev.data.first().copied().unwrap_or(0),
489                        ev.data.get(1).copied().unwrap_or(0),
490                        ev.data.get(2).copied().unwrap_or(0),
491                    ],
492                    channel: ev.data.first().map(|b| b & 0x0F).unwrap_or(0),
493                    flags: 0,
494                    _pad: 0,
495                };
496                if !midi_ring.push(midi_event) {
497                    break;
498                }
499            }
500        }
501
502        if events.signal_host().is_err() {
503            ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
504            return Vec::new();
505        }
506
507        let timeout = Duration::from_millis(100);
508        if events.wait_host(timeout).is_err() {
509            ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
510            return Vec::new();
511        }
512
513        {
514            let child = self.child.lock();
515            if let Some(ref mut c) = child.as_mut()
516                && let Ok(Some(status)) = c.try_wait()
517                && !status.success()
518            {
519                self.crash_count.fetch_add(1, Ordering::Relaxed);
520                ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
521                return Vec::new();
522            }
523        }
524
525        unsafe {
526            ipc::copy_outputs_from_shm(&self.audio_outputs, ptr, frames);
527        }
528
529        let mut midi_out = Vec::new();
530        unsafe {
531            let midi_out_buf = midi_out_ring_ptr(ptr);
532            let (midi_out_w, midi_out_r) = midi_out_indices(ptr);
533            let midi_out_ring =
534                RingBuffer::new(midi_out_buf, midi_out_w, midi_out_r, RING_CAPACITY);
535            while let Some(ev) = midi_out_ring.pop() {
536                midi_out.push(ClapMidiOutputEvent {
537                    port: 0,
538                    event: crate::midi::io::MidiEvent::new(ev.sample_offset, ev.data.to_vec()),
539                });
540            }
541        }
542
543        *self.last_process_time.lock() = Instant::now();
544        midi_out
545    }
546
547    pub fn path(&self) -> &str {
548        &self.path
549    }
550
551    pub fn plugin_id(&self) -> &str {
552        &self.plugin_id
553    }
554
555    pub fn name(&self) -> &str {
556        &self.name
557    }
558
559    pub fn take_stderr(&self) -> Option<ChildStderr> {
560        self.stderr.lock().take()
561    }
562
563    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
564        Ok(())
565    }
566
567    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
568        Ok(())
569    }
570
571    pub fn run_host_callbacks_main_thread(&self) {}
572
573    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
574        Ok(false)
575    }
576
577    pub fn ui_begin_session(&self) {}
578    pub fn ui_end_session(&self) {}
579    pub fn ui_should_close(&self) -> bool {
580        false
581    }
582    pub fn ui_take_due_timers(&self) -> Vec<u32> {
583        Vec::new()
584    }
585    pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
586        Vec::new()
587    }
588    pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
589        None
590    }
591
592    pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
593        Err("GUI not yet supported for CLAP plugins".to_string())
594    }
595
596    pub fn gui_create(&self, _api: &str, _is_floating: bool) -> Result<(), String> {
597        Err("GUI not yet supported for CLAP plugins".to_string())
598    }
599
600    pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
601        Err("GUI not yet supported for CLAP plugins".to_string())
602    }
603
604    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
605        if let Some(ref mapping) = self.mapping {
606            let header = unsafe { header_mut(mapping.as_ptr()) };
607            header.set_parent_window(window);
608            return Ok(());
609        }
610        Err("No active host to set parent window".to_string())
611    }
612
613    pub fn gui_show(&self) -> Result<(), String> {
614        if let Some(ref mapping) = self.mapping
615            && let Some(ref events) = self.events
616        {
617            let header = unsafe { header_mut(mapping.as_ptr()) };
618            header.request_type.store(3, Ordering::Release);
619            let _ = events.signal_host();
620            return Ok(());
621        }
622        Err("No active host to show GUI".to_string())
623    }
624
625    pub fn gui_hide(&self) {
626        if let Some(ref mapping) = self.mapping
627            && let Some(ref events) = self.events
628        {
629            let header = unsafe { header_mut(mapping.as_ptr()) };
630            header.request_type.store(4, Ordering::Release);
631            let _ = events.signal_host();
632        }
633    }
634
635    pub fn gui_destroy(&self) {}
636
637    pub fn gui_on_main_thread(&self) {}
638
639    pub fn gui_on_timer(&self, _timer_id: u32) {}
640
641    pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
642        std::collections::HashMap::new()
643    }
644
645    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
646        let mut result = Vec::new();
647        if let Some(ref mapping) = self.mapping {
648            let ring = unsafe {
649                let buf = echo_ring_ptr(mapping.as_ptr());
650                let (w, r) = echo_indices(mapping.as_ptr());
651                RingBuffer::new(buf, w, r, RING_CAPACITY)
652            };
653            while let Some(ev) = ring.pop() {
654                result.push(ev);
655            }
656        }
657        result
658    }
659
660    pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
661        let mut result = Vec::new();
662        if let Some(ref mapping) = self.mapping {
663            let ring = unsafe {
664                let buf = midi_out_ring_ptr(mapping.as_ptr());
665                let (w, r) = midi_out_indices(mapping.as_ptr());
666                RingBuffer::new(buf, w, r, RING_CAPACITY)
667            };
668            while let Some(ev) = ring.pop() {
669                result.push(crate::midi::io::MidiEvent {
670                    frame: ev.sample_offset,
671                    data: ev.data.to_vec(),
672                });
673            }
674        }
675        result
676    }
677}
678
679impl Drop for ClapProcessor {
680    fn drop(&mut self) {
681        ipc::drop_host(&self.mapping, &self.events, &self.child, &self.shm_name);
682    }
683}
684
685crate::impl_ipc_processor_wrapper!(ClapProcessor);
686
687impl UnsafeMutex<ClapProcessor> {
688    pub fn process_with_midi(
689        &self,
690        frames: usize,
691        midi_events: &[MidiEvent],
692        transport: ClapTransportInfo,
693    ) -> Vec<ClapMidiOutputEvent> {
694        self.lock()
695            .process_with_midi(frames, midi_events, transport)
696    }
697
698    pub fn is_bypassed(&self) -> bool {
699        self.lock().is_bypassed()
700    }
701
702    pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
703        self.lock().parameter_infos()
704    }
705
706    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
707        self.lock().set_parameter(param_id, value)
708    }
709
710    pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
711        self.lock().set_parameter_at(param_id, value, frame)
712    }
713
714    pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
715        self.lock().begin_parameter_edit_at(param_id, frame)
716    }
717
718    pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
719        self.lock().end_parameter_edit_at(param_id, frame)
720    }
721
722    pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
723        self.lock().snapshot_state()
724    }
725
726    pub fn restore_state(
727        &self,
728        state: &crate::plugins::types::ClapPluginState,
729    ) -> Result<(), String> {
730        self.lock().restore_state(state)
731    }
732
733    pub fn take_state_dirty(&self) -> bool {
734        self.lock().take_state_dirty()
735    }
736
737    pub fn path(&self) -> String {
738        self.lock().path().to_string()
739    }
740
741    pub fn plugin_id(&self) -> String {
742        self.lock().plugin_id().to_string()
743    }
744
745    pub fn ui_begin_session(&self) {
746        self.lock().ui_begin_session();
747    }
748
749    pub fn ui_end_session(&self) {
750        self.lock().ui_end_session();
751    }
752
753    pub fn ui_should_close(&self) -> bool {
754        self.lock().ui_should_close()
755    }
756
757    pub fn ui_take_due_timers(&self) -> Vec<u32> {
758        self.lock().ui_take_due_timers()
759    }
760
761    pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
762        self.lock().ui_take_param_updates()
763    }
764
765    pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
766        self.lock().ui_take_state_update()
767    }
768
769    pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
770        self.lock().gui_info()
771    }
772
773    pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
774        self.lock().gui_create(api, is_floating)
775    }
776
777    pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
778        self.lock().gui_get_size()
779    }
780
781    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
782        self.lock().gui_set_parent_x11(window)
783    }
784
785    pub fn gui_show(&self) -> Result<(), String> {
786        self.lock().gui_show()
787    }
788
789    pub fn gui_hide(&self) {
790        self.lock().gui_hide();
791    }
792
793    pub fn gui_destroy(&self) {
794        self.lock().gui_destroy();
795    }
796
797    pub fn gui_on_main_thread(&self) {
798        self.lock().gui_on_main_thread();
799    }
800
801    pub fn gui_on_timer(&self, timer_id: u32) {
802        self.lock().gui_on_timer(timer_id);
803    }
804
805    pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
806        self.lock().note_names()
807    }
808}
809
810fn split_plugin_spec(spec: &str) -> (&str, &str) {
811    if let Some(pos) = spec.rfind("::") {
812        (&spec[..pos], &spec[pos + 2..])
813    } else if let Some(pos) = spec.rfind('#') {
814        (&spec[..pos], &spec[pos + 1..])
815    } else {
816        (spec, "")
817    }
818}
819
820#[cfg(test)]
821mod tests {
822    use super::*;
823
824    fn find_host_binary() -> PathBuf {
825        let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
826        let workspace_root = std::path::Path::new(&manifest)
827            .parent()
828            .unwrap()
829            .join("daw");
830        workspace_root
831            .join("target")
832            .join("debug")
833            .join("maolan-plugin-host")
834    }
835
836    #[test]
837    fn clap_processor_processes_audio() {
838        let host_bin = find_host_binary();
839        if !host_bin.exists() {
840            return;
841        }
842
843        let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
844            .parent()
845            .unwrap()
846            .join("daw")
847            .join("plugin-host")
848            .join("tests")
849            .join("test_passthrough.clap");
850
851        if !plugin_path.exists() {
852            return;
853        }
854
855        let processor = ClapProcessor::new(
856            48000.0,
857            256,
858            &format!("{}#com.maolan.test.passthrough", plugin_path.display()),
859            2,
860            2,
861            host_bin,
862        )
863        .expect("should create processor");
864
865        processor.setup_audio_ports();
866
867        for (i, input) in processor.audio_inputs().iter().enumerate() {
868            let buf = input.buffer.lock();
869            for (j, sample) in buf.iter_mut().enumerate() {
870                *sample = (i * 1000 + j) as f32;
871            }
872            *input.finished.lock() = true;
873        }
874
875        processor.process_with_audio_io(256);
876
877        for output in processor.audio_outputs().iter() {
878            let buf = output.buffer.lock();
879            assert!(
880                buf.iter().any(|&s| s != 0.0),
881                "output buffer should contain non-zero samples"
882            );
883        }
884    }
885
886    #[test]
887    fn clap_processor_crash_bypass() {
888        let host_bin = find_host_binary();
889        if !host_bin.exists() {
890            return;
891        }
892
893        let processor = ClapProcessor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
894            .expect("should create processor for crash test");
895
896        processor.setup_audio_ports();
897
898        {
899            let buf = processor.audio_inputs()[0].buffer.lock();
900            buf.fill(1.0);
901            *processor.audio_inputs()[0].finished.lock() = true;
902        }
903
904        // Give the aborted host a moment to be reaped so the crash is visible.
905        std::thread::sleep(std::time::Duration::from_millis(50));
906
907        processor.process_with_audio_io(256);
908
909        let out_buf = processor.audio_outputs()[0].buffer.lock();
910        assert!(
911            out_buf.iter().all(|&s| s == 1.0),
912            "after crash, output should be bypass copy of input"
913        );
914    }
915
916    #[test]
917    fn clap_track_integration() {
918        use crate::track::Track;
919
920        let host_bin = find_host_binary();
921        if !host_bin.exists() {
922            return;
923        }
924
925        let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
926            .parent()
927            .unwrap()
928            .join("daw")
929            .join("plugin-host")
930            .join("tests")
931            .join("test_passthrough.clap");
932
933        if !plugin_path.exists() {
934            return;
935        }
936
937        let mut track = Track::new("test-track".to_string(), 2, 2, 0, 0, 256, 48000.0);
938
939        track
940            .load_clap_plugin(
941                &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
942                None,
943            )
944            .expect("should load CLAP plugin on track");
945
946        assert_eq!(track.clap_plugins.len(), 1);
947
948        let processor = track.clap_plugins[0].processor.lock();
949        processor.setup_audio_ports();
950
951        for (i, input) in processor.audio_inputs().iter().enumerate() {
952            let buf = input.buffer.lock();
953            for (j, sample) in buf.iter_mut().enumerate() {
954                *sample = (i * 1000 + j) as f32;
955            }
956            *input.finished.lock() = true;
957        }
958
959        processor.process_with_audio_io(256);
960
961        for (ch, output) in processor.audio_outputs().iter().enumerate() {
962            let buf = output.buffer.lock();
963            assert!(
964                buf.iter().any(|&s| s != 0.0),
965                "plugin output ch={ch} should contain non-zero samples after CLAP processing"
966            );
967        }
968    }
969}