//! Exercise the actual exported CLAP ABI without requiring a GUI or installed DAW. use clap_sys::{ audio_buffer::*, events::*, ext::{note_ports::*, 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 exported_factory_state_and_silent_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 = &rivet::clap_entry; assert!(entry.init.unwrap()(c"rivet.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"Rivet"); assert_eq!(CStr::from_ptr((*descriptor).id), c"io.storyboard.motif"); 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_NOTE_PORTS.as_ptr()) as *const clap_plugin_note_ports; assert!(!ports.is_null()); assert_eq!((*ports).count.unwrap()(plugin, false), 1); assert_eq!((*ports).count.unwrap()(plugin, true), 0); 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(); let mut session = rivet_core::Session { source: Some(rivet_core::demo_motif()), tempo: 150., ..Default::default() }; session.source.as_mut().unwrap().notes[1].locked = true; session.harmony = rivet_core::Harmony::progression(0, rivet_core::Scale::Minor); session.audition = rivet_core::Audition::Midi; session.regenerate().unwrap(); session.pin_selected(1).unwrap(); session.accept(); session.selected = 2; session.accept(); session.performance.mode = rivet_core::RoleMode::CallResponse; session.performance.voices[1].controller.enabled = true; session.performance.voices[1].controller.cc = 20; session.performance.voices[1].attack_ms = 75; session.save_gesture("Airy question", None).unwrap(); session.save_branch("Original call").unwrap(); session .apply_move(rivet_core::CreativeMove::Thin, Some(0)) .unwrap(); envelope.fields.insert( "story-session-v1".into(), String::from_utf8(session.to_json().unwrap()).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!( rivet_core::Session::from_json(restored.fields["story-session-v1"].as_bytes()).unwrap(), session ); for rate in [44100., 48000., 96000.] { assert!(p.activate.unwrap()(plugin, rate, 16, 512)); assert!(p.start_processing.unwrap()(plugin)); for frames in [16, 127, 512] { let mut left = [1f32; 512]; let mut right = [1f32; 512]; let mut channels = [left.as_mut_ptr(), right.as_mut_ptr()]; let mut output = clap_audio_buffer { data32: 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: -1, frames_count: frames, transport: ptr::null(), audio_inputs: ptr::null(), audio_outputs: &mut output, audio_inputs_count: 0, audio_outputs_count: 1, in_events: &input, out_events: &outgoing, }; assert_ne!(p.process.unwrap()(plugin, &process), CLAP_PROCESS_ERROR); assert!(left[..frames as usize] .iter() .chain(&right[..frames as usize]) .all(|&s| s == 0.)); } p.stop_processing.unwrap()(plugin); p.deactivate.unwrap()(plugin); } p.destroy.unwrap()(plugin); entry.deinit.unwrap()(); } }