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