1use crate::audio::io::AudioIO;
4use crate::midi::io::MidiEvent;
5use crate::mutex::UnsafeMutex;
6use crate::plugins::ipc;
7use crate::plugins::types::{
8 ClapMidiOutputEvent, ClapParamUpdate, ClapParameterInfo, ClapTransportInfo,
9};
10use maolan_plugin_protocol::events::EventPair;
11use maolan_plugin_protocol::protocol::*;
12use maolan_plugin_protocol::ringbuf::RingBuffer;
13use maolan_plugin_protocol::shm::ShmMapping;
14use std::collections::HashMap;
15use std::path::PathBuf;
16use std::process::Child;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::{Arc, atomic::AtomicU32};
19use std::time::{Duration, Instant};
20
21pub struct ClapProcessor {
23 path: String,
24 plugin_id: String,
25 name: String,
26 audio_inputs: Vec<Arc<AudioIO>>,
27 audio_outputs: Vec<Arc<AudioIO>>,
28 main_audio_inputs: usize,
29 main_audio_outputs: usize,
30 midi_inputs: usize,
31 midi_outputs: usize,
32 param_infos: Vec<ClapParameterInfo>,
33 param_values: UnsafeMutex<HashMap<u32, f64>>,
34 bypassed: Arc<AtomicBool>,
35 child: UnsafeMutex<Option<Child>>,
37 mapping: Option<ShmMapping>,
38 events: Option<EventPair>,
39 shm_name: String,
40 crash_count: AtomicU32,
42 last_process_time: UnsafeMutex<Instant>,
43}
44
45pub type SharedClapProcessor = Arc<UnsafeMutex<ClapProcessor>>;
46
47impl ClapProcessor {
48 pub fn new(
49 _sample_rate: f64,
50 buffer_size: usize,
51 plugin_spec: &str,
52 input_count: usize,
53 output_count: usize,
54 host_binary: PathBuf,
55 ) -> Result<Self, String> {
56 let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
57
58 let instance_id = ipc::unique_instance_id("clap");
60 let plugin_spec = if plugin_id.is_empty() {
61 plugin_path.to_string()
62 } else {
63 format!("{plugin_path}::{plugin_id}")
64 };
65 let (mut child, mapping, events, shm_name) = ipc::spawn_host(ipc::HostSpawnArgs {
66 host_binary: &host_binary,
67 format: "clap",
68 plugin_spec: &plugin_spec,
69 instance_id: &instance_id,
70 extra_args: &[],
71 })?;
72
73 let header = unsafe { header_ref(mapping.as_ptr()) };
74 if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
75 let _ = child.kill();
76 return Err("host did not signal ready".to_string());
77 }
78
79 let name = unsafe {
80 let mut name = None;
81 for _ in 0..50 {
82 name = maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(
83 mapping.as_ptr(),
84 );
85 if name.is_some() {
86 break;
87 }
88 std::thread::sleep(std::time::Duration::from_millis(10));
89 }
90 name.unwrap_or_else(|| plugin_id.to_string())
91 };
92
93 let (actual_audio_in, actual_audio_out, actual_midi_in, actual_midi_out) = unsafe {
95 let mut counts = None;
96 for _ in 0..50 {
97 counts = maolan_plugin_protocol::protocol::read_port_counts_from_scratch(
98 mapping.as_ptr(),
99 );
100 if counts.is_some() {
101 break;
102 }
103 std::thread::sleep(std::time::Duration::from_millis(10));
104 }
105 let result = counts.unwrap_or((input_count as u32, output_count as u32, 0, 0));
106 tracing::info!(
107 plugin = %plugin_spec,
108 audio_in = result.0,
109 audio_out = result.1,
110 midi_in = result.2,
111 midi_out = result.3,
112 from_host = counts.is_some(),
113 "CLAP processor port counts"
114 );
115 result
116 };
117
118 let audio_inputs = (0..actual_audio_in as usize)
119 .map(|_| Arc::new(AudioIO::new(buffer_size)))
120 .collect::<Vec<_>>();
121 let audio_outputs = (0..actual_audio_out as usize)
122 .map(|_| Arc::new(AudioIO::new(buffer_size)))
123 .collect::<Vec<_>>();
124
125 let param_infos = Vec::new();
128
129 Ok(Self {
130 path: plugin_spec.to_string(),
131 plugin_id: plugin_id.to_string(),
132 name,
133 audio_inputs,
134 audio_outputs,
135 main_audio_inputs: actual_audio_in as usize,
136 main_audio_outputs: actual_audio_out as usize,
137 midi_inputs: actual_midi_in as usize,
138 midi_outputs: actual_midi_out as usize,
139 param_infos,
140 param_values: UnsafeMutex::new(HashMap::new()),
141 bypassed: Arc::new(AtomicBool::new(false)),
142 child: UnsafeMutex::new(Some(child)),
143 mapping: Some(mapping),
144 events: Some(events),
145 shm_name,
146 crash_count: AtomicU32::new(0),
147 last_process_time: UnsafeMutex::new(Instant::now()),
148 })
149 }
150
151 pub fn setup_audio_ports(&self) {
152 for port in &self.audio_inputs {
153 port.setup();
154 }
155 for port in &self.audio_outputs {
156 port.setup();
157 }
158 }
159
160 pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
161 &self.audio_inputs
162 }
163
164 pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
165 &self.audio_outputs
166 }
167
168 pub fn main_audio_input_count(&self) -> usize {
169 self.main_audio_inputs
170 }
171
172 pub fn main_audio_output_count(&self) -> usize {
173 self.main_audio_outputs
174 }
175
176 pub fn midi_input_count(&self) -> usize {
177 self.midi_inputs
178 }
179
180 pub fn midi_output_count(&self) -> usize {
181 self.midi_outputs
182 }
183
184 pub fn set_bypassed(&self, bypassed: bool) {
185 self.bypassed.store(bypassed, Ordering::Relaxed);
186 }
187
188 pub fn is_bypassed(&self) -> bool {
189 self.bypassed.load(Ordering::Relaxed)
190 }
191
192 pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
193 self.param_infos.clone()
194 }
195
196 pub fn parameter_values(&self) -> HashMap<u32, f64> {
197 self.param_values.lock().clone()
198 }
199
200 pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
201 self.set_parameter_at(param_id, value, 0)
202 }
203
204 pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
205 self.param_values.lock().insert(param_id, value);
206 if let Some(ref mapping) = self.mapping {
208 let ring = unsafe {
209 let buf = param_ring_ptr(mapping.as_ptr());
210 let (w, r) = param_indices(mapping.as_ptr());
211 RingBuffer::new(buf, w, r, RING_CAPACITY)
212 };
213 let ev = ParameterEvent {
214 param_index: param_id,
215 value: value as f32,
216 sample_offset: 0,
217 event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
218 };
219 if !ring.push(ev) {
220 tracing::warn!("param ring full, dropping parameter event");
221 }
222 }
223 Ok(())
224 }
225
226 pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
227 Ok(())
228 }
229
230 pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
231 Ok(())
232 }
233
234 pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
235 false
236 }
237
238 pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
239 let (mapping, events) = match (&self.mapping, &self.events) {
240 (Some(m), Some(e)) => (m, e),
241 _ => return Err("CLAP processor not initialized".to_string()),
242 };
243 let ptr = mapping.as_ptr();
244 let header = unsafe { header_mut(ptr) };
245
246 header.request_type.store(1, Ordering::Release);
247 header.request_status.store(0, Ordering::Release);
248 if let Err(e) = events.signal_host() {
249 header.request_type.store(0, Ordering::Release);
250 return Err(format!("Failed to signal host for state save: {e}"));
251 }
252
253 if let Err(e) = events.wait_host(Duration::from_secs(5)) {
254 header.request_type.store(0, Ordering::Release);
255 return Err(format!("Host did not respond to state save: {e}"));
256 }
257
258 let status = header.request_status.load(Ordering::Acquire);
259 let size = header.scratch_size.load(Ordering::Acquire) as usize;
260 if status != 1 {
261 header.request_type.store(0, Ordering::Release);
262 return Err("State save failed in host".to_string());
263 }
264 if size > SCRATCH_SIZE {
265 header.request_type.store(0, Ordering::Release);
266 return Err(format!("Host returned invalid CLAP state size: {size}"));
267 }
268
269 let scratch = unsafe { scratch_ptr(ptr) };
270 let mut bytes = vec![0u8; size];
271 unsafe {
272 std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
273 }
274 header.request_type.store(0, Ordering::Release);
275 Ok(crate::plugins::types::ClapPluginState { bytes })
276 }
277
278 pub fn restore_state(
279 &self,
280 state: &crate::plugins::types::ClapPluginState,
281 ) -> Result<(), String> {
282 let (mapping, events) = match (&self.mapping, &self.events) {
283 (Some(m), Some(e)) => (m, e),
284 _ => return Err("CLAP processor not initialized".to_string()),
285 };
286 if state.bytes.len() > SCRATCH_SIZE {
287 return Err(format!(
288 "CLAP state is too large for scratch buffer: {} bytes",
289 state.bytes.len()
290 ));
291 }
292
293 let ptr = mapping.as_ptr();
294 let header = unsafe { header_mut(ptr) };
295 let scratch = unsafe { scratch_ptr(ptr) };
296 unsafe {
297 std::ptr::copy_nonoverlapping(state.bytes.as_ptr(), scratch, state.bytes.len());
298 }
299 header
300 .scratch_size
301 .store(state.bytes.len() as u32, Ordering::Release);
302
303 header.request_type.store(2, Ordering::Release);
304 header.request_status.store(0, Ordering::Release);
305 if let Err(e) = events.signal_host() {
306 header.request_type.store(0, Ordering::Release);
307 return Err(format!("Failed to signal host for state restore: {e}"));
308 }
309
310 if let Err(e) = events.wait_host(Duration::from_secs(5)) {
311 header.request_type.store(0, Ordering::Release);
312 return Err(format!("Host did not respond to state restore: {e}"));
313 }
314
315 let status = header.request_status.load(Ordering::Acquire);
316 header.request_type.store(0, Ordering::Release);
317 if status != 1 {
318 return Err("State restore failed in host".to_string());
319 }
320 Ok(())
321 }
322
323 pub fn process_with_audio_io(&self, frames: usize) {
324 let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
325 }
326
327 pub fn process_with_midi(
328 &self,
329 frames: usize,
330 midi_in: &[MidiEvent],
331 transport: ClapTransportInfo,
332 ) -> Vec<ClapMidiOutputEvent> {
333 if self.bypassed.load(Ordering::Relaxed) {
334 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
335 return Vec::new();
336 }
337
338 {
339 let child = self.child.lock();
340 if let Some(ref mut c) = child.as_mut() {
341 match c.try_wait() {
342 Ok(Some(status)) if !status.success() => {
343 tracing::error!("plugin host crashed for '{}' ({})", self.name, self.path);
344 self.crash_count.fetch_add(1, Ordering::Relaxed);
345 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
346 return Vec::new();
347 }
348 _ => {}
349 }
350 }
351 }
352
353 let started = Instant::now();
354
355 let (mapping, events) = match (&self.mapping, &self.events) {
356 (Some(m), Some(e)) => (m, e),
357 _ => {
358 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
359 return Vec::new();
360 }
361 };
362
363 let ptr = mapping.as_ptr();
364 unsafe {
365 ipc::configure_shm_header(
366 ptr,
367 frames,
368 self.audio_inputs.len(),
369 self.audio_outputs.len(),
370 );
371 ipc::copy_inputs_to_shm(&self.audio_inputs, ptr, frames);
372
373 let t = transport_mut(ptr);
375 t.playhead_sample = transport.transport_sample as u64;
376 t.tempo = transport.bpm;
377 t.numerator = transport.tsig_num as u32;
378 t.denominator = transport.tsig_denom as u32;
379 t.flags = if transport.playing { 1 } else { 0 };
380
381 let midi_buf = midi_ring_ptr(ptr);
383 let (midi_w, midi_r) = midi_indices(ptr);
384 let midi_ring = RingBuffer::new(midi_buf, midi_w, midi_r, RING_CAPACITY);
385 if !midi_in.is_empty() {
386 eprintln!(
387 "[CLAP-PROC] {} forwarding {} MIDI events to host",
388 self.name,
389 midi_in.len()
390 );
391 }
392 for ev in midi_in {
393 let midi_event = maolan_plugin_protocol::protocol::MidiEvent {
394 sample_offset: ev.frame,
395 data: [
396 ev.data.first().copied().unwrap_or(0),
397 ev.data.get(1).copied().unwrap_or(0),
398 ev.data.get(2).copied().unwrap_or(0),
399 ],
400 channel: ev.data.first().map(|b| b & 0x0F).unwrap_or(0),
401 flags: 0,
402 _pad: 0,
403 };
404 if !midi_ring.push(midi_event) {
405 tracing::warn!(
406 "MIDI input ring full for '{}' ({}), dropping event",
407 self.name,
408 self.path
409 );
410 break;
411 }
412 }
413 }
414
415 if let Err(e) = events.signal_host() {
416 tracing::error!("Failed to signal host: {e}");
417 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
418 return Vec::new();
419 }
420
421 let timeout = Duration::from_millis(100);
422 if let Err(e) = events.wait_host(timeout) {
423 tracing::error!(
424 "host did not respond for '{}' ({}): {e}",
425 self.name,
426 self.path
427 );
428 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
429 return Vec::new();
430 }
431
432 unsafe {
433 ipc::copy_outputs_from_shm(&self.audio_outputs, ptr, frames);
434 }
435
436 let mut midi_out = Vec::new();
438 unsafe {
439 let midi_out_buf = midi_out_ring_ptr(ptr);
440 let (midi_out_w, midi_out_r) = midi_out_indices(ptr);
441 let midi_out_ring =
442 RingBuffer::new(midi_out_buf, midi_out_w, midi_out_r, RING_CAPACITY);
443 while let Some(ev) = midi_out_ring.pop() {
444 midi_out.push(ClapMidiOutputEvent {
445 port: 0,
446 event: crate::midi::io::MidiEvent::new(ev.sample_offset, ev.data.to_vec()),
447 });
448 }
449 }
450
451 let elapsed = started.elapsed();
452 if elapsed > Duration::from_millis(20) {
453 tracing::warn!(
454 "Slow process '{}' ({}) took {:.3} ms for {} frames",
455 self.name,
456 self.path,
457 elapsed.as_secs_f64() * 1000.0,
458 frames
459 );
460 }
461
462 *self.last_process_time.lock() = Instant::now();
463 midi_out
464 }
465
466 pub fn path(&self) -> &str {
467 &self.path
468 }
469
470 pub fn plugin_id(&self) -> &str {
471 &self.plugin_id
472 }
473
474 pub fn name(&self) -> &str {
475 &self.name
476 }
477
478 pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
479 Ok(())
480 }
481
482 pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
483 Ok(())
484 }
485
486 pub fn run_host_callbacks_main_thread(&self) {}
487
488 pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
489 Ok(false)
490 }
491
492 pub fn ui_begin_session(&self) {}
493 pub fn ui_end_session(&self) {}
494 pub fn ui_should_close(&self) -> bool {
495 false
496 }
497 pub fn ui_take_due_timers(&self) -> Vec<u32> {
498 Vec::new()
499 }
500 pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
501 Vec::new()
502 }
503 pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
504 None
505 }
506
507 pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
508 Err("GUI not yet supported for CLAP plugins".to_string())
509 }
510
511 pub fn gui_create(&self, _api: &str, _is_floating: bool) -> Result<(), String> {
512 Err("GUI not yet supported for CLAP plugins".to_string())
513 }
514
515 pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
516 Err("GUI not yet supported for CLAP plugins".to_string())
517 }
518
519 pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
520 if let Some(ref mapping) = self.mapping {
521 let header = unsafe { header_mut(mapping.as_ptr()) };
522 header.set_parent_window(window);
523 return Ok(());
524 }
525 Err("No active host to set parent window".to_string())
526 }
527
528 pub fn gui_show(&self) -> Result<(), String> {
529 if let Some(ref mapping) = self.mapping
530 && let Some(ref events) = self.events
531 {
532 let header = unsafe { header_mut(mapping.as_ptr()) };
533 header.request_type.store(3, Ordering::Release);
534 let _ = events.signal_host();
535 return Ok(());
536 }
537 Err("No active host to show GUI".to_string())
538 }
539
540 pub fn gui_hide(&self) {
541 if let Some(ref mapping) = self.mapping
542 && let Some(ref events) = self.events
543 {
544 let header = unsafe { header_mut(mapping.as_ptr()) };
545 header.request_type.store(4, Ordering::Release);
546 let _ = events.signal_host();
547 }
548 }
549
550 pub fn gui_destroy(&self) {}
551
552 pub fn gui_on_main_thread(&self) {}
553
554 pub fn gui_on_timer(&self, _timer_id: u32) {}
555
556 pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
557 std::collections::HashMap::new()
558 }
559
560 pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
561 let mut result = Vec::new();
562 if let Some(ref mapping) = self.mapping {
563 let ring = unsafe {
564 let buf = echo_ring_ptr(mapping.as_ptr());
565 let (w, r) = echo_indices(mapping.as_ptr());
566 RingBuffer::new(buf, w, r, RING_CAPACITY)
567 };
568 while let Some(ev) = ring.pop() {
569 result.push(ev);
570 }
571 }
572 result
573 }
574
575 pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
576 let mut result = Vec::new();
577 if let Some(ref mapping) = self.mapping {
578 let ring = unsafe {
579 let buf = midi_out_ring_ptr(mapping.as_ptr());
580 let (w, r) = midi_out_indices(mapping.as_ptr());
581 RingBuffer::new(buf, w, r, RING_CAPACITY)
582 };
583 while let Some(ev) = ring.pop() {
584 result.push(crate::midi::io::MidiEvent {
585 frame: ev.sample_offset,
586 data: ev.data.to_vec(),
587 });
588 }
589 }
590 result
591 }
592}
593
594impl Drop for ClapProcessor {
595 fn drop(&mut self) {
596 ipc::drop_host(&self.mapping, &self.events, &self.child, &self.shm_name);
597 }
598}
599
600crate::impl_ipc_processor_wrapper!(ClapProcessor);
601
602impl UnsafeMutex<ClapProcessor> {
603 pub fn process_with_midi(
604 &self,
605 frames: usize,
606 midi_events: &[MidiEvent],
607 transport: ClapTransportInfo,
608 ) -> Vec<ClapMidiOutputEvent> {
609 self.lock()
610 .process_with_midi(frames, midi_events, transport)
611 }
612
613 pub fn is_bypassed(&self) -> bool {
614 self.lock().is_bypassed()
615 }
616
617 pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
618 self.lock().parameter_infos()
619 }
620
621 pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
622 self.lock().set_parameter(param_id, value)
623 }
624
625 pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
626 self.lock().set_parameter_at(param_id, value, frame)
627 }
628
629 pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
630 self.lock().begin_parameter_edit_at(param_id, frame)
631 }
632
633 pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
634 self.lock().end_parameter_edit_at(param_id, frame)
635 }
636
637 pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
638 self.lock().snapshot_state()
639 }
640
641 pub fn restore_state(
642 &self,
643 state: &crate::plugins::types::ClapPluginState,
644 ) -> Result<(), String> {
645 self.lock().restore_state(state)
646 }
647
648 pub fn path(&self) -> String {
649 self.lock().path().to_string()
650 }
651
652 pub fn plugin_id(&self) -> String {
653 self.lock().plugin_id().to_string()
654 }
655
656 pub fn ui_begin_session(&self) {
657 self.lock().ui_begin_session();
658 }
659
660 pub fn ui_end_session(&self) {
661 self.lock().ui_end_session();
662 }
663
664 pub fn ui_should_close(&self) -> bool {
665 self.lock().ui_should_close()
666 }
667
668 pub fn ui_take_due_timers(&self) -> Vec<u32> {
669 self.lock().ui_take_due_timers()
670 }
671
672 pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
673 self.lock().ui_take_param_updates()
674 }
675
676 pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
677 self.lock().ui_take_state_update()
678 }
679
680 pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
681 self.lock().gui_info()
682 }
683
684 pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
685 self.lock().gui_create(api, is_floating)
686 }
687
688 pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
689 self.lock().gui_get_size()
690 }
691
692 pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
693 self.lock().gui_set_parent_x11(window)
694 }
695
696 pub fn gui_show(&self) -> Result<(), String> {
697 self.lock().gui_show()
698 }
699
700 pub fn gui_hide(&self) {
701 self.lock().gui_hide();
702 }
703
704 pub fn gui_destroy(&self) {
705 self.lock().gui_destroy();
706 }
707
708 pub fn gui_on_main_thread(&self) {
709 self.lock().gui_on_main_thread();
710 }
711
712 pub fn gui_on_timer(&self, timer_id: u32) {
713 self.lock().gui_on_timer(timer_id);
714 }
715
716 pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
717 self.lock().note_names()
718 }
719}
720
721fn split_plugin_spec(spec: &str) -> (&str, &str) {
728 if let Some(pos) = spec.rfind("::") {
730 (&spec[..pos], &spec[pos + 2..])
731 } else if let Some(pos) = spec.rfind('#') {
732 (&spec[..pos], &spec[pos + 1..])
733 } else {
734 (spec, "")
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 fn find_host_binary() -> PathBuf {
743 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
744 let workspace_root = std::path::Path::new(&manifest)
745 .parent()
746 .unwrap()
747 .join("daw");
748 workspace_root
749 .join("target")
750 .join("debug")
751 .join("maolan-plugin-host")
752 }
753
754 #[test]
755 fn clap_processor_processes_audio() {
756 let host_bin = find_host_binary();
757 if !host_bin.exists() {
758 eprintln!(
759 "Skipping test: host binary not found at {}",
760 host_bin.display()
761 );
762 return;
763 }
764
765 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
766 .parent()
767 .unwrap()
768 .join("daw")
769 .join("plugin-host")
770 .join("tests")
771 .join("test_passthrough.clap");
772
773 if !plugin_path.exists() {
774 eprintln!(
775 "Skipping test: plugin not found at {}",
776 plugin_path.display()
777 );
778 return;
779 }
780
781 let processor = ClapProcessor::new(
782 48000.0,
783 256,
784 &format!("{}#com.maolan.test.passthrough", plugin_path.display()),
785 2,
786 2,
787 host_bin,
788 )
789 .expect("should create processor");
790
791 processor.setup_audio_ports();
792
793 for (i, input) in processor.audio_inputs().iter().enumerate() {
795 let buf = input.buffer.lock();
796 for (j, sample) in buf.iter_mut().enumerate() {
797 *sample = (i * 1000 + j) as f32;
798 }
799 *input.finished.lock() = true;
800 }
801
802 processor.process_with_audio_io(256);
804
805 for output in processor.audio_outputs().iter() {
807 let buf = output.buffer.lock();
808 assert!(
809 buf.iter().any(|&s| s != 0.0),
810 "output buffer should contain non-zero samples"
811 );
812 }
813
814 }
816
817 #[test]
818 fn clap_processor_crash_bypass() {
819 let host_bin = find_host_binary();
820 if !host_bin.exists() {
821 eprintln!("Skipping crash test: host binary not found");
822 return;
823 }
824
825 let processor = ClapProcessor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
827 .expect("should create processor for crash test");
828
829 processor.setup_audio_ports();
830
831 {
833 let buf = processor.audio_inputs()[0].buffer.lock();
834 buf.fill(1.0);
835 *processor.audio_inputs()[0].finished.lock() = true;
836 }
837
838 processor.process_with_audio_io(256);
840
841 let out_buf = processor.audio_outputs()[0].buffer.lock();
843 assert!(
844 out_buf.iter().all(|&s| s == 1.0),
845 "after crash, output should be bypass copy of input"
846 );
847 }
848
849 #[test]
850 fn clap_track_integration() {
851 use crate::track::Track;
852
853 let host_bin = find_host_binary();
854 if !host_bin.exists() {
855 eprintln!("Skipping track integration test: host binary not found");
856 return;
857 }
858
859 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
860 .parent()
861 .unwrap()
862 .join("daw")
863 .join("plugin-host")
864 .join("tests")
865 .join("test_passthrough.clap");
866
867 if !plugin_path.exists() {
868 eprintln!(
869 "Skipping track integration test: plugin not found at {}",
870 plugin_path.display()
871 );
872 return;
873 }
874
875 let mut track = Track::new("test-track".to_string(), 2, 2, 0, 0, 256, 48000.0);
876
877 track
878 .load_clap_plugin(
879 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
880 None,
881 )
882 .expect("should load CLAP plugin on track");
883
884 assert_eq!(track.clap_plugins.len(), 1);
885
886 let processor = track.clap_plugins[0].processor.lock();
890 processor.setup_audio_ports();
891
892 for (i, input) in processor.audio_inputs().iter().enumerate() {
893 let buf = input.buffer.lock();
894 for (j, sample) in buf.iter_mut().enumerate() {
895 *sample = (i * 1000 + j) as f32;
896 }
897 *input.finished.lock() = true;
898 }
899
900 processor.process_with_audio_io(256);
901
902 for (ch, output) in processor.audio_outputs().iter().enumerate() {
903 let buf = output.buffer.lock();
904 assert!(
905 buf.iter().any(|&s| s != 0.0),
906 "plugin output ch={ch} should contain non-zero samples after CLAP processing"
907 );
908 }
909 }
910}