feat(weld): add sidechain test build
Weld will phase-align a bass layer to another layer it hears through the sidechain. This first build only checks that FL Studio delivers the sidechain to a CLAP aux port: it passes the main layer through unchanged, meters the sidechain and offers Layer/Sum/Sum dry listening. No latency.
This commit is contained in:
1 parent
b3ebe4d952
commit
e3286f9094
21 files changed
+3471
-9
No files matched your search
@@ -0,0 +1,150 @@
|
||||
//! Weld engine: aligns the phase of the main layer to a sidechain layer so the two sum
|
||||
//! instead of cancel. Allocation-free after `new`; zero latency.
|
||||
mod settings;
|
||||
pub use settings::{Listen, Settings};
|
||||
|
||||
/// Levels for the editor, published a few times per second.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct AnalyzerFrame {
|
||||
/// RMS of the main input in dBFS since the last frame.
|
||||
pub main_db: f32,
|
||||
/// RMS of the sidechain input in dBFS since the last frame.
|
||||
pub side_db: f32,
|
||||
/// Peak of the sidechain input in dBFS since the last frame.
|
||||
pub side_peak_db: f32,
|
||||
/// True once any sidechain sample above −90 dBFS arrived since activation.
|
||||
pub side_seen: bool,
|
||||
pub sample_rate: f32,
|
||||
}
|
||||
impl Default for AnalyzerFrame {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
main_db: -120.,
|
||||
side_db: -120.,
|
||||
side_peak_db: -120.,
|
||||
side_seen: false,
|
||||
sample_rate: 48000.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn db(power_or_amp: f32, power: bool) -> f32 {
|
||||
let v = power_or_amp.max(1e-12);
|
||||
if power {
|
||||
10. * v.log10()
|
||||
} else {
|
||||
20. * v.log10()
|
||||
}
|
||||
.max(-120.)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Levels {
|
||||
main: f64,
|
||||
side: f64,
|
||||
peak: f32,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
pub struct Engine {
|
||||
rate: f32,
|
||||
channels: usize,
|
||||
settings: Settings,
|
||||
gain: f32,
|
||||
target_gain: f32,
|
||||
gain_coef: f32,
|
||||
fresh: bool,
|
||||
levels: Levels,
|
||||
side_seen: bool,
|
||||
}
|
||||
impl Engine {
|
||||
pub fn new(rate: f32, channels: usize) -> Result<Self, &'static str> {
|
||||
if !(rate.is_finite() && (8000.0..=768000.).contains(&rate)) {
|
||||
return Err("unsupported sample rate");
|
||||
}
|
||||
if !(1..=2).contains(&channels) {
|
||||
return Err("unsupported channel count");
|
||||
}
|
||||
Ok(Self {
|
||||
rate,
|
||||
channels,
|
||||
settings: Settings::default(),
|
||||
gain: 1.,
|
||||
target_gain: 1.,
|
||||
gain_coef: 1. - (-1. / (0.02 * rate)).exp(),
|
||||
fresh: true,
|
||||
levels: Levels::default(),
|
||||
side_seen: false,
|
||||
})
|
||||
}
|
||||
pub const fn latency_samples(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
pub fn channels(&self) -> usize {
|
||||
self.channels
|
||||
}
|
||||
pub fn settings(&self) -> &Settings {
|
||||
&self.settings
|
||||
}
|
||||
pub fn set_settings(&mut self, settings: Settings) -> Result<(), &'static str> {
|
||||
settings.validate()?;
|
||||
self.settings = settings;
|
||||
self.target_gain = 10f32.powf(settings.output / 20.);
|
||||
if self.fresh {
|
||||
self.gain = self.target_gain;
|
||||
self.fresh = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.gain = self.target_gain;
|
||||
self.levels = Levels::default();
|
||||
}
|
||||
/// One frame of main input `x` and sidechain input `side`. Mono uses index 0 only.
|
||||
#[inline]
|
||||
pub fn process_sample(&mut self, x: [f32; 2], side: [f32; 2]) -> [f32; 2] {
|
||||
let n = self.channels;
|
||||
let (mut m, mut s) = (0f32, 0f32);
|
||||
for c in 0..n {
|
||||
m += x[c];
|
||||
s += side[c];
|
||||
self.levels.peak = self.levels.peak.max(side[c].abs());
|
||||
}
|
||||
m /= n as f32;
|
||||
s /= n as f32;
|
||||
self.levels.main += (m * m) as f64;
|
||||
self.levels.side += (s * s) as f64;
|
||||
self.levels.count += 1;
|
||||
if s.abs() > 3e-5 {
|
||||
self.side_seen = true;
|
||||
}
|
||||
self.gain += (self.target_gain - self.gain) * self.gain_coef;
|
||||
if self.settings.bypass {
|
||||
return x;
|
||||
}
|
||||
let mut out = [0.; 2];
|
||||
for c in 0..n {
|
||||
let layer = x[c];
|
||||
out[c] = self.gain
|
||||
* match self.settings.listen {
|
||||
Listen::Layer => layer,
|
||||
Listen::Sum => layer + side[c],
|
||||
Listen::SumDry => x[c] + side[c],
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Levels since the previous call.
|
||||
pub fn take_analyzer(&mut self) -> AnalyzerFrame {
|
||||
let n = self.levels.count.max(1) as f64;
|
||||
let frame = AnalyzerFrame {
|
||||
main_db: db((self.levels.main / n) as f32, true),
|
||||
side_db: db((self.levels.side / n) as f32, true),
|
||||
side_peak_db: db(self.levels.peak, false),
|
||||
side_seen: self.side_seen,
|
||||
sample_rate: self.rate,
|
||||
};
|
||||
self.levels = Levels::default();
|
||||
frame
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Host-independent controls. Gains in dB.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// What the plugin outputs.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub enum Listen {
|
||||
/// The aligned main layer only. The sidechain layer reaches the mix on its own track.
|
||||
#[default]
|
||||
Layer,
|
||||
/// Aligned main layer plus the sidechain layer, to judge the sum in one place.
|
||||
Sum,
|
||||
/// Unprocessed main layer plus the sidechain layer, to compare with `Sum`.
|
||||
SumDry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub listen: Listen,
|
||||
pub output: f32,
|
||||
pub bypass: bool,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen: Listen::Layer,
|
||||
output: 0.,
|
||||
bypass: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<(), &'static str> {
|
||||
if !self.output.is_finite() {
|
||||
return Err("non-finite setting");
|
||||
}
|
||||
if (-12.0..=12.).contains(&self.output) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("setting out of range")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user