pub const DEFAULT_LOW_SHELF_DB: f32 = 2.0;
pub const DEFAULT_LOW_SHELF_HZ: f32 = 180.0;
pub const DEFAULT_HIGH_SHELF_DB: f32 = 1.5;
pub const DEFAULT_HIGH_SHELF_HZ: f32 = 5_000.0;
pub fn apply_warmth(samples: &mut [i16], sample_rate: u32) {
apply_warmth_with_params(
samples,
sample_rate,
DEFAULT_LOW_SHELF_DB,
DEFAULT_LOW_SHELF_HZ,
DEFAULT_HIGH_SHELF_DB,
DEFAULT_HIGH_SHELF_HZ,
);
}
pub fn apply_warmth_with_params(
samples: &mut [i16],
sample_rate: u32,
low_shelf_db: f32,
low_freq_hz: f32,
high_shelf_db: f32,
high_freq_hz: f32,
) {
if samples.is_empty() || sample_rate == 0 {
return;
}
let sr = sample_rate as f32;
let (lo_b0, lo_b1, lo_b2, lo_a1, lo_a2) = low_shelf_coeffs(low_freq_hz, low_shelf_db, sr);
let (hi_b0, hi_b1, hi_b2, hi_a1, hi_a2) = high_shelf_coeffs(high_freq_hz, high_shelf_db, sr);
let mut lo_x1 = 0f32;
let mut lo_x2 = 0f32;
let mut lo_y1 = 0f32;
let mut lo_y2 = 0f32;
let mut hi_x1 = 0f32;
let mut hi_x2 = 0f32;
let mut hi_y1 = 0f32;
let mut hi_y2 = 0f32;
for s in samples.iter_mut() {
let x = *s as f32;
let lo_y = lo_b0 * x + lo_b1 * lo_x1 + lo_b2 * lo_x2 - lo_a1 * lo_y1 - lo_a2 * lo_y2;
lo_x2 = lo_x1;
lo_x1 = x;
lo_y2 = lo_y1;
lo_y1 = lo_y;
let hi_y = hi_b0 * lo_y + hi_b1 * hi_x1 + hi_b2 * hi_x2 - hi_a1 * hi_y1 - hi_a2 * hi_y2;
hi_x2 = hi_x1;
hi_x1 = lo_y;
hi_y2 = hi_y1;
hi_y1 = hi_y;
*s = hi_y.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
}
}
fn low_shelf_coeffs(freq_hz: f32, gain_db: f32, sr: f32) -> (f32, f32, f32, f32, f32) {
let a = 10f32.powf(gain_db / 40.0); let w0 = 2.0 * std::f32::consts::PI * freq_hz / sr;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / 2.0 * (a + 1.0 / a).sqrt();
let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha;
let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha) / a0;
let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0) / a0;
let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha) / a0;
let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0) / a0;
let a2 = ((a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha) / a0;
(b0, b1, b2, a1, a2)
}
fn high_shelf_coeffs(freq_hz: f32, gain_db: f32, sr: f32) -> (f32, f32, f32, f32, f32) {
let a = 10f32.powf(gain_db / 40.0);
let w0 = 2.0 * std::f32::consts::PI * freq_hz / sr;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / 2.0 * (a + 1.0 / a).sqrt();
let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha;
let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * a.sqrt() * alpha) / a0;
let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0) / a0;
let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha) / a0;
let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0) / a0;
let a2 = ((a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * a.sqrt() * alpha) / a0;
(b0, b1, b2, a1, a2)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyse::rms_db;
const SR: u32 = 24_000;
fn pure_tone(freq_hz: f32, amplitude: f32, secs: f32) -> Vec<i16> {
let n = (SR as f32 * secs) as usize;
(0..n)
.map(|i| {
let v =
amplitude * (2.0 * std::f32::consts::PI * freq_hz * i as f32 / SR as f32).sin();
v.clamp(i16::MIN as f32, i16::MAX as f32) as i16
})
.collect()
}
#[test]
fn empty_input_is_a_no_op() {
let mut samples: Vec<i16> = Vec::new();
apply_warmth(&mut samples, SR); }
#[test]
fn low_shelf_boosts_bass_tone() {
let original = pure_tone(100.0, 6_000.0, 0.5);
let mut boosted = original.clone();
apply_warmth_with_params(&mut boosted, SR, 3.0, 180.0, 0.0, 5_000.0);
let orig_rms: f32 = rms_db(&original);
let proc_rms: f32 = rms_db(&boosted);
assert!(
proc_rms > orig_rms,
"Low shelf should boost bass tone: before={:.2} after={:.2}",
orig_rms,
proc_rms
);
}
#[test]
fn high_shelf_boosts_presence_tone() {
let original = pure_tone(8_000.0, 6_000.0, 0.5);
let mut boosted = original.clone();
apply_warmth_with_params(&mut boosted, SR, 0.0, 180.0, 3.0, 5_000.0);
let orig_rms: f32 = rms_db(&original);
let proc_rms: f32 = rms_db(&boosted);
assert!(
proc_rms > orig_rms,
"High shelf should boost presence tone: before={:.2} after={:.2}",
orig_rms,
proc_rms
);
}
#[test]
fn midband_tone_is_unaffected() {
let original = pure_tone(1_000.0, 8_000.0, 0.5);
let mut processed = original.clone();
apply_warmth(&mut processed, SR);
let orig_rms = rms_db(&original);
let proc_rms = rms_db(&processed);
let diff_db = proc_rms - orig_rms;
assert!(diff_db.abs() < 0.5, "Mid-band altered by {:.2} dB", diff_db);
}
#[test]
fn boost_does_not_clip_headroom() {
let mut samples = pure_tone(100.0, 20_000.0, 0.3);
apply_warmth(&mut samples, SR);
let max = samples.iter().map(|&s| s.abs()).max().unwrap_or(0);
assert!(max <= i16::MAX, "Warmth EQ clipped: max sample = {}", max);
}
}