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
405 lines
14 KiB
Rust
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;
|
|
}
|
|
}
|