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 return Ok(());
880 }
881 Err("No active host to set parent window".to_string())
882 }
883
884 pub fn gui_set_floating_mode(&self, floating: bool) -> Result<(), String> {
885 if let Some(ref mapping) = self.mapping {
886 let header = unsafe { header_mut(mapping.as_ptr()) };
887 header.set_gui_mode(if floating {
888 GuiMode::Floating
889 } else {
890 GuiMode::Embedded
891 });
892 return Ok(());
893 }
894 Err("No active host to set GUI mode".to_string())
895 }
896
897 pub fn gui_show(&self) -> Result<(), String> {
898 if let Some(ref mapping) = self.mapping
899 && let Some(ref events) = self.events
900 {
901 let header = unsafe { header_mut(mapping.as_ptr()) };
902 header.request_type.store(3, Ordering::Release);
903 let _ = events.signal_host();
904 return Ok(());
905 }
906 Err("No active host to show GUI".to_string())
907 }
908
909 pub fn gui_hide(&self) {
910 if let Some(ref mapping) = self.mapping
911 && let Some(ref events) = self.events
912 {
913 let header = unsafe { header_mut(mapping.as_ptr()) };
914 header.request_type.store(4, Ordering::Release);
915 let _ = events.signal_host();
916 }
917 }
918
919 pub fn gui_destroy(&self) {}
920
921 pub fn gui_on_main_thread(&self) {}
922
923 pub fn gui_on_timer(&self, _timer_id: u32) {}
924
925 fn deserialize_clap_note_names(
926 scratch: *const u8,
927 size: usize,
928 ) -> Result<HashMap<u8, String>, String> {
929 if size < 4 {
930 return Err("scratch too small for CLAP note names".to_string());
931 }
932 let mut offset = 0usize;
933 let count = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
934 offset += 4;
935
936 let mut note_names = HashMap::with_capacity(count);
937 for _ in 0..count {
938 if offset + 4 > size {
939 return Err("scratch underflow".to_string());
940 }
941 let note = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) };
942 offset += 4;
943 if note > 127 {
944 return Err(format!("CLAP note name key out of range: {note}"));
945 }
946
947 if offset + 4 > size {
948 return Err("scratch underflow".to_string());
949 }
950 let name_len =
951 unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
952 offset += 4;
953 if offset + name_len > size {
954 return Err("scratch underflow".to_string());
955 }
956 let mut name_bytes = vec![0u8; name_len];
957 unsafe {
958 std::ptr::copy_nonoverlapping(
959 scratch.add(offset),
960 name_bytes.as_mut_ptr(),
961 name_len,
962 );
963 }
964 offset += name_len;
965 let name = String::from_utf8(name_bytes).map_err(|e| e.to_string())?;
966 note_names.insert(note as u8, name);
967 }
968
969 Ok(note_names)
970 }
971
972 pub fn note_names(&self) -> Result<HashMap<u8, String>, String> {
973 let (mapping, events) = match (&self.mapping, &self.events) {
974 (Some(m), Some(e)) => (m, e),
975 _ => return Err("CLAP processor not initialized".to_string()),
976 };
977 let ptr = mapping.as_ptr();
978 let header = unsafe { header_mut(ptr) };
979
980 header
981 .request_type
982 .store(REQUEST_CLAP_NOTE_NAMES, Ordering::Release);
983 header.request_status.store(0, Ordering::Release);
984 if let Err(e) = events.signal_host() {
985 header.request_type.store(0, Ordering::Release);
986 return Err(format!("Failed to signal host for CLAP note names: {e}"));
987 }
988
989 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
990 header.request_type.store(0, Ordering::Release);
991 return Err(format!("Host did not respond to CLAP note names: {e}"));
992 }
993
994 let status = header.request_status.load(Ordering::Acquire);
995 let size = header.scratch_size.load(Ordering::Acquire) as usize;
996 if status != 1 {
997 header.request_type.store(0, Ordering::Release);
998 return Err("CLAP note name enumeration failed in host".to_string());
999 }
1000
1001 let scratch = unsafe { scratch_ptr(ptr) };
1002 let result = Self::deserialize_clap_note_names(scratch, size);
1003 header.request_type.store(0, Ordering::Release);
1004 result
1005 }
1006
1007 pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
1008 let mut result = Vec::new();
1009 if let Some(ref mapping) = self.mapping {
1010 let ring = unsafe {
1011 let buf = echo_ring_ptr(mapping.as_ptr());
1012 let (w, r) = echo_indices(mapping.as_ptr());
1013 RingBuffer::new(buf, w, r, RING_CAPACITY)
1014 };
1015 while let Some(ev) = ring.pop() {
1016 result.push(ev);
1017 }
1018 }
1019 result
1020 }
1021
1022 pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
1023 let mut result = Vec::new();
1024 if let Some(ref mapping) = self.mapping {
1025 let ring = unsafe {
1026 let buf = midi_out_ring_ptr(mapping.as_ptr(), 0);
1027 let (w, r) = midi_out_indices(mapping.as_ptr(), 0);
1028 RingBuffer::new(buf, w, r, RING_CAPACITY)
1029 };
1030 while let Some(ev) = ring.pop() {
1031 result.push(crate::midi::io::MidiEvent {
1032 frame: ev.sample_offset,
1033 data: ev.data.to_vec(),
1034 });
1035 }
1036 }
1037 result
1038 }
1039}
1040
1041impl Drop for ClapProcessor {
1042 fn drop(&mut self) {
1043 let mapping = self.mapping.take();
1044 let events = self.events.take();
1045 let child = self.child.get_mut().take();
1046 let shm_name = std::mem::take(&mut self.shm_name);
1047 ipc::drop_host(mapping, events, child, shm_name);
1048 }
1049}
1050
1051fn split_plugin_spec(spec: &str) -> (&str, &str) {
1052 if let Some(pos) = spec.rfind("::") {
1053 (&spec[..pos], &spec[pos + 2..])
1054 } else if let Some(pos) = spec.rfind('#') {
1055 (&spec[..pos], &spec[pos + 1..])
1056 } else {
1057 (spec, "")
1058 }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063 use super::*;
1064
1065 fn find_host_binary() -> PathBuf {
1066 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
1067 let workspace_root = std::path::Path::new(&manifest)
1068 .parent()
1069 .unwrap()
1070 .join("daw");
1071 workspace_root
1072 .join("target")
1073 .join("debug")
1074 .join("maolan-plugin-host")
1075 }
1076
1077 #[cfg_attr(
1078 all(miri, target_os = "freebsd"),
1079 ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1080 )]
1081 #[test]
1082 fn clap_processor_processes_audio() {
1083 let host_bin = find_host_binary();
1084 if !host_bin.exists() {
1085 return;
1086 }
1087
1088 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1089 .parent()
1090 .unwrap()
1091 .join("daw")
1092 .join("plugin-host")
1093 .join("tests")
1094 .join("test_passthrough.clap");
1095
1096 if !plugin_path.exists() {
1097 return;
1098 }
1099
1100 let processor = ClapProcessor::new(
1101 48000.0,
1102 256,
1103 &format!("{}#com.maolan.test.passthrough", plugin_path.display()),
1104 2,
1105 2,
1106 host_bin,
1107 )
1108 .expect("should create processor");
1109
1110 processor.setup_audio_ports();
1111
1112 let input_buffers = (0..processor.audio_inputs().len())
1113 .map(|i| (0..256).map(|j| (i * 1000 + j) as f32).collect::<Vec<_>>())
1114 .collect::<Vec<_>>();
1115 let mut output_buffers = vec![vec![0.0; 256]; processor.audio_outputs().len()];
1116 let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1117 let mut outputs = output_buffers
1118 .iter_mut()
1119 .map(Vec::as_mut_slice)
1120 .collect::<Vec<_>>();
1121 processor.process_with_audio_buffers(
1122 256,
1123 &[],
1124 ClapTransportInfo::default(),
1125 &inputs,
1126 &mut outputs,
1127 );
1128
1129 for output in output_buffers.iter() {
1130 assert!(
1131 output.iter().any(|&s| s != 0.0),
1132 "output buffer should contain non-zero samples"
1133 );
1134 }
1135 }
1136
1137 #[cfg_attr(
1138 all(miri, target_os = "freebsd"),
1139 ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1140 )]
1141 #[test]
1142 fn clap_processor_crash_bypass() {
1143 let host_bin = find_host_binary();
1144 if !host_bin.exists() {
1145 return;
1146 }
1147
1148 let processor = ClapProcessor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
1149 .expect("should create processor for crash test");
1150
1151 processor.setup_audio_ports();
1152
1153 let input_buffers = [vec![1.0; 256]];
1154 let mut output_buffers = [vec![0.0; 256]];
1155 let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1156 let mut outputs = output_buffers
1157 .iter_mut()
1158 .map(Vec::as_mut_slice)
1159 .collect::<Vec<_>>();
1160
1161 std::thread::sleep(std::time::Duration::from_millis(50));
1163
1164 processor.process_with_audio_buffers(
1165 256,
1166 &[],
1167 ClapTransportInfo::default(),
1168 &inputs,
1169 &mut outputs,
1170 );
1171
1172 assert!(
1173 output_buffers[0].iter().all(|&s| s == 1.0),
1174 "after crash, output should be bypass copy of input"
1175 );
1176 }
1177
1178 #[cfg_attr(
1179 all(miri, target_os = "freebsd"),
1180 ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
1181 )]
1182 #[test]
1183 fn clap_track_integration() {
1184 use crate::track::Track;
1185
1186 let host_bin = find_host_binary();
1187 if !host_bin.exists() {
1188 return;
1189 }
1190
1191 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1192 .parent()
1193 .unwrap()
1194 .join("daw")
1195 .join("plugin-host")
1196 .join("tests")
1197 .join("test_passthrough.clap");
1198
1199 if !plugin_path.exists() {
1200 return;
1201 }
1202
1203 let mut track = Track::new("test-track".to_string(), 2, 2, 0, 0, 256, 48000.0);
1204
1205 track
1206 .load_clap_plugin(
1207 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
1208 None,
1209 )
1210 .expect("should load CLAP plugin on track");
1211
1212 assert_eq!(track.clap_plugins.len(), 1);
1213
1214 let processor = track.clap_plugins[0].processor.clone();
1215 processor.setup_audio_ports();
1216
1217 let input_buffers = (0..processor.audio_inputs().len())
1218 .map(|i| (0..256).map(|j| (i * 1000 + j) as f32).collect::<Vec<_>>())
1219 .collect::<Vec<_>>();
1220 let mut output_buffers = vec![vec![0.0; 256]; processor.audio_outputs().len()];
1221 let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
1222 let mut outputs = output_buffers
1223 .iter_mut()
1224 .map(Vec::as_mut_slice)
1225 .collect::<Vec<_>>();
1226 processor.process_with_audio_buffers(
1227 256,
1228 &[],
1229 ClapTransportInfo::default(),
1230 &inputs,
1231 &mut outputs,
1232 );
1233
1234 for (ch, output) in output_buffers.iter().enumerate() {
1235 assert!(
1236 output.iter().any(|&s| s != 0.0),
1237 "plugin output ch={ch} should contain non-zero samples after CLAP processing"
1238 );
1239 }
1240 }
1241}