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