//! 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 sidechain_port_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 = &weld::clap_entry; assert!(entry.init.unwrap()(c"weld.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"Weld"); assert_eq!( CStr::from_ptr((*descriptor).id), c"io.narl.bass-foundry.weld" ); 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()); // Main input plus the sidechain input; one output. assert_eq!((*ports).count.unwrap()(plugin, true), 2); assert_eq!((*ports).count.unwrap()(plugin, false), 1); let mut info: clap_audio_port_info = std::mem::zeroed(); assert!((*ports).get.unwrap()(plugin, 1, true, &mut info)); assert_eq!(info.channel_count, 2); assert_eq!(info.flags & CLAP_AUDIO_PORT_IS_MAIN, 0); 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("listen".into(), ParamValue::String("sum".into())); envelope .params .insert("mode".into(), ParamValue::String("hold".into())); envelope .params .insert("sections".into(), ParamValue::I32(2)); // A saved alignment that flips polarity. let flipped = weld_core::Fit { flip: true, ..weld_core::Fit::NONE }; envelope .fields .insert("weld-fit".into(), serde_json::to_string(&flipped).unwrap()); 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: weld_core::Fit = serde_json::from_str(&restored.fields["weld-fit"]).unwrap(); assert_eq!(saved, flipped); // Listen = Sum with the restored flip: a sidechain equal to the main input cancels // it from the first sample on. A silent sidechain leaves the level alone. CLAP hosts // pass every port, so the test always passes both (nih_plug reads one port past a // short count). for (rate, with_side, expected_db) in [ (44100., true, None), (48000., false, Some(0.)), (96000., true, None), ] { assert!(p.activate.unwrap()(plugin, rate, 1, 512)); assert_eq!((*latency).get.unwrap()(plugin), 0); assert!(p.start_processing.unwrap()(plugin)); let total = 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); 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 side_left, mut side_right) = if with_side { (input_left, input_right) } else { ([0f32; 512], [0f32; 512]) }; 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 side_channels = [side_left.as_mut_ptr(), side_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, }, clap_audio_buffer { data32: side_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.as_ptr(), audio_outputs: &mut outgoing_audio, audio_inputs_count: 2, 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; energy_in += 2. * (tone(n) 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 / energy_in).log10(); match expected_db { Some(db) => assert!((ratio_db - db).abs() < 0.2, "{rate} {with_side} {ratio_db}"), None => assert!(ratio_db < -60., "{rate} {with_side} {ratio_db}"), } p.stop_processing.unwrap()(plugin); p.deactivate.unwrap()(plugin); } p.destroy.unwrap()(plugin); entry.deinit.unwrap()(); } }