Quench splits the signal with a Linkwitz-Riley crossover and lets the sub pass unclipped. The top is clipped symmetrically at the ceiling minus the sub's held peak, so it does not pump at the sub's rate and the sum stays under the ceiling. What the clip removes is high-passed at the split, so its products stay out of the low band. Two correction passes and an oversampled hard clip hold the ceiling; a safety clip catches the downsampler's ringing. - Controls: Drive, Ceiling, Split, Hold, Soft, Bypass. - Listen to Quench, a plain clipper at the same settings, or the delta. - Editor meter compares how much the low band changed for Quench and for a plain clip, with a scope of the output, the sub and the top's room. - Measured: sub level +0.04 dB against -2.2 dB for a plain clip, low-band products -35 dB against -16 dB, at 0.5-1.2 dB less RMS. - Registered in the catalog, workflows, editor-check, README, chain and development docs, and the website copy.
206 lines
8.7 KiB
Rust
206 lines
8.7 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 = &quench::clap_entry;
|
||
assert!(entry.init.unwrap()(c"quench.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"Quench");
|
||
assert_eq!(
|
||
CStr::from_ptr((*descriptor).id),
|
||
c"io.narl.bass-foundry.quench"
|
||
);
|
||
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("soft".into(), ParamValue::F32(40.));
|
||
envelope.params.insert("hold".into(), ParamValue::F32(100.));
|
||
envelope.params.insert("split".into(), ParamValue::F32(90.));
|
||
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()
|
||
);
|
||
for rate in [44100., 48000., 96000.] {
|
||
assert!(p.activate.unwrap()(plugin, rate, 1, 512));
|
||
let delay = (*latency).get.unwrap()(plugin) as usize;
|
||
// Only the 4× oversampling filters: under a millisecond at every rate.
|
||
assert_eq!(delay, 37);
|
||
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 well under the ceiling passes 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 + 4800 {
|
||
// The tone's onset rings in the filters, but never past the ceiling.
|
||
assert!(left[i].abs() <= 0.9661, "{}", left[i]);
|
||
} 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()();
|
||
}
|
||
}
|