Files
bass-foundry/plugins/weld/core/src/lib.rs
T
nvrl e3286f9094 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.
2026-11-06 07:53:20 +01:00

151 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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
}
}