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