Skip to main content

maolan_engine/plugins/
clap_proc.rs

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