1use truce_rack_core::buffer::AudioBuffer;
22use truce_rack_core::bus::BusLayout;
23use truce_rack_core::error::{Error, Result};
24use truce_rack_core::events::EventList;
25use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
26use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
27use truce_rack_core::transport::TransportInfo;
28use truce_rack_core::scanner::PluginScanner;
29use truce_rack_core::wrapper::run_audio_block_with;
30
31use clap_sys::audio_buffer::clap_audio_buffer;
32use clap_sys::entry::clap_plugin_entry;
33use clap_sys::events::{
34 CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON,
35 CLAP_EVENT_PARAM_VALUE, CLAP_EVENT_TRANSPORT, CLAP_TRANSPORT_HAS_BEATS_TIMELINE,
36 CLAP_TRANSPORT_HAS_SECONDS_TIMELINE, CLAP_TRANSPORT_HAS_TEMPO,
37 CLAP_TRANSPORT_HAS_TIME_SIGNATURE, CLAP_TRANSPORT_IS_LOOP_ACTIVE, CLAP_TRANSPORT_IS_PLAYING,
38 CLAP_TRANSPORT_IS_RECORDING, clap_event_header, clap_event_midi, clap_event_note,
39 clap_event_param_value, clap_event_transport, clap_input_events, clap_output_events,
40 clap_transport_flags,
41};
42use clap_sys::fixedpoint::{CLAP_BEATTIME_FACTOR, CLAP_SECTIME_FACTOR};
43use clap_sys::ext::gui::{
44 CLAP_EXT_GUI, CLAP_WINDOW_API_COCOA, CLAP_WINDOW_API_WIN32, CLAP_WINDOW_API_X11,
45 clap_plugin_gui, clap_window, clap_window_handle,
46};
47use clap_sys::ext::params::{
48 CLAP_EXT_PARAMS, CLAP_PARAM_IS_AUTOMATABLE, CLAP_PARAM_IS_BYPASS, CLAP_PARAM_IS_ENUM,
49 CLAP_PARAM_IS_HIDDEN, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, clap_param_info,
50 clap_plugin_params,
51};
52use clap_sys::ext::state::{CLAP_EXT_STATE, clap_plugin_state};
53use clap_sys::factory::plugin_factory::{CLAP_PLUGIN_FACTORY_ID, clap_plugin_factory};
54use clap_sys::plugin::{clap_plugin, clap_plugin_descriptor};
55use clap_sys::process::{
56 CLAP_PROCESS_CONTINUE, CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, CLAP_PROCESS_ERROR,
57 CLAP_PROCESS_SLEEP, CLAP_PROCESS_TAIL, clap_process,
58};
59use clap_sys::stream::{clap_istream, clap_ostream};
60
61use std::ffi::{CStr, CString, c_char};
62use std::path::{Path, PathBuf};
63use std::ptr;
64
65pub const FORMAT: &str = "clap";
68
69pub const CLAP_EXTENSION: &str = ".clap";
72
73const ENTRY_SYMBOL: &[u8] = b"clap_entry\0";
75
76#[derive(Debug, Default)]
84pub struct ClapScanner;
85
86impl ClapScanner {
87 #[must_use]
91 pub fn new() -> Self {
92 Self
93 }
94}
95
96impl PluginScanner for ClapScanner {
97 type Plugin = ClapPlugin;
98
99 fn scan(&self) -> Result<Vec<PluginInfo>> {
100 let mut out = Vec::new();
101 for dir in default_clap_paths() {
102 if dir.exists() {
103 scan_dir_into(&dir, &mut out);
104 }
105 }
106 Ok(out)
107 }
108
109 fn scan_path(&self, path: &Path) -> Result<Vec<PluginInfo>> {
110 let mut out = Vec::new();
111 if path.exists() {
112 scan_dir_into(path, &mut out);
113 }
114 Ok(out)
115 }
116
117 fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
118 ClapPlugin::load_from(info)
119 }
120}
121
122#[must_use]
125pub fn default_clap_paths() -> Vec<PathBuf> {
126 let mut out = Vec::new();
127 if let Some(home) = std::env::var_os("HOME") {
128 let mut user = PathBuf::from(home);
129 #[cfg(target_os = "macos")]
130 user.push("Library/Audio/Plug-Ins/CLAP");
131 #[cfg(target_os = "linux")]
132 user.push(".clap");
133 #[cfg(target_os = "windows")]
134 user.push("AppData/Local/Programs/Common/CLAP");
135 out.push(user);
136 }
137 #[cfg(target_os = "macos")]
138 out.push(PathBuf::from("/Library/Audio/Plug-Ins/CLAP"));
139 #[cfg(target_os = "linux")]
140 out.push(PathBuf::from("/usr/lib/clap"));
141 #[cfg(target_os = "windows")]
142 {
143 if let Some(pf) = std::env::var_os("CommonProgramFiles") {
144 let mut p = PathBuf::from(pf);
145 p.push("CLAP");
146 out.push(p);
147 }
148 }
149 out
150}
151
152fn scan_dir_into(dir: &Path, out: &mut Vec<PluginInfo>) {
153 let Ok(entries) = std::fs::read_dir(dir) else {
154 return;
155 };
156 for entry in entries.flatten() {
157 let path = entry.path();
158 let name = match path.file_name().and_then(|n| n.to_str()) {
159 Some(n) => n.to_string(),
160 None => continue,
161 };
162 if !name.ends_with(CLAP_EXTENSION) {
163 continue;
164 }
165 if let Err(err) = scan_bundle_into(&path, out) {
168 eprintln!("[truce-rack-clap] skipping {}: {err}", path.display());
169 }
170 }
171}
172
173fn scan_bundle_into(bundle_path: &Path, out: &mut Vec<PluginInfo>) -> Result<()> {
174 let binary = bundle_binary_path(bundle_path);
175 let handle = unsafe { LoadedLibrary::open(&binary)? };
176 let entry = handle.entry()?;
177 unsafe { entry.init(&binary)? };
178 let factory = unsafe { entry.factory() };
179 if !factory.is_null() {
180 let count = unsafe { ((*factory).get_plugin_count.unwrap_or(empty_count))(factory) };
181 for idx in 0..count {
182 let desc =
183 unsafe { ((*factory).get_plugin_descriptor.unwrap_or(empty_desc))(factory, idx) };
184 if desc.is_null() {
185 continue;
186 }
187 out.push(unsafe { descriptor_to_info(bundle_path, &*desc) });
188 }
189 }
190 unsafe { entry.deinit() };
191 Ok(())
192}
193
194unsafe extern "C" fn empty_count(_: *const clap_plugin_factory) -> u32 {
195 0
196}
197
198unsafe extern "C" fn empty_desc(
199 _: *const clap_plugin_factory,
200 _: u32,
201) -> *const clap_plugin_descriptor {
202 ptr::null()
203}
204
205fn bundle_binary_path(bundle: &Path) -> PathBuf {
209 #[cfg(target_os = "macos")]
210 {
211 let stem = bundle.file_stem().unwrap_or_default();
212 if bundle.is_dir() {
213 return bundle.join("Contents/MacOS").join(stem);
214 }
215 }
216 bundle.to_path_buf()
217}
218
219unsafe fn descriptor_to_info(bundle_path: &Path, desc: &clap_plugin_descriptor) -> PluginInfo {
220 let id = unsafe { cstr_to_string(desc.id) };
221 let name = unsafe { cstr_to_string(desc.name) };
222 let vendor = unsafe { cstr_to_string(desc.vendor) };
223 let version_str = unsafe { cstr_to_string(desc.version) };
224 let version = parse_version(&version_str);
225 let (category, accepts_midi) = unsafe { categorize(desc.features) };
226 PluginInfo {
227 name,
228 vendor,
229 version,
230 category,
231 path: bundle_path.to_path_buf(),
232 unique_id: id,
233 format: FORMAT,
234 has_editor: false, accepts_midi,
236 }
237}
238
239unsafe fn categorize(features: *const *const c_char) -> (PluginCategory, bool) {
244 if features.is_null() {
245 return (PluginCategory::Effect, false);
246 }
247 let mut category = PluginCategory::Effect;
248 let mut accepts_midi = false;
249 let mut idx = 0usize;
250 loop {
251 let p = unsafe { *features.add(idx) };
252 if p.is_null() {
253 break;
254 }
255 let bytes = unsafe { CStr::from_ptr(p).to_bytes() };
256 match bytes {
257 b"instrument" => category = PluginCategory::Instrument,
258 b"note-effect" => {
259 category = PluginCategory::NoteEffect;
260 accepts_midi = true;
261 }
262 b"analyzer" => category = PluginCategory::Analyzer,
263 b"utility" => category = PluginCategory::Tool,
264 b"note-input" => accepts_midi = true,
265 _ => {}
266 }
267 idx += 1;
268 }
269 (category, accepts_midi)
270}
271
272fn parse_version(s: &str) -> u32 {
277 let mut parts = s.split('.').map(|p| p.parse::<u32>().unwrap_or(0));
278 let major = parts.next().unwrap_or(0);
279 let minor = parts.next().unwrap_or(0);
280 let patch = parts.next().unwrap_or(0);
281 (major << 16) | (minor << 8) | patch
282}
283
284fn empty_param_info() -> clap_param_info {
285 clap_param_info {
286 id: 0,
287 flags: 0,
288 cookie: ptr::null_mut(),
289 name: [0; clap_sys::string_sizes::CLAP_NAME_SIZE],
290 module: [0; clap_sys::string_sizes::CLAP_PATH_SIZE],
291 min_value: 0.0,
292 max_value: 0.0,
293 default_value: 0.0,
294 }
295}
296
297fn clap_param_info_to_rack(info: &clap_param_info) -> ParameterInfo {
298 let name = c_buf_to_string(&info.name);
299 let mut flags = truce_rack_core::info::ParameterFlags::empty();
300 if info.flags & CLAP_PARAM_IS_BYPASS != 0 {
301 flags |= truce_rack_core::info::ParameterFlags::BYPASS;
302 }
303 if info.flags & CLAP_PARAM_IS_AUTOMATABLE != 0 {
304 flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
305 }
306 if info.flags & CLAP_PARAM_IS_HIDDEN != 0 {
307 flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
308 }
309 if info.flags & CLAP_PARAM_IS_READONLY != 0 {
310 flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
311 }
312 if info.flags & CLAP_PARAM_IS_ENUM != 0 {
313 flags |= truce_rack_core::info::ParameterFlags::ENUMERATED;
314 }
315 let step_count = if info.flags & CLAP_PARAM_IS_STEPPED != 0 {
316 #[allow(clippy::cast_possible_truncation)]
319 let span = (info.max_value - info.min_value).round() as i64;
320 u32::try_from(span + 1).unwrap_or(0)
321 } else {
322 0
323 };
324 ParameterInfo {
325 id: info.id,
326 name: name.clone(),
327 short_name: name,
328 unit: String::new(), min: info.min_value,
330 max: info.max_value,
331 default: info.default_value,
332 step_count,
333 flags,
334 }
335}
336
337#[allow(clippy::cast_sign_loss)]
338fn c_buf_to_string(buf: &[c_char]) -> String {
339 let bytes: Vec<u8> = buf
343 .iter()
344 .take_while(|&&b| b != 0)
345 .map(|&b| b as u8)
346 .collect();
347 String::from_utf8_lossy(&bytes).into_owned()
348}
349
350unsafe fn cstr_to_string(p: *const c_char) -> String {
351 if p.is_null() {
352 return String::new();
353 }
354 unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
355}
356
357struct LoadedLibrary {
360 library: libloading::Library,
361}
362
363impl LoadedLibrary {
364 unsafe fn open(path: &Path) -> Result<Self> {
365 let library = unsafe { libloading::Library::new(path) }.map_err(|e| Error::LoadFailed {
369 path: path.to_path_buf(),
370 reason: format!("dlopen failed: {e}"),
371 })?;
372 Ok(Self { library })
373 }
374
375 fn entry(&self) -> Result<EntryRef<'_>> {
376 let symbol: libloading::Symbol<'_, *const clap_plugin_entry> = unsafe {
377 self.library
378 .get(ENTRY_SYMBOL)
379 .map_err(|e| Error::LoadFailed {
380 path: PathBuf::new(),
381 reason: format!("missing clap_entry symbol: {e}"),
382 })?
383 };
384 let entry = *symbol;
385 if entry.is_null() {
386 return Err(Error::LoadFailed {
387 path: PathBuf::new(),
388 reason: "clap_entry symbol resolved to NULL".into(),
389 });
390 }
391 Ok(EntryRef {
392 entry,
393 _phantom: std::marker::PhantomData,
394 })
395 }
396}
397
398struct EntryRef<'a> {
399 entry: *const clap_plugin_entry,
400 #[allow(dead_code)]
403 _phantom: std::marker::PhantomData<&'a LoadedLibrary>,
404}
405
406impl EntryRef<'_> {
407 unsafe fn init(&self, path: &Path) -> Result<()> {
408 let init = unsafe { (*self.entry).init };
409 if let Some(init) = init {
410 let c_path =
411 CString::new(path.to_string_lossy().as_bytes()).map_err(|e| Error::LoadFailed {
412 path: path.to_path_buf(),
413 reason: format!("plugin path is not a valid C string: {e}"),
414 })?;
415 if !unsafe { init(c_path.as_ptr()) } {
416 return Err(Error::LoadFailed {
417 path: path.to_path_buf(),
418 reason: "clap_entry::init returned false".into(),
419 });
420 }
421 }
422 Ok(())
423 }
424
425 unsafe fn deinit(&self) {
426 if let Some(deinit) = unsafe { (*self.entry).deinit } {
427 unsafe { deinit() };
428 }
429 }
430
431 unsafe fn factory(&self) -> *const clap_plugin_factory {
432 let Some(get_factory) = (unsafe { (*self.entry).get_factory }) else {
433 return ptr::null();
434 };
435 let raw = unsafe { get_factory(CLAP_PLUGIN_FACTORY_ID.as_ptr()) };
436 raw.cast::<clap_plugin_factory>()
437 }
438}
439
440pub struct ClapPlugin {
446 info: PluginInfo,
447 layouts: Vec<BusLayout>,
448 active_layout: Option<BusLayout>,
449 plugin: *const clap_plugin,
450 library: LoadedLibrary,
451 bundle_path: PathBuf,
452 started_processing: bool,
453 params_ext: *const clap_plugin_params,
456 state_ext: *const clap_plugin_state,
459 gui_ext: *const clap_plugin_gui,
462 gui_open: bool,
465 steady_time: i64,
468 pending_param_changes: Vec<(u32, f64)>,
472}
473
474unsafe impl Send for ClapPlugin {}
479
480impl ClapPlugin {
481 fn load_from(info: &PluginInfo) -> Result<Self> {
482 let bundle_path = info.path.clone();
483 let binary = bundle_binary_path(&bundle_path);
484 let library = unsafe { LoadedLibrary::open(&binary)? };
485 let entry = library.entry()?;
486 unsafe { entry.init(&binary)? };
487 let factory = unsafe { entry.factory() };
488 if factory.is_null() {
489 unsafe { entry.deinit() };
490 return Err(Error::LoadFailed {
491 path: bundle_path,
492 reason: "plugin has no clap.plugin-factory".into(),
493 });
494 }
495 let create = unsafe { (*factory).create_plugin }.ok_or_else(|| Error::LoadFailed {
496 path: bundle_path.clone(),
497 reason: "factory missing create_plugin".into(),
498 })?;
499 let id_cstring = CString::new(info.unique_id.as_str()).map_err(|e| Error::LoadFailed {
500 path: bundle_path.clone(),
501 reason: format!("plugin id is not a valid C string: {e}"),
502 })?;
503 let host = ptr::null(); let plugin = unsafe { create(factory, host, id_cstring.as_ptr()) };
505 if plugin.is_null() {
506 unsafe { entry.deinit() };
507 return Err(Error::LoadFailed {
508 path: bundle_path,
509 reason: "factory.create_plugin returned NULL".into(),
510 });
511 }
512 let init = unsafe { (*plugin).init }.ok_or_else(|| Error::LoadFailed {
513 path: bundle_path.clone(),
514 reason: "plugin missing init".into(),
515 })?;
516 if !unsafe { init(plugin) } {
517 if let Some(destroy) = unsafe { (*plugin).destroy } {
518 unsafe { destroy(plugin) };
519 }
520 unsafe { entry.deinit() };
521 return Err(Error::LoadFailed {
522 path: bundle_path,
523 reason: "clap_plugin::init returned false".into(),
524 });
525 }
526 let params_ext = unsafe { lookup_extension::<clap_plugin_params>(plugin, CLAP_EXT_PARAMS) };
527 let state_ext = unsafe { lookup_extension::<clap_plugin_state>(plugin, CLAP_EXT_STATE) };
528 let gui_ext = unsafe { lookup_extension::<clap_plugin_gui>(plugin, CLAP_EXT_GUI) };
529 let mut info = info.clone();
530 if !gui_ext.is_null() && unsafe { gui_supports_current_platform(plugin, gui_ext) } {
531 info.has_editor = true;
532 }
533 Ok(Self {
534 info,
535 layouts: vec![BusLayout::stereo()],
536 active_layout: None,
537 plugin,
538 library,
539 bundle_path,
540 started_processing: false,
541 params_ext,
542 state_ext,
543 gui_ext,
544 gui_open: false,
545 steady_time: 0,
546 pending_param_changes: Vec::new(),
547 })
548 }
549}
550
551unsafe fn lookup_extension<T>(plugin: *const clap_plugin, id: &CStr) -> *const T {
556 let Some(get_extension) = (unsafe { (*plugin).get_extension }) else {
557 return ptr::null();
558 };
559 let raw = unsafe { get_extension(plugin, id.as_ptr()) };
560 raw.cast::<T>()
561}
562
563impl Drop for ClapPlugin {
564 fn drop(&mut self) {
565 if !self.plugin.is_null() {
566 if self.gui_open
567 && let Some(destroy) = unsafe { (*self.gui_ext).destroy }
568 {
569 unsafe { destroy(self.plugin) };
570 self.gui_open = false;
571 }
572 if self.started_processing
573 && let Some(stop) = unsafe { (*self.plugin).stop_processing }
574 {
575 unsafe { stop(self.plugin) };
576 }
577 if self.active_layout.is_some()
578 && let Some(deactivate) = unsafe { (*self.plugin).deactivate }
579 {
580 unsafe { deactivate(self.plugin) };
581 }
582 if let Some(destroy) = unsafe { (*self.plugin).destroy } {
583 unsafe { destroy(self.plugin) };
584 }
585 }
586 if let Ok(entry) = self.library.entry() {
587 unsafe { entry.deinit() };
588 }
589 let _ = &self.bundle_path; }
592}
593
594impl PluginCore for ClapPlugin {
595 fn info(&self) -> &PluginInfo {
596 &self.info
597 }
598
599 fn active_layout(&self) -> Option<&BusLayout> {
600 self.active_layout.as_ref()
601 }
602
603 fn supported_layouts(&self) -> &[BusLayout] {
604 &self.layouts
605 }
606
607 fn parameter_count(&self) -> usize {
608 if self.params_ext.is_null() {
609 return 0;
610 }
611 let count = unsafe { (*self.params_ext).count };
612 count.map_or(0, |c| unsafe { c(self.plugin) as usize })
613 }
614
615 fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
616 if self.params_ext.is_null() {
617 return Err(Error::InvalidParameter(index));
618 }
619 let get_info =
620 unsafe { (*self.params_ext).get_info }.ok_or(Error::InvalidParameter(index))?;
621 let mut info = empty_param_info();
622 let idx_u32 = u32::try_from(index).map_err(|_| Error::InvalidParameter(index))?;
623 let ok = unsafe { get_info(self.plugin, idx_u32, &raw mut info) };
624 if !ok {
625 return Err(Error::InvalidParameter(index));
626 }
627 Ok(clap_param_info_to_rack(&info))
628 }
629
630 fn parameter_value(&self, index: usize) -> Result<f64> {
631 if self.params_ext.is_null() {
632 return Err(Error::InvalidParameter(index));
633 }
634 let info = self.parameter_info(index)?;
638 let get_value =
639 unsafe { (*self.params_ext).get_value }.ok_or(Error::InvalidParameter(index))?;
640 let mut out = 0.0f64;
641 let ok = unsafe { get_value(self.plugin, info.id, &raw mut out) };
642 if !ok {
643 return Err(Error::InvalidParameter(index));
644 }
645 Ok(out)
646 }
647
648 fn parameter_value_string(&self, index: usize, value: f64) -> Result<String> {
649 if self.params_ext.is_null() {
650 return Err(Error::InvalidParameter(index));
651 }
652 let info = self.parameter_info(index)?;
653 let value_to_text =
654 unsafe { (*self.params_ext).value_to_text }.ok_or(Error::InvalidParameter(index))?;
655 let mut buf = [0i8; clap_sys::string_sizes::CLAP_NAME_SIZE];
656 let buf_len = u32::try_from(buf.len()).unwrap_or(u32::MAX);
657 let ok = unsafe { value_to_text(self.plugin, info.id, value, buf.as_mut_ptr(), buf_len) };
658 if !ok {
659 return Err(Error::InvalidParameter(index));
660 }
661 Ok(c_buf_to_string(&buf))
662 }
663
664 fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
665 if self.params_ext.is_null() {
666 return Err(Error::InvalidParameter(index));
667 }
668 let info = self.parameter_info(index)?;
669 self.pending_param_changes.push((info.id, value));
673 if !self.started_processing
674 && let Some(flush) = unsafe { (*self.params_ext).flush }
675 {
676 let events = std::mem::take(&mut self.pending_param_changes);
677 let mut converted = ConvertedInputEvents { events: Vec::new() };
678 for (param_id, value) in events {
679 converted.push_param_value(0, param_id, value);
680 }
681 let input = converted.as_clap();
682 let mut sink = OutputEventsSink::new(None);
683 let output = sink.as_clap();
684 unsafe { flush(self.plugin, &raw const input, &raw const output) };
685 }
686 Ok(())
687 }
688
689 fn preset_count(&self) -> usize {
690 0
691 }
692
693 fn preset_info(&self, index: usize) -> Result<PresetInfo> {
694 Err(Error::InvalidParameter(index))
695 }
696
697 fn load_preset(&mut self, _preset_number: i32) -> Result<()> {
698 Err(Error::Other("clap preset loading not yet wired".into()))
699 }
700
701 fn save_state(&self) -> Result<Vec<u8>> {
702 if self.state_ext.is_null() {
703 return Err(Error::Other("plugin missing clap.state extension".into()));
704 }
705 let save = unsafe { (*self.state_ext).save }
706 .ok_or_else(|| Error::Other("clap.state extension missing save fn".into()))?;
707 let mut buffer = WriteBuffer::default();
708 let stream = clap_ostream {
709 ctx: (&raw mut buffer).cast(),
710 write: Some(ostream_write),
711 };
712 let ok = unsafe { save(self.plugin, &raw const stream) };
713 if !ok {
714 return Err(Error::Other("clap state save returned false".into()));
715 }
716 Ok(buffer.bytes)
717 }
718
719 fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
720 if self.state_ext.is_null() {
721 return Err(Error::Other("plugin missing clap.state extension".into()));
722 }
723 let load = unsafe { (*self.state_ext).load }
724 .ok_or_else(|| Error::Other("clap.state extension missing load fn".into()))?;
725 let mut cursor = ReadCursor { bytes, position: 0 };
726 let stream = clap_istream {
727 ctx: (&raw mut cursor).cast(),
728 read: Some(istream_read),
729 };
730 let ok = unsafe { load(self.plugin, &raw const stream) };
731 if !ok {
732 return Err(Error::Other("clap state load returned false".into()));
733 }
734 Ok(())
735 }
736
737 fn activate(
738 &mut self,
739 layout: BusLayout,
740 sample_rate: f64,
741 max_block_size: usize,
742 ) -> Result<()> {
743 let Some(activate) = (unsafe { (*self.plugin).activate }) else {
744 return Err(Error::Other("clap plugin missing activate".into()));
745 };
746 let ok = unsafe {
747 activate(
748 self.plugin,
749 sample_rate,
750 1,
751 u32::try_from(max_block_size).unwrap_or(u32::MAX),
752 )
753 };
754 if !ok {
755 return Err(Error::Other("clap_plugin::activate returned false".into()));
756 }
757 self.active_layout = Some(layout);
758 Ok(())
759 }
760
761 fn deactivate(&mut self) {
762 if self.started_processing {
763 if let Some(stop) = unsafe { (*self.plugin).stop_processing } {
764 unsafe { stop(self.plugin) };
765 }
766 self.started_processing = false;
767 }
768 if let Some(deactivate) = unsafe { (*self.plugin).deactivate } {
769 unsafe { deactivate(self.plugin) };
770 }
771 self.active_layout = None;
772 }
773
774 fn is_active(&self) -> bool {
775 self.active_layout.is_some()
776 }
777
778 fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
779 if self.gui_ext.is_null() {
780 return None;
781 }
782 Some(self)
783 }
784}
785
786const fn platform_api() -> &'static CStr {
789 #[cfg(target_os = "macos")]
790 {
791 CLAP_WINDOW_API_COCOA
792 }
793 #[cfg(target_os = "windows")]
794 {
795 CLAP_WINDOW_API_WIN32
796 }
797 #[cfg(target_os = "linux")]
798 {
799 CLAP_WINDOW_API_X11
800 }
801 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
802 {
803 CLAP_WINDOW_API_COCOA
804 }
805}
806
807unsafe fn gui_supports_current_platform(
808 plugin: *const clap_plugin,
809 gui_ext: *const clap_plugin_gui,
810) -> bool {
811 let Some(is_supported) = (unsafe { (*gui_ext).is_api_supported }) else {
812 return false;
813 };
814 unsafe { is_supported(plugin, platform_api().as_ptr(), false) }
815}
816
817fn handle_to_clap_window(handle: truce_rack_core::editor::WindowHandle) -> clap_window {
818 use truce_rack_core::editor::WindowHandle;
819 let (api, specific) = match handle {
820 WindowHandle::NSView(p) => (
821 CLAP_WINDOW_API_COCOA.as_ptr(),
822 clap_window_handle { cocoa: p },
823 ),
824 WindowHandle::HWND(p) => (
825 CLAP_WINDOW_API_WIN32.as_ptr(),
826 clap_window_handle { win32: p },
827 ),
828 WindowHandle::X11(id) => (
829 CLAP_WINDOW_API_X11.as_ptr(),
830 clap_window_handle { x11: id as _ },
831 ),
832 };
833 clap_window { api, specific }
834}
835
836impl truce_rack_core::editor::PluginEditor for ClapPlugin {
837 fn open(
838 &mut self,
839 parent: truce_rack_core::editor::WindowHandle,
840 scale: f64,
841 ) -> truce_rack_core::error::Result<()> {
842 if self.gui_open {
843 return Ok(());
844 }
845 if self.gui_ext.is_null() {
846 return Err(Error::Other("clap.gui extension absent".into()));
847 }
848 let api = platform_api();
849 let create = unsafe { (*self.gui_ext).create }
850 .ok_or_else(|| Error::Other("clap.gui missing `create`".into()))?;
851 if !unsafe { create(self.plugin, api.as_ptr(), false) } {
852 return Err(Error::Other("clap.gui::create returned false".into()));
853 }
854 if let Some(set_scale) = unsafe { (*self.gui_ext).set_scale } {
855 let _ = unsafe { set_scale(self.plugin, scale) };
856 }
857 let window = handle_to_clap_window(parent);
858 if let Some(set_parent) = unsafe { (*self.gui_ext).set_parent }
859 && !unsafe { set_parent(self.plugin, &raw const window) }
860 {
861 if let Some(destroy) = unsafe { (*self.gui_ext).destroy } {
862 unsafe { destroy(self.plugin) };
863 }
864 return Err(Error::Other("clap.gui::set_parent returned false".into()));
865 }
866 if let Some(show) = unsafe { (*self.gui_ext).show } {
867 let _ = unsafe { show(self.plugin) };
868 }
869 self.gui_open = true;
870 Ok(())
871 }
872
873 fn close(&mut self) {
874 if !self.gui_open {
875 return;
876 }
877 if let Some(hide) = unsafe { (*self.gui_ext).hide } {
878 let _ = unsafe { hide(self.plugin) };
879 }
880 if let Some(destroy) = unsafe { (*self.gui_ext).destroy } {
881 unsafe { destroy(self.plugin) };
882 }
883 self.gui_open = false;
884 }
885
886 fn is_open(&self) -> bool {
887 self.gui_open
888 }
889
890 fn size(&self) -> Option<(u32, u32)> {
891 if !self.gui_open {
892 return None;
893 }
894 let get_size = unsafe { (*self.gui_ext).get_size }?;
895 let mut w: u32 = 0;
896 let mut h: u32 = 0;
897 if unsafe { get_size(self.plugin, &raw mut w, &raw mut h) } {
898 Some((w, h))
899 } else {
900 None
901 }
902 }
903
904 fn is_resizable(&self) -> bool {
905 if !self.gui_open {
906 return false;
907 }
908 unsafe { (*self.gui_ext).can_resize }.is_some_and(|f| unsafe { f(self.plugin) })
909 }
910
911 fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
912 if !self.gui_open {
913 return None;
914 }
915 let mut w = width;
916 let mut h = height;
917 if let Some(adjust) = unsafe { (*self.gui_ext).adjust_size } {
918 let _ = unsafe { adjust(self.plugin, &raw mut w, &raw mut h) };
919 }
920 let set = unsafe { (*self.gui_ext).set_size }?;
921 if unsafe { set(self.plugin, w, h) } {
922 Some((w, h))
923 } else {
924 None
925 }
926 }
927
928 fn show(&mut self) {
929 if let Some(show) = unsafe { (*self.gui_ext).show } {
930 let _ = unsafe { show(self.plugin) };
931 }
932 }
933
934 fn hide(&mut self) {
935 if let Some(hide) = unsafe { (*self.gui_ext).hide } {
936 let _ = unsafe { hide(self.plugin) };
937 }
938 }
939}
940
941impl Plugin<f32> for ClapPlugin {
942 fn process(
943 &mut self,
944 buffer: &mut AudioBuffer<'_, f32>,
945 events: &EventList,
946 context: &mut ProcessContext<'_>,
947 ) -> Result<ProcessStatus> {
948 if !self.is_active() {
949 return Err(Error::NotActivated);
950 }
951 if !self.started_processing {
952 if let Some(start) = unsafe { (*self.plugin).start_processing } {
953 let ok = unsafe { start(self.plugin) };
954 if !ok {
955 return Err(Error::Other(
956 "clap_plugin::start_processing returned false".into(),
957 ));
958 }
959 }
960 self.started_processing = true;
961 }
962
963 let transport_event = context
971 .transport
972 .map(|t| build_clap_transport(&t, context.sample_rate));
973
974 let mut converted = ConvertedInputEvents::from_rack_events(events);
975 for (param_id, value) in self.pending_param_changes.drain(..) {
976 converted.push_param_value(0, param_id, value);
977 }
978 let input_events = converted.as_clap();
979 let mut sink = OutputEventsSink::new(Some(context.output_events));
980 let output_events = sink.as_clap();
981
982 let num_frames = buffer.num_frames();
986 let main_inputs = buffer.main_inputs();
987 let mut input_ptrs: Vec<*mut f32> = main_inputs
988 .iter()
989 .map(|chan| chan.as_ptr().cast_mut())
990 .collect();
991 let input_audio = clap_audio_buffer {
992 data32: input_ptrs.as_mut_ptr(),
993 data64: ptr::null_mut(),
994 channel_count: u32::try_from(input_ptrs.len()).unwrap_or(0),
995 latency: 0,
996 constant_mask: 0,
997 };
998
999 let main_outputs = buffer.main_outputs();
1000 let mut output_ptrs: Vec<*mut f32> = main_outputs
1001 .iter_mut()
1002 .map(|chan| chan.as_mut_ptr())
1003 .collect();
1004 let mut output_audio = clap_audio_buffer {
1005 data32: output_ptrs.as_mut_ptr(),
1006 data64: ptr::null_mut(),
1007 channel_count: u32::try_from(output_ptrs.len()).unwrap_or(0),
1008 latency: 0,
1009 constant_mask: 0,
1010 };
1011
1012 let process = clap_process {
1013 steady_time: self.steady_time,
1014 frames_count: u32::try_from(num_frames).unwrap_or(u32::MAX),
1015 transport: transport_event
1016 .as_ref()
1017 .map_or(ptr::null(), std::ptr::from_ref),
1018 audio_inputs: if input_ptrs.is_empty() {
1019 ptr::null()
1020 } else {
1021 &raw const input_audio
1022 },
1023 audio_outputs: if output_ptrs.is_empty() {
1024 ptr::null_mut()
1025 } else {
1026 &raw mut output_audio
1027 },
1028 audio_inputs_count: u32::from(!input_ptrs.is_empty()),
1029 audio_outputs_count: u32::from(!output_ptrs.is_empty()),
1030 in_events: &raw const input_events,
1031 out_events: &raw const output_events,
1032 };
1033
1034 let plugin = self.plugin;
1035 let process_ptr = unsafe { (*plugin).process };
1036 let status = match process_ptr {
1037 Some(process_fn) => {
1038 run_audio_block_with::<ClapPlugin, i32>(FORMAT, CLAP_PROCESS_ERROR, || unsafe {
1039 process_fn(plugin, &raw const process)
1040 })
1041 }
1042 None => CLAP_PROCESS_ERROR,
1043 };
1044
1045 self.steady_time = self
1046 .steady_time
1047 .saturating_add(i64::try_from(num_frames).unwrap_or(0));
1048
1049 Ok(map_clap_status(status))
1050 }
1051}
1052
1053#[allow(
1060 clippy::cast_precision_loss,
1061 clippy::cast_possible_truncation,
1062 clippy::cast_possible_wrap
1063)]
1064fn build_clap_transport(t: &TransportInfo, sample_rate: f64) -> clap_event_transport {
1065 let beats_to_fixed = |b: f64| (b * CLAP_BEATTIME_FACTOR as f64).round() as i64;
1066 let secs_to_fixed = |s: f64| (s * CLAP_SECTIME_FACTOR as f64).round() as i64;
1067
1068 let mut flags: clap_transport_flags = 0;
1069 let tempo = t.tempo_bpm.unwrap_or(0.0);
1070 if t.tempo_bpm.is_some() {
1071 flags |= CLAP_TRANSPORT_HAS_TEMPO;
1072 }
1073 let song_pos_beats = match t.song_position_beats {
1074 Some(b) => {
1075 flags |= CLAP_TRANSPORT_HAS_BEATS_TIMELINE;
1076 beats_to_fixed(b)
1077 }
1078 None => 0,
1079 };
1080 let song_pos_seconds = match t.song_position_samples {
1081 Some(s) => {
1082 flags |= CLAP_TRANSPORT_HAS_SECONDS_TIMELINE;
1083 secs_to_fixed(s as f64 / sample_rate.max(1.0))
1084 }
1085 None => 0,
1086 };
1087 let (tsig_num, tsig_denom) = match t.time_signature {
1088 Some((n, d)) => {
1089 flags |= CLAP_TRANSPORT_HAS_TIME_SIGNATURE;
1090 (n as u16, d as u16)
1091 }
1092 None => (0, 0),
1093 };
1094 let bar_start = t.bar_start_beats.map_or(0, beats_to_fixed);
1095 let bar_number = match (t.bar_start_beats, t.time_signature) {
1098 (Some(bsb), Some((n, d))) => {
1099 let beats_per_bar = f64::from(n) * 4.0 / f64::from(d.max(1));
1100 (bsb / beats_per_bar.max(f64::EPSILON)).round() as i32
1101 }
1102 _ => 0,
1103 };
1104 if t.playing {
1105 flags |= CLAP_TRANSPORT_IS_PLAYING;
1106 }
1107 if t.recording {
1108 flags |= CLAP_TRANSPORT_IS_RECORDING;
1109 }
1110 if t.loop_active {
1111 flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE;
1112 }
1113
1114 clap_event_transport {
1115 header: clap_event_header {
1116 size: u32::try_from(std::mem::size_of::<clap_event_transport>()).unwrap_or(0),
1117 time: 0,
1118 space_id: CLAP_CORE_EVENT_SPACE_ID,
1119 type_: CLAP_EVENT_TRANSPORT,
1120 flags: 0,
1121 },
1122 flags,
1123 song_pos_beats,
1124 song_pos_seconds,
1125 tempo,
1126 tempo_inc: 0.0,
1127 loop_start_beats: 0,
1128 loop_end_beats: 0,
1129 loop_start_seconds: 0,
1130 loop_end_seconds: 0,
1131 bar_start,
1132 bar_number,
1133 tsig_num,
1134 tsig_denom,
1135 }
1136}
1137
1138fn map_clap_status(status: i32) -> ProcessStatus {
1139 match status {
1140 CLAP_PROCESS_CONTINUE | CLAP_PROCESS_CONTINUE_IF_NOT_QUIET => ProcessStatus::Continue,
1141 CLAP_PROCESS_SLEEP => ProcessStatus::Sleep,
1142 CLAP_PROCESS_TAIL => ProcessStatus::Tail { tail_samples: 0 },
1143 _ => ProcessStatus::Error,
1144 }
1145}
1146
1147fn make_param_value_event(sample_offset: u32, param_id: u32, value: f64) -> clap_event_param_value {
1148 clap_event_param_value {
1149 header: clap_event_header {
1150 size: u32::try_from(std::mem::size_of::<clap_event_param_value>()).unwrap_or(0),
1151 time: sample_offset,
1152 space_id: CLAP_CORE_EVENT_SPACE_ID,
1153 type_: CLAP_EVENT_PARAM_VALUE,
1154 flags: 0,
1155 },
1156 param_id,
1157 cookie: ptr::null_mut(),
1158 note_id: -1,
1159 port_index: -1,
1160 channel: -1,
1161 key: -1,
1162 value,
1163 }
1164}
1165
1166fn make_note_event(
1167 sample_offset: u32,
1168 event_type: u16,
1169 channel: u8,
1170 key: u8,
1171 velocity: f64,
1172) -> clap_event_note {
1173 clap_event_note {
1174 header: clap_event_header {
1175 size: u32::try_from(std::mem::size_of::<clap_event_note>()).unwrap_or(0),
1176 time: sample_offset,
1177 space_id: CLAP_CORE_EVENT_SPACE_ID,
1178 type_: event_type,
1179 flags: 0,
1180 },
1181 note_id: -1,
1182 port_index: -1,
1183 channel: i16::from(channel),
1184 key: i16::from(key),
1185 velocity,
1186 }
1187}
1188
1189fn make_midi_event(sample_offset: u32, bytes: [u8; 3]) -> clap_event_midi {
1190 clap_event_midi {
1191 header: clap_event_header {
1192 size: u32::try_from(std::mem::size_of::<clap_event_midi>()).unwrap_or(0),
1193 time: sample_offset,
1194 space_id: CLAP_CORE_EVENT_SPACE_ID,
1195 type_: CLAP_EVENT_MIDI,
1196 flags: 0,
1197 },
1198 port_index: 0,
1199 data: bytes,
1200 }
1201}
1202
1203#[derive(Default)]
1206struct WriteBuffer {
1207 bytes: Vec<u8>,
1208}
1209
1210unsafe extern "C" fn ostream_write(
1211 stream: *const clap_ostream,
1212 buffer: *const std::ffi::c_void,
1213 size: u64,
1214) -> i64 {
1215 if stream.is_null() || buffer.is_null() {
1216 return -1;
1217 }
1218 let ctx = unsafe { (*stream).ctx.cast::<WriteBuffer>() };
1219 if ctx.is_null() {
1220 return -1;
1221 }
1222 let Ok(size_usize) = usize::try_from(size) else {
1223 return -1;
1224 };
1225 let slice = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), size_usize) };
1226 unsafe { (*ctx).bytes.extend_from_slice(slice) };
1227 i64::try_from(size_usize).unwrap_or(i64::MAX)
1228}
1229
1230struct ReadCursor<'a> {
1233 bytes: &'a [u8],
1234 position: usize,
1235}
1236
1237unsafe extern "C" fn istream_read(
1238 stream: *const clap_istream,
1239 buffer: *mut std::ffi::c_void,
1240 size: u64,
1241) -> i64 {
1242 if stream.is_null() || buffer.is_null() {
1243 return -1;
1244 }
1245 let ctx = unsafe { (*stream).ctx.cast::<ReadCursor<'_>>() };
1246 if ctx.is_null() {
1247 return -1;
1248 }
1249 let cursor = unsafe { &mut *ctx };
1250 let Ok(want) = usize::try_from(size) else {
1251 return -1;
1252 };
1253 let available = cursor.bytes.len().saturating_sub(cursor.position);
1254 let take = want.min(available);
1255 if take > 0 {
1256 unsafe {
1257 std::ptr::copy_nonoverlapping(
1258 cursor.bytes.as_ptr().add(cursor.position),
1259 buffer.cast::<u8>(),
1260 take,
1261 );
1262 }
1263 cursor.position += take;
1264 }
1265 i64::try_from(take).unwrap_or(i64::MAX)
1266}
1267
1268struct ConvertedInputEvents {
1275 events: Vec<EventStorage>,
1279}
1280
1281#[allow(dead_code)]
1282enum EventStorage {
1283 Param(clap_event_param_value),
1284 Note(clap_event_note),
1285 Midi(clap_event_midi),
1286}
1287
1288impl EventStorage {
1289 fn header(&self) -> *const clap_event_header {
1290 match self {
1291 Self::Param(e) => &raw const e.header,
1292 Self::Note(e) => &raw const e.header,
1293 Self::Midi(e) => &raw const e.header,
1294 }
1295 }
1296}
1297
1298impl ConvertedInputEvents {
1299 fn from_rack_events(list: &EventList) -> Self {
1300 let mut out = Self { events: Vec::new() };
1301 for event in list {
1302 out.push_rack(event);
1303 }
1304 out
1305 }
1306
1307 fn push_param_value(&mut self, sample_offset: u32, param_id: u32, value: f64) {
1308 self.events.push(EventStorage::Param(make_param_value_event(
1309 sample_offset,
1310 param_id,
1311 value,
1312 )));
1313 }
1314
1315 fn push_rack(&mut self, event: &truce_rack_core::events::Event) {
1316 use truce_rack_core::events::EventBody;
1317 let offset = event.sample_offset;
1318 match event.body {
1319 EventBody::Midi(midi) => self.push_midi(offset, midi),
1320 EventBody::ParamValue { param_id, value } => {
1321 self.push_param_value(offset, param_id, value);
1322 }
1323 EventBody::ParamGesture { .. } | EventBody::TransportFlag(_) => {
1324 }
1330 }
1331 }
1332
1333 fn push_midi(&mut self, offset: u32, midi: truce_rack_core::events::MidiData) {
1334 use truce_rack_core::events::MidiData;
1335 match midi {
1336 MidiData::NoteOn {
1337 channel,
1338 note,
1339 velocity,
1340 } => {
1341 self.events.push(EventStorage::Note(make_note_event(
1342 offset,
1343 CLAP_EVENT_NOTE_ON,
1344 channel,
1345 note,
1346 f64::from(velocity) / 127.0,
1347 )));
1348 }
1349 MidiData::NoteOff {
1350 channel,
1351 note,
1352 velocity,
1353 } => {
1354 self.events.push(EventStorage::Note(make_note_event(
1355 offset,
1356 CLAP_EVENT_NOTE_OFF,
1357 channel,
1358 note,
1359 f64::from(velocity) / 127.0,
1360 )));
1361 }
1362 MidiData::ControlChange {
1363 channel,
1364 controller,
1365 value,
1366 } => {
1367 let status = 0xB0 | (channel & 0x0F);
1368 self.events.push(EventStorage::Midi(make_midi_event(
1369 offset,
1370 [status, controller & 0x7F, value & 0x7F],
1371 )));
1372 }
1373 MidiData::ProgramChange { channel, program } => {
1374 let status = 0xC0 | (channel & 0x0F);
1375 self.events.push(EventStorage::Midi(make_midi_event(
1376 offset,
1377 [status, program & 0x7F, 0],
1378 )));
1379 }
1380 MidiData::PolyAftertouch {
1381 channel,
1382 note,
1383 pressure,
1384 } => {
1385 let status = 0xA0 | (channel & 0x0F);
1386 self.events.push(EventStorage::Midi(make_midi_event(
1387 offset,
1388 [status, note & 0x7F, pressure & 0x7F],
1389 )));
1390 }
1391 MidiData::ChannelAftertouch { channel, pressure } => {
1392 let status = 0xD0 | (channel & 0x0F);
1393 self.events.push(EventStorage::Midi(make_midi_event(
1394 offset,
1395 [status, pressure & 0x7F, 0],
1396 )));
1397 }
1398 MidiData::PitchBend { channel, value } => {
1399 let status = 0xE0 | (channel & 0x0F);
1400 let lsb = u8::try_from(value & 0x7F).unwrap_or(0);
1401 let msb = u8::try_from((value >> 7) & 0x7F).unwrap_or(0);
1402 self.events.push(EventStorage::Midi(make_midi_event(
1403 offset,
1404 [status, lsb, msb],
1405 )));
1406 }
1407 MidiData::Raw { len, data } => {
1408 if len >= 3 {
1409 self.events.push(EventStorage::Midi(make_midi_event(
1410 offset,
1411 [data[0], data[1], data[2]],
1412 )));
1413 }
1414 }
1415 }
1416 }
1417
1418 fn as_clap(&self) -> clap_input_events {
1419 clap_input_events {
1420 ctx: std::ptr::from_ref::<Self>(self)
1421 .cast::<std::ffi::c_void>()
1422 .cast_mut(),
1423 size: Some(input_events_size),
1424 get: Some(input_events_get),
1425 }
1426 }
1427}
1428
1429unsafe extern "C" fn input_events_size(list: *const clap_input_events) -> u32 {
1430 let ctx = unsafe { (*list).ctx.cast::<ConvertedInputEvents>() };
1431 if ctx.is_null() {
1432 return 0;
1433 }
1434 u32::try_from(unsafe { (*ctx).events.len() }).unwrap_or(u32::MAX)
1435}
1436
1437unsafe extern "C" fn input_events_get(
1438 list: *const clap_input_events,
1439 index: u32,
1440) -> *const clap_event_header {
1441 let ctx = unsafe { (*list).ctx.cast::<ConvertedInputEvents>() };
1442 if ctx.is_null() {
1443 return ptr::null();
1444 }
1445 let idx = index as usize;
1446 let events = unsafe { &(*ctx).events };
1447 events.get(idx).map_or(ptr::null(), EventStorage::header)
1448}
1449
1450struct OutputEventsSink<'a> {
1452 target: Option<&'a mut EventList>,
1453}
1454
1455impl<'a> OutputEventsSink<'a> {
1456 fn new(target: Option<&'a mut EventList>) -> Self {
1457 Self { target }
1458 }
1459
1460 fn as_clap(&mut self) -> clap_output_events {
1461 clap_output_events {
1462 ctx: std::ptr::from_mut::<Self>(self).cast::<std::ffi::c_void>(),
1463 try_push: Some(output_events_try_push),
1464 }
1465 }
1466}
1467
1468unsafe extern "C" fn output_events_try_push(
1469 list: *const clap_output_events,
1470 event: *const clap_event_header,
1471) -> bool {
1472 if event.is_null() {
1473 return false;
1474 }
1475 let header = unsafe { &*event };
1476 let ctx = unsafe { (*list).ctx.cast::<OutputEventsSink<'_>>() };
1477 if ctx.is_null() {
1478 return true;
1479 }
1480 let target = unsafe { (*ctx).target.as_deref_mut() };
1481 let Some(target) = target else {
1482 return true;
1486 };
1487 if let Some(rack_event) = unsafe { clap_event_to_rack(header) } {
1488 target.push(rack_event);
1489 }
1490 true
1491}
1492
1493unsafe fn clap_event_to_rack(header: &clap_event_header) -> Option<truce_rack_core::events::Event> {
1494 use truce_rack_core::events::{Event, EventBody, MidiData};
1495 if header.space_id != CLAP_CORE_EVENT_SPACE_ID {
1496 return None;
1497 }
1498 let offset = header.time;
1499 match header.type_ {
1500 t if t == CLAP_EVENT_PARAM_VALUE => {
1501 let e: &clap_event_param_value =
1502 unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1503 Some(Event {
1504 sample_offset: offset,
1505 body: EventBody::ParamValue {
1506 param_id: e.param_id,
1507 value: e.value,
1508 },
1509 })
1510 }
1511 t if t == CLAP_EVENT_NOTE_ON => {
1512 let e: &clap_event_note =
1513 unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1514 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1515 let velocity = (e.velocity * 127.0).round().clamp(0.0, 127.0) as u8;
1516 Some(Event {
1517 sample_offset: offset,
1518 body: EventBody::Midi(MidiData::NoteOn {
1519 channel: u8::try_from(e.channel.max(0)).unwrap_or(0),
1520 note: u8::try_from(e.key.max(0)).unwrap_or(0),
1521 velocity,
1522 }),
1523 })
1524 }
1525 t if t == CLAP_EVENT_NOTE_OFF => {
1526 let e: &clap_event_note =
1527 unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1528 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1529 let velocity = (e.velocity * 127.0).round().clamp(0.0, 127.0) as u8;
1530 Some(Event {
1531 sample_offset: offset,
1532 body: EventBody::Midi(MidiData::NoteOff {
1533 channel: u8::try_from(e.channel.max(0)).unwrap_or(0),
1534 note: u8::try_from(e.key.max(0)).unwrap_or(0),
1535 velocity,
1536 }),
1537 })
1538 }
1539 t if t == CLAP_EVENT_MIDI => {
1540 let e: &clap_event_midi =
1541 unsafe { &*std::ptr::from_ref::<clap_event_header>(header).cast() };
1542 Some(Event {
1543 sample_offset: offset,
1544 body: EventBody::Midi(MidiData::Raw {
1545 len: 3,
1546 data: [e.data[0], e.data[1], e.data[2], 0, 0, 0, 0, 0],
1547 }),
1548 })
1549 }
1550 _ => None,
1551 }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556 use super::*;
1557
1558 #[test]
1559 fn parse_version_components() {
1560 assert_eq!(parse_version("1.2.3"), (1 << 16) | (2 << 8) | 3);
1561 assert_eq!(parse_version("0.5"), 5 << 8);
1562 assert_eq!(parse_version(""), 0);
1563 }
1564
1565 #[test]
1566 fn bundle_binary_macos() {
1567 let p = bundle_binary_path(Path::new("/tmp/MyPlugin.clap"));
1568 #[cfg(target_os = "macos")]
1569 assert!(!p.exists() || p.starts_with("/tmp/MyPlugin.clap"));
1570 #[cfg(not(target_os = "macos"))]
1571 assert_eq!(p, Path::new("/tmp/MyPlugin.clap"));
1572 }
1573}