//! Exercise the actual exported CLAP ABI without requiring a GUI or installed DAW. use clap_sys::{ audio_buffer::*, events::*, ext::{audio_ports::*, latency::*, state::*}, factory::plugin_factory::*, host::*, process::*, stream::*, version::*, }; use std::{ ffi::{c_char, c_void, CStr}, io::{Cursor, Read}, ptr, }; unsafe extern "C" fn extension(_: *const clap_host, _: *const c_char) -> *const c_void { ptr::null() } unsafe extern "C" fn request(_: *const clap_host) {} unsafe extern "C" fn count(_: *const clap_input_events) -> u32 { 0 } unsafe extern "C" fn event(_: *const clap_input_events, _: u32) -> *const clap_event_header { ptr::null() } unsafe extern "C" fn push(_: *const clap_output_events, _: *const clap_event_header) -> bool { true } unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { // SAFETY: The test owns the Vec for the entire synchronous save call. let bytes = unsafe { &mut *((*stream).ctx as *mut Vec) }; bytes.extend_from_slice(unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) }); size as i64 } unsafe extern "C" fn read(stream: *const clap_istream, buffer: *mut c_void, size: u64) -> i64 { // SAFETY: The test owns the Cursor; the wrapper supplies writable stream storage. let cursor = unsafe { &mut *((*stream).ctx as *mut Cursor>) }; cursor .read(unsafe { std::slice::from_raw_parts_mut(buffer as *mut u8, size as usize) }) .unwrap() as i64 } #[test] fn effect_ports_state_latency_and_audio_lifecycle() { // SAFETY: All callbacks, strings, streams, buffers, and the host outlive their // synchronous ABI calls. Activation/processing/destruction follow CLAP order. unsafe { let entry = &lathe::clap_entry; assert!(entry.init.unwrap()(c"lathe.clap".as_ptr())); let factory = entry.get_factory.unwrap()(CLAP_PLUGIN_FACTORY_ID.as_ptr()) as *const clap_plugin_factory; assert!(!factory.is_null()); let factory = &*factory; assert_eq!(factory.get_plugin_count.unwrap()(factory), 1); let descriptor = factory.get_plugin_descriptor.unwrap()(factory, 0); assert!(!descriptor.is_null()); assert_eq!(CStr::from_ptr((*descriptor).name), c"Lathe"); assert_eq!( CStr::from_ptr((*descriptor).id), c"io.narl.bass-foundry.lathe" ); assert!(factory.get_plugin_descriptor.unwrap()(factory, 1).is_null()); let host = clap_host { clap_version: CLAP_VERSION, host_data: ptr::null_mut(), name: c"Storyboard test host".as_ptr(), vendor: c"Tests".as_ptr(), url: c"".as_ptr(), version: c"1.0".as_ptr(), get_extension: Some(extension), request_restart: Some(request), request_process: Some(request), request_callback: Some(request), }; let plugin = factory.create_plugin.unwrap()(factory, &host, (*descriptor).id); assert!(!plugin.is_null()); let p = &*plugin; assert!(p.init.unwrap()(plugin)); let ports = p.get_extension.unwrap()(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()) as *const clap_plugin_audio_ports; assert!(!ports.is_null()); assert_eq!((*ports).count.unwrap()(plugin, false), 1); assert_eq!((*ports).count.unwrap()(plugin, true), 1); let latency = p.get_extension.unwrap()(plugin, CLAP_EXT_LATENCY.as_ptr()) as *const clap_plugin_latency; assert!(!latency.is_null()); let state = p.get_extension.unwrap()(plugin, CLAP_EXT_STATE.as_ptr()) as *const clap_plugin_state; assert!(!state.is_null()); let state = &*state; let save = || { let mut bytes = Vec::new(); let stream = clap_ostream { ctx: &mut bytes as *mut Vec as *mut c_void, write: Some(write), }; assert!(state.save.unwrap()(plugin, &stream)); bytes }; let mut envelope: nih_plug::prelude::PluginState = serde_json::from_slice(&save()[8..]).unwrap(); use nih_plug::wrapper::state::ParamValue; envelope.params.insert("tilt".into(), ParamValue::F32(-3.)); envelope .params .insert("harmonics".into(), ParamValue::I32(16)); let mut gains = [0f32; 32]; gains[2] = -12.; gains[4] = 6.; envelope .fields .insert("lathe-gains".into(), serde_json::to_string(&gains).unwrap()); envelope .params .insert("snap".into(), ParamValue::Bool(false)); envelope .params .insert("source".into(), ParamValue::String("midi".into())); let encoded = serde_json::to_vec(&envelope).unwrap(); let mut framed = (encoded.len() as u64).to_le_bytes().to_vec(); framed.extend_from_slice(&encoded); let mut bytes = Cursor::new(framed); let stream = clap_istream { ctx: &mut bytes as *mut Cursor> as *mut c_void, read: Some(read), }; assert!(state.load.unwrap()(plugin, &stream)); let restored: nih_plug::prelude::PluginState = serde_json::from_slice(&save()[8..]).unwrap(); assert_eq!( serde_json::to_value(&restored.params).unwrap(), serde_json::to_value(&envelope.params).unwrap() ); let saved: [f32; 32] = serde_json::from_str(&restored.fields["lathe-gains"]).unwrap(); assert_eq!(saved, gains); for rate in [44100., 48000., 96000.] { assert!(p.activate.unwrap()(plugin, rate, 1, 512)); let delay = (*latency).get.unwrap()(plugin) as usize; // About 67 ms lookahead: two periods of the lowest detected note (30 Hz). assert!((delay as f64 / rate - 0.067).abs() < 0.01, "{delay}"); assert!(p.start_processing.unwrap()(plugin)); let total = delay + 9600; let mut position = 0; let (mut energy_in, mut energy_out) = (0f64, 0f64); while position < total { let frames = [1, 127, 512, 31][position % 4].min(total - position); // A 1 kHz tone sits above the split and passes through unchanged. let tone = |n: usize| { 0.25 * (std::f64::consts::TAU * 1000. * n as f64 / rate).sin() as f32 }; let mut input_left = [0f32; 512]; let mut input_right = [0f32; 512]; for i in 0..frames { input_left[i] = tone(position + i); input_right[i] = -tone(position + i); } let mut left = [0f32; 512]; let mut right = [0f32; 512]; let mut in_channels = [input_left.as_mut_ptr(), input_right.as_mut_ptr()]; let mut out_channels = [left.as_mut_ptr(), right.as_mut_ptr()]; let incoming = clap_audio_buffer { data32: in_channels.as_mut_ptr(), data64: ptr::null_mut(), channel_count: 2, latency: 0, constant_mask: 0, }; let mut outgoing_audio = clap_audio_buffer { data32: out_channels.as_mut_ptr(), data64: ptr::null_mut(), channel_count: 2, latency: 0, constant_mask: 0, }; let input = clap_input_events { ctx: ptr::null_mut(), size: Some(count), get: Some(event), }; let outgoing = clap_output_events { ctx: ptr::null_mut(), try_push: Some(push), }; let process = clap_process { steady_time: position as i64, frames_count: frames as u32, transport: ptr::null(), audio_inputs: &incoming, audio_outputs: &mut outgoing_audio, audio_inputs_count: 1, audio_outputs_count: 1, in_events: &input, out_events: &outgoing, }; assert_ne!(p.process.unwrap()(plugin, &process), CLAP_PROCESS_ERROR); for i in 0..frames { let n = position + i; if n < delay { assert!(left[i].abs() < 1e-6, "output before the lookahead"); } else if n > delay + 4800 { energy_in += (tone(n - delay) as f64).powi(2); energy_out += (left[i] as f64).powi(2) + (right[i] as f64).powi(2); } } position += frames; } let ratio_db = 10. * (energy_out / (2. * energy_in)).log10(); assert!(ratio_db.abs() < 0.2, "{ratio_db}"); p.stop_processing.unwrap()(plugin); p.deactivate.unwrap()(plugin); } p.destroy.unwrap()(plugin); entry.deinit.unwrap()(); } }