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
19fn wait_for_host_request_complete(
20 header: &ShmHeader,
21 events: &EventPair,
22 timeout: Duration,
23) -> Result<(), String> {
24 let start = Instant::now();
25 loop {
26 if header.request_type.load(Ordering::Acquire) == 0
30 || header.request_status.load(Ordering::Acquire) != 0
31 {
32 return Ok(());
33 }
34 let elapsed = start.elapsed();
35 if elapsed >= timeout {
36 return Err("Host did not respond to request".to_string());
37 }
38 if let Err(e) = events.wait_host(timeout - elapsed) {
39 return Err(format!("Host did not respond to request: {e}"));
40 }
41 }
42}
43
44pub struct ClapProcessor {
45 path: String,
46 plugin_id: String,
47 name: String,
48 audio_inputs: Vec<Arc<AudioIO>>,
49 audio_outputs: Vec<Arc<AudioIO>>,
50 main_audio_inputs: usize,
51 main_audio_outputs: usize,
52 midi_inputs: usize,
53 midi_outputs: usize,
54 param_infos: Vec<ClapParameterInfo>,
55 param_values: UnsafeMutex<HashMap<u32, f64>>,
56 bypassed: Arc<AtomicBool>,
57
58 child: UnsafeMutex<Option<Child>>,
59 stderr: UnsafeMutex<Option<ChildStderr>>,
60 mapping: Option<ShmMapping>,
61 events: Option<EventPair>,
62 shm_name: String,
63
64 crash_count: AtomicU32,
65 last_process_time: UnsafeMutex<Instant>,
66}
67
68pub type SharedClapProcessor = Arc<UnsafeMutex<ClapProcessor>>;
69
70impl ClapProcessor {
71 pub fn new(
72 _sample_rate: f64,
73 buffer_size: usize,
74 plugin_spec: &str,
75 input_count: usize,
76 output_count: usize,
77 host_binary: PathBuf,
78 ) -> Result<Self, String> {
79 let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
80
81 let instance_id = ipc::unique_instance_id("clap");
82 let plugin_spec = if plugin_id.is_empty() {
83 plugin_path.to_string()
84 } else {
85 format!("{plugin_path}::{plugin_id}")
86 };
87 let (mut child, mapping, events, shm_name, stderr) = ipc::spawn_host(ipc::HostSpawnArgs {
88 host_binary: &host_binary,
89 format: "clap",
90 plugin_spec: &plugin_spec,
91 instance_id: &instance_id,
92 extra_args: &[],
93 })?;
94
95 let header = unsafe { header_ref(mapping.as_ptr()) };
96 if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
97 let _ = child.kill();
98 return Err("host did not signal ready".to_string());
99 }
100
101 let name = unsafe {
102 maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(mapping.as_ptr())
103 .unwrap_or_else(|| plugin_id.to_string())
104 };
105
106 let (actual_audio_in, actual_audio_out, actual_midi_in, actual_midi_out) = unsafe {
107 let counts =
108 maolan_plugin_protocol::protocol::read_port_counts_from_scratch(mapping.as_ptr());
109
110 counts.unwrap_or((input_count as u32, output_count as u32, 0, 0))
111 };
112
113 let audio_inputs = (0..actual_audio_in as usize)
114 .map(|_| Arc::new(AudioIO::new(buffer_size)))
115 .collect::<Vec<_>>();
116 let audio_outputs = (0..actual_audio_out as usize)
117 .map(|_| Arc::new(AudioIO::new(buffer_size)))
118 .collect::<Vec<_>>();
119
120 let param_infos = Vec::new();
121
122 Ok(Self {
123 path: plugin_spec.to_string(),
124 plugin_id: plugin_id.to_string(),
125 name,
126 audio_inputs,
127 audio_outputs,
128 main_audio_inputs: actual_audio_in as usize,
129 main_audio_outputs: actual_audio_out as usize,
130 midi_inputs: actual_midi_in as usize,
131 midi_outputs: actual_midi_out as usize,
132 param_infos,
133 param_values: UnsafeMutex::new(HashMap::new()),
134 bypassed: Arc::new(AtomicBool::new(false)),
135 child: UnsafeMutex::new(Some(child)),
136 stderr: UnsafeMutex::new(stderr),
137 mapping: Some(mapping),
138 events: Some(events),
139 shm_name,
140 crash_count: AtomicU32::new(0),
141 last_process_time: UnsafeMutex::new(Instant::now()),
142 })
143 }
144
145 pub fn setup_audio_ports(&self) {
146 for port in &self.audio_inputs {
147 port.setup();
148 }
149 for port in &self.audio_outputs {
150 port.setup();
151 }
152 }
153
154 pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
155 &self.audio_inputs
156 }
157
158 pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
159 &self.audio_outputs
160 }
161
162 pub fn main_audio_input_count(&self) -> usize {
163 self.main_audio_inputs
164 }
165
166 pub fn main_audio_output_count(&self) -> usize {
167 self.main_audio_outputs
168 }
169
170 pub fn midi_input_count(&self) -> usize {
171 self.midi_inputs
172 }
173
174 pub fn midi_output_count(&self) -> usize {
175 self.midi_outputs
176 }
177
178 pub fn set_bypassed(&self, bypassed: bool) {
179 self.bypassed.store(bypassed, Ordering::Relaxed);
180 }
181
182 pub fn is_bypassed(&self) -> bool {
183 self.bypassed.load(Ordering::Relaxed)
184 }
185
186 pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
187 self.param_infos.clone()
188 }
189
190 pub fn parameter_values(&self) -> HashMap<u32, f64> {
191 self.param_values.lock().clone()
192 }
193
194 pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
195 self.set_parameter_at(param_id, value, 0)
196 }
197
198 pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
199 self.param_values.lock().insert(param_id, value);
200
201 if let Some(ref mapping) = self.mapping {
202 let ring = unsafe {
203 let buf = param_ring_ptr(mapping.as_ptr());
204 let (w, r) = param_indices(mapping.as_ptr());
205 RingBuffer::new(buf, w, r, RING_CAPACITY)
206 };
207 let ev = ParameterEvent {
208 param_index: param_id,
209 value: value as f32,
210 sample_offset: 0,
211 event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
212 };
213 if !ring.push(ev) {}
214 }
215 Ok(())
216 }
217
218 pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
219 Ok(())
220 }
221
222 pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
223 Ok(())
224 }
225
226 pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
227 false
228 }
229
230 pub fn take_state_dirty(&self) -> bool {
231 let header = match self.mapping.as_ref() {
232 Some(m) => unsafe { header_mut(m.as_ptr()) },
233 None => return false,
234 };
235 header.state_dirty.swap(0, Ordering::Acquire) != 0
236 }
237
238 pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
239 let (mapping, events) = match (&self.mapping, &self.events) {
240 (Some(m), Some(e)) => (m, e),
241 _ => return Err("CLAP processor not initialized".to_string()),
242 };
243 let ptr = mapping.as_ptr();
244 let header = unsafe { header_mut(ptr) };
245
246 header.request_type.store(1, Ordering::Release);
247 header.request_status.store(0, Ordering::Release);
248 if let Err(e) = events.signal_host() {
249 header.request_type.store(0, Ordering::Release);
250 return Err(format!("Failed to signal host for state save: {e}"));
251 }
252
253 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
254 header.request_type.store(0, Ordering::Release);
255 return Err(format!("Host did not respond to state save: {e}"));
256 }
257
258 let status = header.request_status.load(Ordering::Acquire);
259 let size = header.scratch_size.load(Ordering::Acquire) as usize;
260 if status != 1 {
261 header.request_type.store(0, Ordering::Release);
262 return Err("State save failed in host".to_string());
263 }
264 if size > SCRATCH_SIZE {
265 header.request_type.store(0, Ordering::Release);
266 return Err(format!("Host returned invalid CLAP state size: {size}"));
267 }
268
269 let scratch = unsafe { scratch_ptr(ptr) };
270 let mut bytes = vec![0u8; size];
271 unsafe {
272 std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
273 }
274 header.request_type.store(0, Ordering::Release);
275 Ok(crate::plugins::types::ClapPluginState { bytes })
276 }
277
278 pub fn restore_state(
279 &self,
280 state: &crate::plugins::types::ClapPluginState,
281 ) -> Result<(), String> {
282 let (mapping, events) = match (&self.mapping, &self.events) {
283 (Some(m), Some(e)) => (m, e),
284 _ => return Err("CLAP processor not initialized".to_string()),
285 };
286 if state.bytes.len() > SCRATCH_SIZE {
287 return Err(format!(
288 "CLAP state is too large for scratch buffer: {} bytes",
289 state.bytes.len()
290 ));
291 }
292
293 let ptr = mapping.as_ptr();
294 let header = unsafe { header_mut(ptr) };
295 let scratch = unsafe { scratch_ptr(ptr) };
296 unsafe {
297 std::ptr::copy_nonoverlapping(state.bytes.as_ptr(), scratch, state.bytes.len());
298 }
299 header
300 .scratch_size
301 .store(state.bytes.len() as u32, Ordering::Release);
302
303 header.request_type.store(2, Ordering::Release);
304 header.request_status.store(0, Ordering::Release);
305 if let Err(e) = events.signal_host() {
306 header.request_type.store(0, Ordering::Release);
307 return Err(format!("Failed to signal host for state restore: {e}"));
308 }
309
310 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
311 header.request_type.store(0, Ordering::Release);
312 return Err(format!("Host did not respond to state restore: {e}"));
313 }
314
315 let status = header.request_status.load(Ordering::Acquire);
316 header.request_type.store(0, Ordering::Release);
317 if status != 1 {
318 return Err("State restore failed in host".to_string());
319 }
320 Ok(())
321 }
322
323 pub fn set_resource_directory(&self, dir: &std::path::Path) -> Result<(), String> {
324 let (mapping, events) = match (&self.mapping, &self.events) {
325 (Some(m), Some(e)) => (m, e),
326 _ => return Err("CLAP processor not initialized".to_string()),
327 };
328 let ptr = mapping.as_ptr();
329 let header = unsafe { header_mut(ptr) };
330 let path_str = dir.to_string_lossy().to_string();
331 unsafe {
332 write_resource_directory_to_scratch(ptr, &path_str)
333 .map_err(|e| format!("Failed to write resource directory: {e}"))?;
334 }
335 std::sync::atomic::fence(Ordering::SeqCst);
336
337 header.request_type.store(5, Ordering::Release);
338 header.request_status.store(0, Ordering::Release);
339 if let Err(e) = events.signal_host() {
340 header.request_type.store(0, Ordering::Release);
341 return Err(format!("Failed to signal host for resource directory: {e}"));
342 }
343
344 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
345 header.request_type.store(0, Ordering::Release);
346 return Err(format!("Host did not respond to resource directory: {e}"));
347 }
348
349 let status = header.request_status.load(Ordering::Acquire);
350 header.request_type.store(0, Ordering::Release);
351 if status != 1 {
352 return Err("Resource directory update failed in host".to_string());
353 }
354 Ok(())
355 }
356
357 pub fn file_references(
358 &self,
359 ) -> Result<Vec<maolan_plugin_protocol::protocol::FileReference>, String> {
360 let (mapping, events) = match (&self.mapping, &self.events) {
361 (Some(m), Some(e)) => (m, e),
362 _ => return Err("CLAP processor not initialized".to_string()),
363 };
364 let ptr = mapping.as_ptr();
365 let header = unsafe { header_mut(ptr) };
366
367 header.request_type.store(6, Ordering::Release);
368 header.request_status.store(0, Ordering::Release);
369 if let Err(e) = events.signal_host() {
370 header.request_type.store(0, Ordering::Release);
371 return Err(format!("Failed to signal host for file references: {e}"));
372 }
373
374 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
375 header.request_type.store(0, Ordering::Release);
376 return Err(format!("Host did not respond to file references: {e}"));
377 }
378
379 let status = header.request_status.load(Ordering::Acquire);
380 if status != 1 {
381 header.request_type.store(0, Ordering::Release);
382 return Err("File references enumeration failed in host".to_string());
383 }
384
385 let paths = unsafe { read_file_references_from_scratch(ptr) }
386 .ok_or("Failed to read file references from scratch")?;
387 header.request_type.store(0, Ordering::Release);
388 Ok(paths)
389 }
390
391 pub fn update_file_reference(&self, index: u32, path: &str) -> Result<(), String> {
392 let (mapping, events) = match (&self.mapping, &self.events) {
393 (Some(m), Some(e)) => (m, e),
394 _ => return Err("CLAP processor not initialized".to_string()),
395 };
396 let ptr = mapping.as_ptr();
397 let header = unsafe { header_mut(ptr) };
398 unsafe {
399 write_file_reference_update_to_scratch(ptr, index, path)
400 .map_err(|e| format!("Failed to write file-reference update: {e}"))?;
401 }
402
403 header.request_type.store(7, Ordering::Release);
404 header.request_status.store(0, Ordering::Release);
405 if let Err(e) = events.signal_host() {
406 header.request_type.store(0, Ordering::Release);
407 return Err(format!(
408 "Failed to signal host for file-reference update: {e}"
409 ));
410 }
411
412 if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
413 header.request_type.store(0, Ordering::Release);
414 return Err(format!(
415 "Host did not respond to file-reference update: {e}"
416 ));
417 }
418
419 let status = header.request_status.load(Ordering::Acquire);
420 header.request_type.store(0, Ordering::Release);
421 if status != 1 {
422 return Err("File-reference update failed in host".to_string());
423 }
424 Ok(())
425 }
426
427 pub fn process_with_audio_io(&self, frames: usize) {
428 let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
429 }
430
431 pub fn process_with_midi(
432 &self,
433 frames: usize,
434 midi_in: &[MidiEvent],
435 transport: ClapTransportInfo,
436 ) -> Vec<ClapMidiOutputEvent> {
437 if self.bypassed.load(Ordering::Relaxed) {
438 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
439 return Vec::new();
440 }
441
442 {
443 let child = self.child.lock();
444 if let Some(ref mut c) = child.as_mut() {
445 match c.try_wait() {
446 Ok(Some(status)) if !status.success() => {
447 self.crash_count.fetch_add(1, Ordering::Relaxed);
448 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
449 return Vec::new();
450 }
451 _ => {}
452 }
453 }
454 }
455
456 let (mapping, events) = match (&self.mapping, &self.events) {
457 (Some(m), Some(e)) => (m, e),
458 _ => {
459 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
460 return Vec::new();
461 }
462 };
463
464 let ptr = mapping.as_ptr();
465 unsafe {
466 ipc::configure_shm_header(
467 ptr,
468 frames,
469 self.audio_inputs.len(),
470 self.audio_outputs.len(),
471 );
472 ipc::copy_inputs_to_shm(&self.audio_inputs, ptr, frames);
473
474 let t = transport_mut(ptr);
475 t.playhead_sample = transport.transport_sample as u64;
476 t.tempo = transport.bpm;
477 t.numerator = transport.tsig_num as u32;
478 t.denominator = transport.tsig_denom as u32;
479 t.flags = if transport.playing { 1 } else { 0 };
480
481 let midi_buf = midi_ring_ptr(ptr);
482 let (midi_w, midi_r) = midi_indices(ptr);
483 let midi_ring = RingBuffer::new(midi_buf, midi_w, midi_r, RING_CAPACITY);
484 for ev in midi_in {
485 let midi_event = maolan_plugin_protocol::protocol::MidiEvent {
486 sample_offset: ev.frame,
487 data: [
488 ev.data.first().copied().unwrap_or(0),
489 ev.data.get(1).copied().unwrap_or(0),
490 ev.data.get(2).copied().unwrap_or(0),
491 ],
492 channel: ev.data.first().map(|b| b & 0x0F).unwrap_or(0),
493 flags: 0,
494 _pad: 0,
495 };
496 if !midi_ring.push(midi_event) {
497 break;
498 }
499 }
500 }
501
502 if events.signal_host().is_err() {
503 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
504 return Vec::new();
505 }
506
507 let timeout = Duration::from_millis(100);
508 if events.wait_host(timeout).is_err() {
509 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
510 return Vec::new();
511 }
512
513 {
514 let child = self.child.lock();
515 if let Some(ref mut c) = child.as_mut()
516 && let Ok(Some(status)) = c.try_wait()
517 && !status.success()
518 {
519 self.crash_count.fetch_add(1, Ordering::Relaxed);
520 ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
521 return Vec::new();
522 }
523 }
524
525 unsafe {
526 ipc::copy_outputs_from_shm(&self.audio_outputs, ptr, frames);
527 }
528
529 let mut midi_out = Vec::new();
530 unsafe {
531 let midi_out_buf = midi_out_ring_ptr(ptr);
532 let (midi_out_w, midi_out_r) = midi_out_indices(ptr);
533 let midi_out_ring =
534 RingBuffer::new(midi_out_buf, midi_out_w, midi_out_r, RING_CAPACITY);
535 while let Some(ev) = midi_out_ring.pop() {
536 midi_out.push(ClapMidiOutputEvent {
537 port: 0,
538 event: crate::midi::io::MidiEvent::new(ev.sample_offset, ev.data.to_vec()),
539 });
540 }
541 }
542
543 *self.last_process_time.lock() = Instant::now();
544 midi_out
545 }
546
547 pub fn path(&self) -> &str {
548 &self.path
549 }
550
551 pub fn plugin_id(&self) -> &str {
552 &self.plugin_id
553 }
554
555 pub fn name(&self) -> &str {
556 &self.name
557 }
558
559 pub fn take_stderr(&self) -> Option<ChildStderr> {
560 self.stderr.lock().take()
561 }
562
563 pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
564 Ok(())
565 }
566
567 pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
568 Ok(())
569 }
570
571 pub fn run_host_callbacks_main_thread(&self) {}
572
573 pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
574 Ok(false)
575 }
576
577 pub fn ui_begin_session(&self) {}
578 pub fn ui_end_session(&self) {}
579 pub fn ui_should_close(&self) -> bool {
580 false
581 }
582 pub fn ui_take_due_timers(&self) -> Vec<u32> {
583 Vec::new()
584 }
585 pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
586 Vec::new()
587 }
588 pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
589 None
590 }
591
592 pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
593 Err("GUI not yet supported for CLAP plugins".to_string())
594 }
595
596 pub fn gui_create(&self, _api: &str, _is_floating: bool) -> Result<(), String> {
597 Err("GUI not yet supported for CLAP plugins".to_string())
598 }
599
600 pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
601 Err("GUI not yet supported for CLAP plugins".to_string())
602 }
603
604 pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
605 if let Some(ref mapping) = self.mapping {
606 let header = unsafe { header_mut(mapping.as_ptr()) };
607 header.set_parent_window(window);
608 return Ok(());
609 }
610 Err("No active host to set parent window".to_string())
611 }
612
613 pub fn gui_show(&self) -> Result<(), String> {
614 if let Some(ref mapping) = self.mapping
615 && let Some(ref events) = self.events
616 {
617 let header = unsafe { header_mut(mapping.as_ptr()) };
618 header.request_type.store(3, Ordering::Release);
619 let _ = events.signal_host();
620 return Ok(());
621 }
622 Err("No active host to show GUI".to_string())
623 }
624
625 pub fn gui_hide(&self) {
626 if let Some(ref mapping) = self.mapping
627 && let Some(ref events) = self.events
628 {
629 let header = unsafe { header_mut(mapping.as_ptr()) };
630 header.request_type.store(4, Ordering::Release);
631 let _ = events.signal_host();
632 }
633 }
634
635 pub fn gui_destroy(&self) {}
636
637 pub fn gui_on_main_thread(&self) {}
638
639 pub fn gui_on_timer(&self, _timer_id: u32) {}
640
641 pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
642 std::collections::HashMap::new()
643 }
644
645 pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
646 let mut result = Vec::new();
647 if let Some(ref mapping) = self.mapping {
648 let ring = unsafe {
649 let buf = echo_ring_ptr(mapping.as_ptr());
650 let (w, r) = echo_indices(mapping.as_ptr());
651 RingBuffer::new(buf, w, r, RING_CAPACITY)
652 };
653 while let Some(ev) = ring.pop() {
654 result.push(ev);
655 }
656 }
657 result
658 }
659
660 pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
661 let mut result = Vec::new();
662 if let Some(ref mapping) = self.mapping {
663 let ring = unsafe {
664 let buf = midi_out_ring_ptr(mapping.as_ptr());
665 let (w, r) = midi_out_indices(mapping.as_ptr());
666 RingBuffer::new(buf, w, r, RING_CAPACITY)
667 };
668 while let Some(ev) = ring.pop() {
669 result.push(crate::midi::io::MidiEvent {
670 frame: ev.sample_offset,
671 data: ev.data.to_vec(),
672 });
673 }
674 }
675 result
676 }
677}
678
679impl Drop for ClapProcessor {
680 fn drop(&mut self) {
681 ipc::drop_host(&self.mapping, &self.events, &self.child, &self.shm_name);
682 }
683}
684
685crate::impl_ipc_processor_wrapper!(ClapProcessor);
686
687impl UnsafeMutex<ClapProcessor> {
688 pub fn process_with_midi(
689 &self,
690 frames: usize,
691 midi_events: &[MidiEvent],
692 transport: ClapTransportInfo,
693 ) -> Vec<ClapMidiOutputEvent> {
694 self.lock()
695 .process_with_midi(frames, midi_events, transport)
696 }
697
698 pub fn is_bypassed(&self) -> bool {
699 self.lock().is_bypassed()
700 }
701
702 pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
703 self.lock().parameter_infos()
704 }
705
706 pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
707 self.lock().set_parameter(param_id, value)
708 }
709
710 pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
711 self.lock().set_parameter_at(param_id, value, frame)
712 }
713
714 pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
715 self.lock().begin_parameter_edit_at(param_id, frame)
716 }
717
718 pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
719 self.lock().end_parameter_edit_at(param_id, frame)
720 }
721
722 pub fn snapshot_state(&self) -> Result<crate::plugins::types::ClapPluginState, String> {
723 self.lock().snapshot_state()
724 }
725
726 pub fn restore_state(
727 &self,
728 state: &crate::plugins::types::ClapPluginState,
729 ) -> Result<(), String> {
730 self.lock().restore_state(state)
731 }
732
733 pub fn take_state_dirty(&self) -> bool {
734 self.lock().take_state_dirty()
735 }
736
737 pub fn path(&self) -> String {
738 self.lock().path().to_string()
739 }
740
741 pub fn plugin_id(&self) -> String {
742 self.lock().plugin_id().to_string()
743 }
744
745 pub fn ui_begin_session(&self) {
746 self.lock().ui_begin_session();
747 }
748
749 pub fn ui_end_session(&self) {
750 self.lock().ui_end_session();
751 }
752
753 pub fn ui_should_close(&self) -> bool {
754 self.lock().ui_should_close()
755 }
756
757 pub fn ui_take_due_timers(&self) -> Vec<u32> {
758 self.lock().ui_take_due_timers()
759 }
760
761 pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
762 self.lock().ui_take_param_updates()
763 }
764
765 pub fn ui_take_state_update(&self) -> Option<crate::plugins::types::ClapPluginState> {
766 self.lock().ui_take_state_update()
767 }
768
769 pub fn gui_info(&self) -> Result<crate::plugins::types::ClapGuiInfo, String> {
770 self.lock().gui_info()
771 }
772
773 pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
774 self.lock().gui_create(api, is_floating)
775 }
776
777 pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
778 self.lock().gui_get_size()
779 }
780
781 pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
782 self.lock().gui_set_parent_x11(window)
783 }
784
785 pub fn gui_show(&self) -> Result<(), String> {
786 self.lock().gui_show()
787 }
788
789 pub fn gui_hide(&self) {
790 self.lock().gui_hide();
791 }
792
793 pub fn gui_destroy(&self) {
794 self.lock().gui_destroy();
795 }
796
797 pub fn gui_on_main_thread(&self) {
798 self.lock().gui_on_main_thread();
799 }
800
801 pub fn gui_on_timer(&self, timer_id: u32) {
802 self.lock().gui_on_timer(timer_id);
803 }
804
805 pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
806 self.lock().note_names()
807 }
808}
809
810fn split_plugin_spec(spec: &str) -> (&str, &str) {
811 if let Some(pos) = spec.rfind("::") {
812 (&spec[..pos], &spec[pos + 2..])
813 } else if let Some(pos) = spec.rfind('#') {
814 (&spec[..pos], &spec[pos + 1..])
815 } else {
816 (spec, "")
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823
824 fn find_host_binary() -> PathBuf {
825 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
826 let workspace_root = std::path::Path::new(&manifest)
827 .parent()
828 .unwrap()
829 .join("daw");
830 workspace_root
831 .join("target")
832 .join("debug")
833 .join("maolan-plugin-host")
834 }
835
836 #[test]
837 fn clap_processor_processes_audio() {
838 let host_bin = find_host_binary();
839 if !host_bin.exists() {
840 return;
841 }
842
843 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
844 .parent()
845 .unwrap()
846 .join("daw")
847 .join("plugin-host")
848 .join("tests")
849 .join("test_passthrough.clap");
850
851 if !plugin_path.exists() {
852 return;
853 }
854
855 let processor = ClapProcessor::new(
856 48000.0,
857 256,
858 &format!("{}#com.maolan.test.passthrough", plugin_path.display()),
859 2,
860 2,
861 host_bin,
862 )
863 .expect("should create processor");
864
865 processor.setup_audio_ports();
866
867 for (i, input) in processor.audio_inputs().iter().enumerate() {
868 let buf = input.buffer.lock();
869 for (j, sample) in buf.iter_mut().enumerate() {
870 *sample = (i * 1000 + j) as f32;
871 }
872 *input.finished.lock() = true;
873 }
874
875 processor.process_with_audio_io(256);
876
877 for output in processor.audio_outputs().iter() {
878 let buf = output.buffer.lock();
879 assert!(
880 buf.iter().any(|&s| s != 0.0),
881 "output buffer should contain non-zero samples"
882 );
883 }
884 }
885
886 #[test]
887 fn clap_processor_crash_bypass() {
888 let host_bin = find_host_binary();
889 if !host_bin.exists() {
890 return;
891 }
892
893 let processor = ClapProcessor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
894 .expect("should create processor for crash test");
895
896 processor.setup_audio_ports();
897
898 {
899 let buf = processor.audio_inputs()[0].buffer.lock();
900 buf.fill(1.0);
901 *processor.audio_inputs()[0].finished.lock() = true;
902 }
903
904 std::thread::sleep(std::time::Duration::from_millis(50));
906
907 processor.process_with_audio_io(256);
908
909 let out_buf = processor.audio_outputs()[0].buffer.lock();
910 assert!(
911 out_buf.iter().all(|&s| s == 1.0),
912 "after crash, output should be bypass copy of input"
913 );
914 }
915
916 #[test]
917 fn clap_track_integration() {
918 use crate::track::Track;
919
920 let host_bin = find_host_binary();
921 if !host_bin.exists() {
922 return;
923 }
924
925 let plugin_path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
926 .parent()
927 .unwrap()
928 .join("daw")
929 .join("plugin-host")
930 .join("tests")
931 .join("test_passthrough.clap");
932
933 if !plugin_path.exists() {
934 return;
935 }
936
937 let mut track = Track::new("test-track".to_string(), 2, 2, 0, 0, 256, 48000.0);
938
939 track
940 .load_clap_plugin(
941 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
942 None,
943 )
944 .expect("should load CLAP plugin on track");
945
946 assert_eq!(track.clap_plugins.len(), 1);
947
948 let processor = track.clap_plugins[0].processor.lock();
949 processor.setup_audio_ports();
950
951 for (i, input) in processor.audio_inputs().iter().enumerate() {
952 let buf = input.buffer.lock();
953 for (j, sample) in buf.iter_mut().enumerate() {
954 *sample = (i * 1000 + j) as f32;
955 }
956 *input.finished.lock() = true;
957 }
958
959 processor.process_with_audio_io(256);
960
961 for (ch, output) in processor.audio_outputs().iter().enumerate() {
962 let buf = output.buffer.lock();
963 assert!(
964 buf.iter().any(|&s| s != 0.0),
965 "plugin output ch={ch} should contain non-zero samples after CLAP processing"
966 );
967 }
968 }
969}