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