Skip to main content

maolan_engine/plugins/
clap_proc.rs

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}