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.
151 lines
4.3 KiB
Rust
151 lines
4.3 KiB
Rust
//! 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
|
||
}
|
||
}
|