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