Check and Windows preview / check (anvil) (push) Successful in 2m0s
Check and Windows preview / check (crucible) (push) Successful in 1m29s
Check and Windows preview / check (lathe) (push) Successful in 1m27s
Check and Windows preview / check (patina) (push) Successful in 1m53s
Check and Windows preview / check (rivet) (push) Successful in 1m29s
Check and Windows preview / check (temper) (push) Successful in 1m29s
Check and Windows preview / check (weld) (push) Successful in 2m38s
Check and Windows preview / previews (push) Successful in 1m28s
Independent plugin release / plan (push) Successful in 8s
Check and Windows preview / windows (push) Successful in 3m14s
Independent plugin release / build (push) Successful in 3m50s
Independent plugin release / publish (push) Successful in 9s
Weld hears the other bass layer through the sidechain and fits polarity plus up to four RBJ allpass sections to the measured cross-spectrum, so the layers sum instead of cancelling. No latency; the layer on its own keeps its sound. - Worker thread: Welch cross-spectrum and coherence, Nelder-Mead fit with 1/f weighting, a per-band loss penalty and model selection by share of the achievable gain; GCC-PHAT lag detection flags a late layer. - Audio thread: allocation-free chain with a 40 ms warm-up and 40 ms crossfade per new fit; mono blocks go to the worker through a lock-free queue. - Follow / Learn / Hold; the fit is persisted and restored without a crossfade. - Editor: per-band loss and recovery against a perfect alignment, phase before and after, recovered dB, time offset and status. - Tests for flat magnitude, crossfade, fit targets, Follow hysteresis, Learn, silence, realtime allocation and the persisted fit in the host.
244 lines
10 KiB
Rust
244 lines
10 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 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.storyboard.layer-align"
|
|
);
|
|
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<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("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<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: 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()();
|
|
}
|
|
}
|