use crate::error::check_range;
use crate::filter::{Biquad, Design};
use crate::numeric::db_to_gain;
use crate::numeric::fixed::{Fixed, Q12, Q14, Q8};
use crate::{Error, SampleRate};
const WINDOW_FRAMES: usize = 100;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub struct WarmConfig {
pub enabled: bool,
pub drive_db: f32,
}
impl Default for WarmConfig {
fn default() -> Self {
Self {
enabled: false,
drive_db: 3.0,
}
}
}
impl WarmConfig {
pub fn validate(self) -> Result<(), Error> {
check_range("warm.drive_db", self.drive_db, 0.0, 6.0)
}
}
#[derive(Debug)]
pub(crate) struct Warm {
filter: Biquad,
peak: Q14,
window_peak: Q14,
counter: usize,
compression: Q8,
smooth: Q14,
drive: Q12,
output_gain: Q14,
input_threshold: Q14,
output_headroom: Q8,
}
impl Warm {
pub(crate) fn new(rate: SampleRate, config: WarmConfig) -> Self {
let mut filter = Biquad::new();
filter.set_design(Design::high_pass_one_pole(rate.hz(), 50.0));
let mut result = Self {
filter,
peak: Q14::default(),
window_peak: Q14::default(),
counter: 0,
compression: Q8::ONE,
smooth: Q14::from_f64(0.5_f64.powf(1.0 / (rate.hz() as f64 * 0.2 / 100.0))),
drive: Q12::default(),
output_gain: Q14::default(),
input_threshold: Q14::from_f64(db_to_gain(-30.0)),
output_headroom: Q8::from_f64(db_to_gain(30.0)),
};
result.set_config(config);
result
}
pub(crate) fn set_config(&mut self, config: WarmConfig) {
self.drive = Q12::from_f64(db_to_gain(config.drive_db as f64));
self.output_gain = Q14::from_f64(db_to_gain(-(config.drive_db as f64) * 0.35));
}
pub(crate) fn reset(&mut self) {
self.filter.reset();
self.peak = Q14::default();
self.window_peak = Q14::default();
self.counter = 0;
self.compression = Q8::ONE;
}
pub(crate) fn process_sample(&mut self, input: i16) -> i16 {
let mut sample = Q14::from_bits(i32::from(input) >> 1);
self.window_peak = self.window_peak.max(sample.abs());
self.counter += 1;
if self.counter == WINDOW_FRAMES {
self.counter = 0;
self.peak = if sample.abs() > self.peak {
sample.abs()
} else {
self.window_peak + self.smooth.mul(self.peak - self.window_peak)
};
self.compression = if self.peak > self.input_threshold {
Fixed::<22>::ONE.div(self.peak)
} else {
self.output_headroom
};
self.window_peak = Q14::default();
}
sample = sample
.mul::<8, 14>(self.compression)
.clamp(-Q14::ONE, Q14::ONE);
if sample > Q14::default() {
let knee: Q12 = (self.drive - Q12::ONE).mul(sample);
sample = (self.drive - knee).mul(sample);
} else {
sample = self.drive.mul::<14, 14>(sample).max(-Q14::ONE);
}
sample = sample.div(self.compression);
let output: Q14 = self.output_gain.mul::<14, 14>(sample) << 1;
self.filter
.process_sample(output)
.to_bits()
.clamp(-32767, 32767) as i16
}
}