use crate::error::check_range;
use crate::numeric::fixed::{Q14, Q8};
use crate::numeric::{coefficient_to_exp_value, db_to_gain, envelope_decay, NumericContext};
use crate::{Error, SampleRate};
const WINDOW_FRAMES: usize = 100;
const BASE_DRIVE: Q8 = Q8::from_bits(401);
const TWO_OVER_PI: Q14 = Q14::from_bits(10430);
#[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 PunchConfig {
pub enabled: bool,
pub gain_db: f32,
pub impact: f32,
pub ceiling_db: f32,
pub attack_ms: f32,
}
impl Default for PunchConfig {
fn default() -> Self {
Self {
enabled: false,
gain_db: 0.0,
impact: 0.5,
ceiling_db: -3.0,
attack_ms: 10.0,
}
}
}
impl PunchConfig {
pub fn validate(self) -> Result<(), Error> {
check_range("punch.gain_db", self.gain_db, -96.0, 12.0)?;
check_range("punch.impact", self.impact, 0.0, 1.0)?;
check_range("punch.ceiling_db", self.ceiling_db, -96.0, 0.0)?;
check_range("punch.attack_ms", self.attack_ms, 0.01, 5000.0)
}
}
#[derive(Debug)]
pub(crate) struct Punch {
rate: SampleRate,
slow: Q14,
fast: Q14,
counter: usize,
peak: Q14,
slow_decay: Q14,
fast_decay: Q14,
attack_decay: Q14,
gain: Q8,
ceiling_gain: Q14,
drive: Q8,
}
impl Punch {
pub(crate) fn new(rate: SampleRate, config: PunchConfig) -> Self {
let mut result = Self {
rate,
slow: Q14::default(),
fast: Q14::default(),
counter: 0,
peak: Q14::default(),
slow_decay: Q14::default(),
fast_decay: Q14::default(),
attack_decay: Q14::default(),
gain: Q8::ONE,
ceiling_gain: Q14::default(),
drive: BASE_DRIVE,
};
result.set_config(config);
result.reset();
result
}
pub(crate) fn drive_ratio(&self) -> f32 {
(self.drive.to_bits() as f64 / BASE_DRIVE.to_bits() as f64) as f32
}
pub(crate) fn set_config(&mut self, config: PunchConfig) {
let impact = coefficient_to_exp_value(1.0, 250.0, 5000.0, 1.0 - config.impact);
let decay = |milliseconds: f64| {
Q14::from_f64(envelope_decay(
milliseconds,
self.rate.hz() as f64,
WINDOW_FRAMES,
))
};
self.slow_decay = decay(impact as f64);
self.fast_decay = decay(5000.0);
self.attack_decay = decay(config.attack_ms as f64);
self.gain = Q8::from_f64(db_to_gain(config.gain_db as f64));
self.ceiling_gain = Q14::from_f64(db_to_gain(config.ceiling_db as f64));
}
pub(crate) fn reset(&mut self) {
self.slow = Q14::default();
self.fast = Q14::default();
self.counter = 0;
self.peak = Q14::default();
self.drive = BASE_DRIVE.mul(self.gain);
}
fn follow(current: Q14, target: Q14, attack: Q14, release: Q14) -> Q14 {
let decay = if target > current { attack } else { release };
target + decay.mul(current - target)
}
pub(crate) fn process_sample(&mut self, input: i16, context: &mut NumericContext) -> i16 {
let sample = Q14::from_bits(i32::from(input) >> 1);
self.peak = self.peak.max(sample.abs());
self.counter += 1;
if self.counter == WINDOW_FRAMES {
self.counter = 0;
self.slow = Self::follow(self.slow, self.peak, self.attack_decay, self.slow_decay);
self.fast = Self::follow(self.fast, self.peak, self.attack_decay, self.fast_decay);
self.peak = Q14::default();
let smooth: Q8 = if self.slow > Q14::default() {
if self.fast >> 6 <= (self.slow >> 6) << 4 {
self.fast.div(self.slow)
} else {
Q8::from_bits(16 << 8)
}
} else {
Q8::ONE
};
self.drive = BASE_DRIVE.mul::<8, 8>(smooth).mul(self.gain);
}
let driven: Q8 = sample.mul(self.drive);
let angle = Q14::from_bits(context.atan_q14.process(driven.to_bits(), 14 - 8));
let output: Q14 = TWO_OVER_PI.mul(self.ceiling_gain.mul::<14, 14>(angle));
output.to_bits().wrapping_shl(1).clamp(-32767, 32767) as i16
}
}