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