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