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