#![allow(clippy::cast_precision_loss)]
use std::f32::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub struct BassEnhancerConfig {
pub frequency_hz: f32,
pub harmonics: u32,
pub drive: f32,
pub mix: f32,
}
impl Default for BassEnhancerConfig {
fn default() -> Self {
Self {
frequency_hz: 80.0,
harmonics: 3,
drive: 0.5,
mix: 0.4,
}
}
}
impl BassEnhancerConfig {
#[must_use]
pub fn gentle() -> Self {
Self {
frequency_hz: 100.0,
harmonics: 2,
drive: 0.25,
mix: 0.25,
}
}
#[must_use]
pub fn aggressive() -> Self {
Self {
frequency_hz: 80.0,
harmonics: 5,
drive: 0.8,
mix: 0.6,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.frequency_hz <= 0.0 {
return Err(format!(
"frequency_hz must be positive, got {}",
self.frequency_hz
));
}
if self.harmonics == 0 || self.harmonics > 16 {
return Err(format!(
"harmonics must be between 1 and 16, got {}",
self.harmonics
));
}
if !(0.0..=1.0).contains(&self.drive) {
return Err(format!("drive must be in [0.0, 1.0], got {}", self.drive));
}
if !(0.0..=1.0).contains(&self.mix) {
return Err(format!("mix must be in [0.0, 1.0], got {}", self.mix));
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct Biquad {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
s1: f32,
s2: f32,
}
impl Biquad {
fn process(&mut self, x: f32) -> f32 {
let y = self.b0 * x + self.s1;
self.s1 = self.b1 * x - self.a1 * y + self.s2;
self.s2 = self.b2 * x - self.a2 * y;
y
}
fn reset(&mut self) {
self.s1 = 0.0;
self.s2 = 0.0;
}
fn butterworth_lowpass(freq_hz: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / std::f32::consts::SQRT_2; let a0 = 1.0 + alpha;
Self {
b0: ((1.0 - cos_w0) / 2.0) / a0,
b1: (1.0 - cos_w0) / a0,
b2: ((1.0 - cos_w0) / 2.0) / a0,
a1: (-2.0 * cos_w0) / a0,
a2: (1.0 - alpha) / a0,
s1: 0.0,
s2: 0.0,
}
}
fn butterworth_highpass(freq_hz: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / std::f32::consts::SQRT_2;
let a0 = 1.0 + alpha;
Self {
b0: ((1.0 + cos_w0) / 2.0) / a0,
b1: (-(1.0 + cos_w0)) / a0,
b2: ((1.0 + cos_w0) / 2.0) / a0,
a1: (-2.0 * cos_w0) / a0,
a2: (1.0 - alpha) / a0,
s1: 0.0,
s2: 0.0,
}
}
}
pub struct BassEnhancer {
config: BassEnhancerConfig,
lp: Biquad,
hp: Biquad,
sample_rate: f32,
}
impl BassEnhancer {
#[must_use]
pub fn new(config: BassEnhancerConfig, sample_rate: f32) -> Self {
let freq = config.frequency_hz.clamp(20.0, sample_rate * 0.45);
let lp = Biquad::butterworth_lowpass(freq, sample_rate);
let hp = Biquad::butterworth_highpass(freq, sample_rate);
Self {
config,
lp,
hp,
sample_rate,
}
}
#[must_use]
pub fn config(&self) -> &BassEnhancerConfig {
&self.config
}
pub fn set_config(&mut self, config: BassEnhancerConfig) {
let freq = config.frequency_hz.clamp(20.0, self.sample_rate * 0.45);
self.lp = Biquad::butterworth_lowpass(freq, self.sample_rate);
self.hp = Biquad::butterworth_highpass(freq, self.sample_rate);
self.config = config;
}
pub fn process_sample(&mut self, input: f32) -> f32 {
let bass = self.lp.process(input);
let harmonic_content = self.generate_harmonics(bass);
let harmonics_hp = self.hp.process(harmonic_content);
input + harmonics_hp * self.config.mix
}
pub fn process(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.process_sample(*sample);
}
}
pub fn reset(&mut self) {
self.lp.reset();
self.hp.reset();
}
fn generate_harmonics(&self, x: f32) -> f32 {
if x.abs() < 1e-10 {
return 0.0;
}
let drive = self.config.drive;
let harmonics = self.config.harmonics;
let mut out = 0.0f32;
for n in 1..=harmonics {
let gain = drive * (1.0 / n as f32);
let driven = x * gain * (1 + n) as f32;
let shaped = if driven.abs() < 20.0 {
driven.tanh()
} else {
driven.signum()
};
out += shaped / n as f32;
}
out * drive
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let c = BassEnhancerConfig::default();
assert!(c.frequency_hz > 0.0);
assert!(c.harmonics >= 1);
assert!(c.drive >= 0.0 && c.drive <= 1.0);
assert!(c.mix >= 0.0 && c.mix <= 1.0);
assert!(c.validate().is_ok());
}
#[test]
fn test_config_gentle() {
let c = BassEnhancerConfig::gentle();
assert!(c.validate().is_ok());
}
#[test]
fn test_config_aggressive() {
let c = BassEnhancerConfig::aggressive();
assert!(c.validate().is_ok());
}
#[test]
fn test_config_validate_bad_frequency() {
let c = BassEnhancerConfig {
frequency_hz: 0.0,
..Default::default()
};
assert!(c.validate().is_err());
}
#[test]
fn test_config_validate_zero_harmonics() {
let c = BassEnhancerConfig {
harmonics: 0,
..Default::default()
};
assert!(c.validate().is_err());
}
#[test]
fn test_config_validate_bad_drive() {
let c = BassEnhancerConfig {
drive: 1.5,
..Default::default()
};
assert!(c.validate().is_err());
let c2 = BassEnhancerConfig {
drive: -0.1,
..Default::default()
};
assert!(c2.validate().is_err());
}
#[test]
fn test_config_validate_bad_mix() {
let c = BassEnhancerConfig {
mix: 2.0,
..Default::default()
};
assert!(c.validate().is_err());
}
#[test]
fn test_new_does_not_panic() {
let _ = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
}
#[test]
fn test_new_with_extreme_frequency() {
let c = BassEnhancerConfig {
frequency_hz: 100_000.0,
..Default::default()
};
let _ = BassEnhancer::new(c, 44_100.0);
}
#[test]
fn test_process_sample_finite() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
for i in 0..1024 {
let s = (i as f32 * 0.01).sin() * 0.5;
let out = e.process_sample(s);
assert!(out.is_finite(), "output not finite at sample {i}");
}
}
#[test]
fn test_process_silent_input() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
let out = e.process_sample(0.0);
assert!((out).abs() < 1e-6);
}
#[test]
fn test_process_buffer_length_preserved() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
let mut buf = vec![0.1f32; 256];
e.process(&mut buf);
assert_eq!(buf.len(), 256);
}
#[test]
fn test_process_buffer_all_finite() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
let mut buf: Vec<f32> = (0..512)
.map(|i| (i as f32 * 2.0 * PI / 512.0 * 80.0).sin() * 0.3)
.collect();
e.process(&mut buf);
for &v in &buf {
assert!(v.is_finite());
}
}
#[test]
fn test_zero_mix_passes_dry() {
let config = BassEnhancerConfig {
mix: 0.0,
..Default::default()
};
let mut e = BassEnhancer::new(config, 48_000.0);
let input = 0.5f32;
let out = e.process_sample(input);
assert!(out.is_finite());
}
#[test]
fn test_harmonics_add_energy() {
let config = BassEnhancerConfig {
harmonics: 4,
drive: 0.8,
mix: 0.9,
..Default::default()
};
let mut e = BassEnhancer::new(config, 48_000.0);
for _ in 0..100 {
e.process_sample(0.3);
}
let out = e.process_sample(0.3f32);
assert!(out.is_finite());
assert!(out.abs() > 0.0);
}
#[test]
fn test_reset_clears_state() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
for _ in 0..100 {
e.process_sample(0.5);
}
e.reset();
let out = e.process_sample(0.5);
assert!(out.is_finite());
}
#[test]
fn test_set_config_updates() {
let mut e = BassEnhancer::new(BassEnhancerConfig::default(), 48_000.0);
let new_cfg = BassEnhancerConfig {
frequency_hz: 120.0,
..Default::default()
};
e.set_config(new_cfg.clone());
assert!((e.config().frequency_hz - 120.0).abs() < 1e-5);
}
#[test]
fn test_biquad_lowpass_dc_passes() {
let mut f = Biquad::butterworth_lowpass(100.0, 48_000.0);
let mut out = 0.0f32;
for _ in 0..1000 {
out = f.process(1.0);
}
assert!((out - 1.0).abs() < 0.01, "DC not passing lowpass: {out}");
}
#[test]
fn test_biquad_highpass_dc_blocked() {
let mut f = Biquad::butterworth_highpass(100.0, 48_000.0);
let mut out = 0.0f32;
for _ in 0..1000 {
out = f.process(1.0);
}
assert!(out.abs() < 0.01, "DC not blocked by highpass: {out}");
}
}