Files
bass-foundry/plugins/patina/core/src/lib.rs
T
nvrl 15853e61ac
Check and Windows preview / check (patina) (push) Successful in 2m20s
Check and Windows preview / check (rivet) (push) Successful in 1m38s
Check and Windows preview / previews (push) Successful in 1m6s
Check and Windows preview / windows (push) Successful in 1m50s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 7s
Independent plugin release / build (push) Successful in 2m21s
restructure
2026-11-05 23:58:00 +01:00

405 lines
14 KiB
Rust

//! Independently designed spectral attenuation. No host or GUI dependencies.
mod settings;
use rustfft::{num_complex::Complex32, Fft, FftPlanner};
pub use settings::{focus_offset, knee, Focus, Settings};
use std::{f32::consts::PI, sync::Arc};
pub const GRID: usize = 512;
#[derive(Clone, Copy, Debug)]
pub struct AnalyzerFrame {
pub input: [f32; GRID],
pub reduction: [f32; GRID],
pub focus: [f32; GRID],
pub input_peak: f32,
pub output_peak: f32,
pub guard: f32,
pub sample_rate: f32,
pub latency: u32,
pub channels: usize,
}
impl Default for AnalyzerFrame {
fn default() -> Self {
Self {
input: [-120.; GRID],
reduction: [0.; GRID],
focus: [0.; GRID],
input_peak: 0.,
output_peak: 0.,
guard: 0.,
sample_rate: 48000.,
latency: 4096,
channels: 2,
}
}
}
pub fn frequency(index: usize) -> f32 {
20. * 1000f32.powf(index as f32 / (GRID - 1) as f32)
}
#[derive(Clone, Copy, Default)]
pub struct Monitor {
pub delta: bool,
}
pub struct Engine {
rate: f32,
channels: usize,
n: usize,
hop: usize,
pos: usize,
hop_pos: usize,
input: [Vec<f32>; 2],
output: [Vec<f32>; 2],
spectra: [Vec<Complex32>; 2],
window: Vec<f32>,
scratch: Vec<Complex32>,
forward: Arc<dyn Fft<f32>>,
inverse: Arc<dyn Fft<f32>>,
levels: [[f32; GRID]; 2],
target: [f32; GRID],
reduction: [f32; GRID],
curve: [f32; GRID],
mask: [f32; GRID],
map: [(usize, f32); GRID],
bin_map: Vec<(usize, f32)>,
kernel: [f32; 65],
radius: usize,
settings: Settings,
frame: AnalyzerFrame,
fresh: bool,
fast: f32,
slow: f32,
protect: f32,
sample_power: f32,
mix: f32,
gain: f32,
bypass: f32,
delta: f32,
mix_initialized: bool,
power_scale: f32,
in_peak: f32,
out_peak: f32,
}
impl Engine {
pub fn new(rate: f32, channels: usize) -> Result<Self, &'static str> {
if ![44100., 48000., 88200., 96000., 176400., 192000.].contains(&rate)
|| !(1..=2).contains(&channels)
{
return Err("Supported rates: 44.1/48/88.2/96/176.4/192 kHz; mono or stereo");
}
let scale = if rate > 96000. {
4
} else if rate > 48000. {
2
} else {
1
};
let n = 4096 * scale;
let hop = n / 8;
let mut planner = FftPlanner::new();
let forward = planner.plan_fft_forward(n);
let inverse = planner.plan_fft_inverse(n);
let scratch_len = forward
.get_inplace_scratch_len()
.max(inverse.get_inplace_scratch_len());
let window: Vec<_> = (0..n)
.map(|i| {
(0.5 - 0.5 * (2. * PI * i as f32 / n as f32).cos())
.max(0.)
.sqrt()
})
.collect();
let power_scale = 2. / window.iter().sum::<f32>().powi(2);
let nyquist = rate * 0.5;
let map = std::array::from_fn(|i| {
let bin = (frequency(i).min(nyquist) * n as f32 / rate).min((n / 2 - 1) as f32);
(bin.floor() as usize, bin.fract())
});
let bin_map = (0..=n / 2)
.map(|k| {
let grid = ((k as f32 * rate / n as f32).max(20.) / 20.).log2() / 1000f32.log2()
* (GRID - 1) as f32;
let grid = grid.clamp(0., (GRID - 1) as f32);
(
(grid.floor() as usize).min(GRID - 2),
if grid >= (GRID - 1) as f32 {
1.
} else {
grid.fract()
},
)
})
.collect();
let mut engine = Self {
rate,
channels,
n,
hop,
pos: 0,
hop_pos: 0,
input: std::array::from_fn(|_| vec![0.; n]),
output: std::array::from_fn(|_| vec![0.; n]),
spectra: std::array::from_fn(|_| vec![Complex32::default(); n]),
window,
scratch: vec![Complex32::default(); scratch_len],
forward,
inverse,
levels: [[-120.; GRID]; 2],
target: [0.; GRID],
reduction: [0.; GRID],
curve: [0.; GRID],
mask: [0.; GRID],
map,
bin_map,
kernel: [0.; 65],
radius: 0,
settings: Settings::default(),
frame: AnalyzerFrame::default(),
fresh: false,
fast: 0.,
slow: 0.,
protect: 0.,
sample_power: 0.,
mix: 1.,
gain: 1.,
bypass: 0.,
delta: 0.,
mix_initialized: false,
power_scale,
in_peak: 0.,
out_peak: 0.,
};
engine.rebuild();
Ok(engine)
}
pub fn latency_samples(&self) -> u32 {
self.n as u32
}
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()?;
if settings != self.settings {
let rebuild = settings.focus != self.settings.focus
|| settings.low != self.settings.low
|| settings.high != self.settings.high
|| settings.cut_width != self.settings.cut_width;
self.settings = settings;
if rebuild {
self.rebuild();
}
}
if !self.mix_initialized {
self.mix = settings.mix / 100.;
self.gain = 10f32.powf(settings.output / 20.);
self.bypass = f32::from(settings.bypass);
self.mix_initialized = true;
}
Ok(())
}
fn rebuild(&mut self) {
let step = 1000f32.log2() / (GRID - 1) as f32;
self.radius = ((self.settings.cut_width / step * 1.3).ceil() as usize).clamp(1, 64);
let sigma = self.settings.cut_width / step / 2.355;
for j in 0..=self.radius {
self.kernel[j] = (-0.5 * (j as f32 / sigma).powi(2)).exp();
}
for i in 0..GRID {
let f = frequency(i);
self.curve[i] = focus_offset(f, &self.settings.focus);
let low = (f / self.settings.low).log2() / 0.15;
let high = (self.settings.high.min(self.rate * 0.49) / f).log2() / 0.15;
let edge = low.min(high).clamp(0., 1.);
self.mask[i] = edge * edge * (3. - 2. * edge);
}
}
pub fn reset(&mut self) {
for channel in &mut self.input {
channel.fill(0.);
}
for channel in &mut self.output {
channel.fill(0.);
}
self.reduction.fill(0.);
self.pos = 0;
self.hop_pos = 0;
self.fast = 0.;
self.slow = 0.;
self.protect = 0.;
self.sample_power = 0.;
self.in_peak = 0.;
self.out_peak = 0.;
self.fresh = false;
self.frame = AnalyzerFrame {
sample_rate: self.rate,
latency: self.n as u32,
..Default::default()
};
self.mix_initialized = false;
self.delta = 0.;
}
pub fn take_analyzer(&mut self) -> Option<AnalyzerFrame> {
if self.fresh {
self.fresh = false;
Some(self.frame)
} else {
None
}
}
/// Sample scheduling is independent of the caller's block partition.
pub fn process_sample(&mut self, sample: [f32; 2], monitor: Monitor) -> [f32; 2] {
let mut result = [0.; 2];
let mut energy = 0f32;
let smooth = 1. - (-1. / (self.rate * 0.01)).exp();
self.mix += (self.settings.mix / 100. - self.mix) * smooth;
self.gain += (10f32.powf(self.settings.output / 20.) - self.gain) * smooth;
self.bypass += (f32::from(self.settings.bypass) - self.bypass) * smooth;
self.delta += (f32::from(monitor.delta) - self.delta) * smooth;
for c in 0..self.channels {
let x = if sample[c].is_finite() {
sample[c].clamp(-64., 64.)
} else {
0.
};
let dry = self.input[c][self.pos];
let wet = self.output[c][self.pos];
self.output[c][self.pos] = 0.;
let x = if x.abs() < 1e-20 { 0. } else { x };
self.input[c][self.pos] = x;
let mixed = (dry + (wet - dry) * self.mix) * self.gain;
let normal = mixed + (dry - mixed) * self.bypass;
let audible = normal + ((dry - wet) - normal) * self.delta;
result[c] = if audible.is_finite() { audible } else { 0. };
self.in_peak = self.in_peak.max(x.abs());
self.out_peak = self.out_peak.max(result[c].abs());
energy = energy.max(x * x);
}
self.fast += (energy - self.fast) * (1. - (-1. / (self.rate * 0.001)).exp());
self.slow += (energy - self.slow) * (1. - (-1. / (self.rate * 0.05)).exp());
let onset =
((10. * ((self.fast + 1e-20) / (self.slow + 1e-20)).log10() - 3.) / 9.).clamp(0., 1.);
self.protect = onset.max(self.protect * (-1. / (self.rate * 0.03)).exp());
self.sample_power += energy;
self.pos = (self.pos + 1) % self.n;
self.hop_pos += 1;
if self.hop_pos == self.hop {
self.hop_pos = 0;
self.process_window();
}
result
}
fn process_window(&mut self) {
for c in 0..self.channels {
for i in 0..self.n {
self.spectra[c][i] =
Complex32::new(self.input[c][(self.pos + i) % self.n] * self.window[i], 0.);
}
self.forward
.process_with_scratch(&mut self.spectra[c], &mut self.scratch);
for i in 0..GRID {
let (k, w) = self.map[i];
let power = self.spectra[c][k].norm_sqr() * (1. - w)
+ self.spectra[c][k + 1].norm_sqr() * w;
self.levels[c][i] = 10. * (power * self.power_scale).max(1e-20).log10();
}
}
let awake = self.sample_power / self.hop as f32 > 1e-9;
self.sample_power = 0.;
let step = 1000f32.log2() / (GRID - 1) as f32;
let radius = (self.settings.neighborhood / step * 0.5).round() as usize;
self.target.fill(0.);
for c in 0..self.channels {
let mut prefix = [0.; GRID + 1];
for i in 0..GRID {
prefix[i + 1] = prefix[i] + self.levels[c][i];
}
for i in 0..GRID {
let low = i.saturating_sub(radius);
let high = (i + radius + 1).min(GRID);
let e0 = i.saturating_sub(1).max(low);
let e1 = (i + 2).min(high);
let count = (high - low) - (e1 - e0);
let baseline =
(prefix[high] - prefix[low] - (prefix[e1] - prefix[e0])) / count.max(1) as f32;
let excess =
self.levels[c][i] - baseline + self.curve[i] - self.settings.sensitivity;
let target = if awake {
knee(excess, self.settings.knee) * self.settings.amount * 0.02
} else {
0.
};
self.target[i] = self.target[i].max(target.min(self.settings.max_cut));
}
}
let attack = (-(self.hop as f32) / (self.rate * self.settings.attack * 0.001)).exp();
let release = (-(self.hop as f32) / (self.rate * self.settings.release * 0.001)).exp();
for i in 0..GRID {
let low = i.saturating_sub(self.radius);
let high = (i + self.radius + 1).min(GRID);
let mut sum = 0.;
let mut weight = 0.;
for j in low..high {
let w = self.kernel[i.abs_diff(j)];
sum += self.target[j] * w;
weight += w;
}
let target = sum / weight;
let alpha = if target > self.reduction[i] {
attack
} else {
release
};
self.reduction[i] = alpha * self.reduction[i] + (1. - alpha) * target;
if self.reduction[i] < 1e-12 {
self.reduction[i] = 0.;
}
self.frame.reduction[i] = (self.reduction[i]
* self.mask[i]
* (1. - self.protect * self.settings.transient_guard / 100.))
.clamp(0., self.settings.max_cut);
self.frame.focus[i] = self.curve[i];
self.frame.input[i] = self.levels[0][i].max(if self.channels == 2 {
self.levels[1][i]
} else {
-200.
});
}
for c in 0..self.channels {
for k in 0..=self.n / 2 {
let (i, w) = self.bin_map[k];
let d = self.frame.reduction[i] * (1. - w) + self.frame.reduction[i + 1] * w;
let f = k as f32 * self.rate / self.n as f32;
let d = if f >= self.settings.low && f <= self.settings.high.min(self.rate * 0.49) {
d
} else {
0.
};
let g = 10f32.powf(-d / 20.);
self.spectra[c][k] *= g;
if k > 0 && k < self.n / 2 {
self.spectra[c][self.n - k] *= g;
}
}
self.inverse
.process_with_scratch(&mut self.spectra[c], &mut self.scratch);
// Eight overlaps of a periodic Hann sum to four.
for i in 0..self.n {
self.output[c][(self.pos + i) % self.n] +=
self.spectra[c][i].re * self.window[i] / (self.n as f32 * 4.);
}
}
self.frame.input_peak = self.in_peak;
self.frame.output_peak = self.out_peak;
self.frame.guard = self.protect;
self.frame.sample_rate = self.rate;
self.frame.latency = self.n as u32;
self.frame.channels = self.channels;
self.in_peak = 0.;
self.out_peak = 0.;
self.fresh = true;
}
}