//! 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 { 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 } }