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