#![forbid(unsafe_code)]
#![allow(clippy::excessive_precision)]
#![allow(clippy::cast_lossless)]
use std::f64::consts::PI;
const REF_SAMPLE_RATE: f64 = 48_000.0;
const K_STAGE1_B_48K: [f64; 3] = [
1.535_124_859_586_97,
-2.691_696_189_406_38,
1.198_392_810_852_85,
];
const K_STAGE1_A_48K: [f64; 2] = [-1.690_659_293_182_41, 0.732_480_774_215_85];
const K_STAGE2_B_48K: [f64; 3] = [1.0, -2.0, 1.0];
const K_STAGE2_A_48K: [f64; 2] = [-1.990_047_454_833_98, 0.990_072_250_366_21];
#[derive(Clone, Debug)]
pub struct KWeightFilter {
sample_rate: f64,
pre_filter: BiquadFilter,
rlb_filter: BiquadFilter,
}
impl KWeightFilter {
#[must_use]
pub fn new(sample_rate: f64) -> Self {
let (pre_filter, rlb_filter) = if (sample_rate - REF_SAMPLE_RATE).abs() < 0.5 {
(
BiquadFilter::from_ba(K_STAGE1_B_48K, K_STAGE1_A_48K),
BiquadFilter::from_ba(K_STAGE2_B_48K, K_STAGE2_A_48K),
)
} else {
let (b1, a1) = Self::calculate_pre_filter(sample_rate);
let (b2, a2) = Self::calculate_rlb_filter(sample_rate);
(BiquadFilter::from_ba(b1, a1), BiquadFilter::from_ba(b2, a2))
};
Self {
sample_rate,
pre_filter,
rlb_filter,
}
}
pub fn process(&mut self, input: f64) -> f64 {
let stage1 = self.pre_filter.process(input);
self.rlb_filter.process(stage1)
}
pub fn process_samples(&mut self, samples: &mut [f64]) {
for sample in samples {
*sample = self.process(*sample);
}
}
pub fn reset(&mut self) {
self.pre_filter.reset();
self.rlb_filter.reset();
}
fn calculate_pre_filter(sample_rate: f64) -> ([f64; 3], [f64; 2]) {
const F0: f64 = 1_681.974_450_955_533;
const G_DB: f64 = 3.999_843_853_973_347;
const Q: f64 = 0.707_175_236_955_420;
let k = (PI * F0 / sample_rate).tan();
let k_squared = k * k;
let vh = 10.0_f64.powf(G_DB / 20.0); let vb = vh.powf(0.5);
let denom = 1.0 + k / Q + k_squared;
let b0 = (vh + vb * k / Q + k_squared) / denom;
let b1 = 2.0 * (k_squared - vh) / denom;
let b2 = (vh - vb * k / Q + k_squared) / denom;
let a1 = 2.0 * (k_squared - 1.0) / denom;
let a2 = (1.0 - k / Q + k_squared) / denom;
([b0, b1, b2], [a1, a2])
}
fn calculate_rlb_filter(sample_rate: f64) -> ([f64; 3], [f64; 2]) {
const F0: f64 = 38.135_470_876_024_44;
const Q: f64 = 0.500_327_037_323_877;
let k = (PI * F0 / sample_rate).tan();
let k_squared = k * k;
let denom = 1.0 + k / Q + k_squared;
let b0 = 1.0 / denom;
let b1 = -2.0 / denom;
let b2 = 1.0 / denom;
let a1 = 2.0 * (k_squared - 1.0) / denom;
let a2 = (1.0 - k / Q + k_squared) / denom;
([b0, b1, b2], [a1, a2])
}
#[must_use]
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
}
#[derive(Clone, Debug)]
struct BiquadFilter {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
x1: f64,
x2: f64,
y1: f64,
y2: f64,
}
impl BiquadFilter {
fn new(b0: f64, b1: f64, b2: f64, a1: f64, a2: f64) -> Self {
Self {
b0,
b1,
b2,
a1,
a2,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
fn from_ba(b: [f64; 3], a: [f64; 2]) -> Self {
Self::new(b[0], b[1], b[2], a[0], a[1])
}
fn process(&mut self, input: f64) -> f64 {
let output = self.b0 * input + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = input;
self.y2 = self.y1;
self.y1 = output;
output
}
fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
#[derive(Clone, Debug)]
pub struct KWeightFilterBank {
filters: Vec<KWeightFilter>,
sample_rate: f64,
}
impl KWeightFilterBank {
#[must_use]
pub fn new(channels: usize, sample_rate: f64) -> Self {
let filters = (0..channels)
.map(|_| KWeightFilter::new(sample_rate))
.collect();
Self {
filters,
sample_rate,
}
}
pub fn process_interleaved(
&mut self,
samples: &[f64],
channels: usize,
output: &mut [f64],
) -> usize {
if channels == 0 || channels != self.filters.len() {
return 0;
}
let frames = samples.len() / channels;
if output.len() < samples.len() {
return 0;
}
for frame in 0..frames {
for ch in 0..channels {
let idx = frame * channels + ch;
output[idx] = self.filters[ch].process(samples[idx]);
}
}
frames
}
pub fn process_planar(&mut self, channels: &mut [Vec<f64>]) {
for (ch, samples) in channels.iter_mut().enumerate() {
if ch < self.filters.len() {
self.filters[ch].process_samples(samples);
}
}
}
pub fn reset(&mut self) {
for filter in &mut self.filters {
filter.reset();
}
}
#[must_use]
pub fn channels(&self) -> usize {
self.filters.len()
}
#[must_use]
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
}
#[cfg(test)]
mod tests {
use super::*;
const ITU_TABLE1_RESPONSE_DB: [(f64, f64); 3] =
[(100.0, -1.1335), (997.0, 0.6910), (4000.0, 3.9680)];
fn measure_chain_gain_db(sample_rate: f64, freq_hz: f64) -> f64 {
let mut filter = KWeightFilter::new(sample_rate);
let warmup = (sample_rate / 5.0) as usize;
for i in 0..warmup {
let x = (2.0 * PI * freq_hz * i as f64 / sample_rate).sin();
filter.process(x);
}
let n = sample_rate as usize;
let mut sum_in = 0.0;
let mut sum_out = 0.0;
for i in warmup..(warmup + n) {
let x = (2.0 * PI * freq_hz * i as f64 / sample_rate).sin();
let y = filter.process(x);
sum_in += x * x;
sum_out += y * y;
}
10.0 * (sum_out / sum_in).log10()
}
#[test]
fn test_k_weight_chain_matches_itu_table1() {
for (freq, expected_db) in ITU_TABLE1_RESPONSE_DB {
let measured = measure_chain_gain_db(48_000.0, freq);
assert!(
(measured - expected_db).abs() <= 0.1,
"K-weighting at {freq} Hz = {measured:.4} dB, expected {expected_db:.4} dB (ITU-R BS.1770-4 Table 1)"
);
}
}
#[test]
fn test_k_weight_chain_matches_itu_table1_at_44100() {
for (freq, expected_db) in ITU_TABLE1_RESPONSE_DB {
let measured = measure_chain_gain_db(44_100.0, freq);
assert!(
(measured - expected_db).abs() <= 0.1,
"K-weighting at {freq} Hz (44.1 kHz) = {measured:.4} dB, expected {expected_db:.4} dB"
);
}
}
#[test]
fn test_k_weight_attenuates_dc() {
let mut filter = KWeightFilter::new(48_000.0);
let mut last = 0.0;
for _ in 0..10_000 {
last = filter.process(1.0);
}
assert!(
last.abs() < 0.01,
"K-weighting must attenuate DC; last output = {last:.6}"
);
}
#[test]
fn test_filter_bank_processes_all_channels() {
let mut bank = KWeightFilterBank::new(2, 48_000.0);
let input = vec![0.1, 0.2, 0.3, 0.4];
let mut output = vec![0.0; 4];
let frames = bank.process_interleaved(&input, 2, &mut output);
assert_eq!(frames, 2);
assert!(output.iter().all(|x| x.is_finite()));
}
#[test]
fn test_filter_reset_clears_state() {
let mut filter = KWeightFilter::new(48_000.0);
filter.process(0.5);
filter.reset();
assert_eq!(filter.process(0.0), 0.0);
}
}