Check and Windows preview / check (anvil) (push) Successful in 2m8s
Check and Windows preview / check (crucible) (push) Successful in 1m32s
Check and Windows preview / check (lathe) (push) Successful in 2m13s
Check and Windows preview / check (patina) (push) Successful in 2m5s
Check and Windows preview / check (rivet) (push) Successful in 1m34s
Check and Windows preview / check (temper) (push) Successful in 2m12s
Check and Windows preview / previews (push) Successful in 1m19s
Check and Windows preview / windows (push) Successful in 2m13s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 6s
Independent plugin release / build (push) Successful in 2m57s
Lathe places EQ bands on harmonics 1-32 of the played note, so a cut on harmonic 3 stays on harmonic 3 on every note. Temper normalises each note's level before an oversampled shaper and tunes its floor, push and bright filters to the note, so every note distorts the same. The note tracker moves from Anvil into common-dsp, together with a state-variable filter, 4x oversampler and harmonic meter. Anvil's behaviour and editor render are unchanged. docs/ideas.md collects all plugin ideas and their status.
220 lines
9.2 KiB
Rust
220 lines
9.2 KiB
Rust
//! 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<u8>) };
|
|
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<Vec<u8>>) };
|
|
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.storyboard.harmonic-eq"
|
|
);
|
|
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<u8> 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<Vec<u8>> 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()();
|
|
}
|
|
}
|