Skip to main content

maolan_engine/plugins/
clap_proc.rs

1//! Out-of-process CLAP processor using `maolan-plugin-host` IPC.
2
3use 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
18/// Shared state for an out-of-process CLAP plugin instance.
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    param_infos: Vec<ClapParameterInfo>,
28    param_values: UnsafeMutex<HashMap<u32, f64>>,
29    bypassed: Arc<AtomicBool>,
30    // IPC state
31    child: UnsafeMutex<Option<Child>>,
32    mapping: Option<ShmMapping>,
33    events: Option<EventPair>,
34    shm_name: String,
35    // Crash recovery
36    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        // Spawn the host immediately so we can query params.
61        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        // Query parameter count from host via a simple param ring echo.
72        // For now, we use a minimal stub param list.
73        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 // Stub: MIDI not yet wired over IPC
122    }
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        // Write to param ring buffer if host is alive.
151        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        // Check if host process has crashed.
206        {
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        // We need mapping and events to proceed.
224        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        // Configure block size and channels for this call.
234        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        // Copy input AudioIO buffers to shared memory (bus 0).
245        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        // Signal host to process.
255        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        // Wait for host to complete (with timeout).
262        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        // Copy output shared memory (bus 1) back to AudioIO buffers.
274        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() // MIDI output stub
297    }
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
611/// Locate the `maolan-plugin-host` binary at runtime.
612///
613/// Search order:
614/// 1. Same directory as the current executable.
615/// 2. Workspace `target/debug` or `target/release` (development).
616/// 3. `PATH` environment variable.
617pub 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    // 1. Same directory as current executable.
623    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    // 2. Development workspace paths.
631    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    // 3. PATH.
648    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        // Fill input buffers with a ramp.
777        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        // Process one block.
786        processor.process_with_audio_io(256);
787
788        // Verify output buffers were written (non-zero).
789        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        // Processor is dropped here, which should gracefully shut down the host.
798    }
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        // Use the crash test mode.
809        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        // Fill input buffer.
815        {
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        // First process should trigger the crash; subsequent calls should bypass.
822        processor.process_with_audio_io(256);
823
824        // After crash, output should be a copy of input (bypass).
825        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        // Fill plugin inputs with a ramp and mark them ready.
870        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        // Process one block.
879        track.process();
880
881        // Verify the plugin's output buffers contain the passthrough signal.
882        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}