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