use base64::{Engine as _, engine::general_purpose};
use byteorder::{BigEndian, WriteBytesExt};
use ndarray::Array1;
use std::f64::consts::PI;
use std::fmt;
use crate::{DEFAULT_Q_HIGH_LOW_PASS, DEFAULT_Q_HIGH_LOW_SHELF, q2bw};
pub type Peq = Vec<(f64, Biquad)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BiquadFilterType {
Lowpass,
Highpass,
HighpassVariableQ,
Bandpass,
Peak,
Notch,
Lowshelf,
Highshelf,
}
impl BiquadFilterType {
pub fn short_name(&self) -> &'static str {
match self {
BiquadFilterType::Lowpass => "LP",
BiquadFilterType::Highpass => "HP",
BiquadFilterType::HighpassVariableQ => "HPQ",
BiquadFilterType::Bandpass => "BP",
BiquadFilterType::Peak => "PK",
BiquadFilterType::Notch => "NO",
BiquadFilterType::Lowshelf => "LS",
BiquadFilterType::Highshelf => "HS",
}
}
pub fn long_name(&self) -> &'static str {
match self {
BiquadFilterType::Lowpass => "Lowpass",
BiquadFilterType::Highpass => "Highpass",
BiquadFilterType::HighpassVariableQ => "HighpassVariableQ",
BiquadFilterType::Bandpass => "Bandpass",
BiquadFilterType::Peak => "Peak",
BiquadFilterType::Notch => "Notch",
BiquadFilterType::Lowshelf => "Lowshelf",
BiquadFilterType::Highshelf => "Highshelf",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Biquad {
pub filter_type: BiquadFilterType,
pub freq: f64,
pub srate: f64,
pub q: f64,
pub db_gain: f64,
a1: f64,
a2: f64,
b0: f64,
b1: f64,
b2: f64,
x1: f64,
x2: f64,
y1: f64,
y2: f64,
r_up0: f64,
r_up1: f64,
r_up2: f64,
r_dw0: f64,
r_dw1: f64,
r_dw2: f64,
}
impl Biquad {
pub fn new(filter_type: BiquadFilterType, freq: f64, srate: f64, q: f64, db_gain: f64) -> Self {
let mut biquad = Biquad {
filter_type,
freq,
srate,
q,
db_gain,
a1: 0.0,
a2: 0.0,
b0: 0.0,
b1: 0.0,
b2: 0.0,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
r_up0: 0.0,
r_up1: 0.0,
r_up2: 0.0,
r_dw0: 0.0,
r_dw1: 0.0,
r_dw2: 0.0,
};
if biquad.filter_type == BiquadFilterType::Notch {
biquad.q = 30.0;
} else if biquad.q == 0.0 {
match biquad.filter_type {
BiquadFilterType::Bandpass
| BiquadFilterType::Highpass
| BiquadFilterType::Lowpass => {
biquad.q = DEFAULT_Q_HIGH_LOW_PASS;
}
BiquadFilterType::Lowshelf | BiquadFilterType::Highshelf => {
biquad.q = DEFAULT_Q_HIGH_LOW_SHELF;
}
_ => {}
}
}
if biquad.q <= 0.0 {
biquad.q = 1.0e-2;
}
biquad.compute_coeffs();
biquad
}
fn compute_coeffs(&mut self) {
let a = 10.0_f64.powf(self.db_gain / 40.0);
let omega = 2.0 * PI * self.freq / self.srate;
let sn = omega.sin();
let cs = omega.cos();
let alpha = sn / (2.0 * self.q);
let beta = (a + a).sqrt();
let (b0, b1, b2, a0, a1, a2);
match self.filter_type {
BiquadFilterType::Lowpass => {
b0 = (1.0 - cs) / 2.0;
b1 = 1.0 - cs;
b2 = (1.0 - cs) / 2.0;
a0 = 1.0 + alpha;
a1 = -2.0 * cs;
a2 = 1.0 - alpha;
}
BiquadFilterType::Highpass | BiquadFilterType::HighpassVariableQ => {
b0 = (1.0 + cs) / 2.0;
b1 = -(1.0 + cs);
b2 = (1.0 + cs) / 2.0;
a0 = 1.0 + alpha;
a1 = -2.0 * cs;
a2 = 1.0 - alpha;
}
BiquadFilterType::Bandpass => {
b0 = alpha;
b1 = 0.0;
b2 = -alpha;
a0 = 1.0 + alpha;
a1 = -2.0 * cs;
a2 = 1.0 - alpha;
}
BiquadFilterType::Notch => {
b0 = 1.0;
b1 = -2.0 * cs;
b2 = 1.0;
a0 = 1.0 + alpha;
a1 = -2.0 * cs;
a2 = 1.0 - alpha;
}
BiquadFilterType::Peak => {
b0 = 1.0 + (alpha * a);
b1 = -2.0 * cs;
b2 = 1.0 - (alpha * a);
a0 = 1.0 + (alpha / a);
a1 = -2.0 * cs;
a2 = 1.0 - (alpha / a);
}
BiquadFilterType::Lowshelf => {
b0 = a * ((a + 1.0) - (a - 1.0) * cs + beta * sn);
b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cs);
b2 = a * ((a + 1.0) - (a - 1.0) * cs - beta * sn);
a0 = (a + 1.0) + (a - 1.0) * cs + beta * sn;
a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cs);
a2 = (a + 1.0) + (a - 1.0) * cs - beta * sn;
}
BiquadFilterType::Highshelf => {
b0 = a * ((a + 1.0) + (a - 1.0) * cs + beta * sn);
b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cs);
b2 = a * ((a + 1.0) + (a - 1.0) * cs - beta * sn);
a0 = (a + 1.0) - (a - 1.0) * cs + beta * sn;
a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cs);
a2 = (a + 1.0) - (a - 1.0) * cs - beta * sn;
}
}
self.b0 = b0 / a0;
self.b1 = b1 / a0;
self.b2 = b2 / a0;
self.a1 = a1 / a0;
self.a2 = a2 / a0;
self.r_up0 = (self.b0 + self.b1 + self.b2).powi(2);
self.r_up1 = -4.0 * (self.b0 * self.b1 + 4.0 * self.b0 * self.b2 + self.b1 * self.b2);
self.r_up2 = 16.0 * self.b0 * self.b2;
self.r_dw0 = (1.0 + self.a1 + self.a2).powi(2);
self.r_dw1 = -4.0 * (self.a1 + 4.0 * self.a2 + self.a1 * self.a2);
self.r_dw2 = 16.0 * self.a2;
}
pub fn process(&mut self, x: f64) -> f64 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
pub fn result(&self, f: f64) -> f64 {
let phi = (PI * f / self.srate).sin().powi(2);
let phi2 = phi * phi;
let numerator = self.r_up0 + self.r_up1 * phi + self.r_up2 * phi2;
let denominator = self.r_dw0 + self.r_dw1 * phi + self.r_dw2 * phi2;
let result = (numerator / denominator).max(0.0);
result.sqrt()
}
pub fn log_result(&self, f: f64) -> f64 {
let result = self.result(f);
if result > 0.0 {
20.0 * result.log10()
} else {
-200.0 }
}
pub fn np_log_result(&self, freq: &Array1<f64>) -> Array1<f64> {
let coeff = PI / self.srate;
let phi = (freq * coeff).mapv(f64::sin).mapv(|x| x.powi(2));
let phi2 = &phi * φ
let r_up = self.r_up0 + self.r_up1 * &phi + self.r_up2 * &phi2;
let r_dw = self.r_dw0 + self.r_dw1 * &phi + self.r_dw2 * &phi2;
let r = r_up / r_dw;
let min_val = 1.0e-20;
r.mapv(|val| val.max(min_val))
.mapv(f64::sqrt)
.mapv(f64::log10)
* 20.0
}
pub fn constants(&self) -> (f64, f64, f64, f64, f64) {
(self.a1, self.a2, self.b0, self.b1, self.b2)
}
}
impl fmt::Display for Biquad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Type:{},Freq:{:.1},Rate:{:.1},Q:{:.1},Gain:{:.1}",
self.filter_type.short_name(),
self.freq,
self.srate,
self.q,
self.db_gain
)
}
}
#[derive(Debug, Clone, Default)]
pub struct FilterRow {
pub freq: f64,
pub q: f64,
pub gain: f64,
pub kind: &'static str,
}
pub fn compute_peq_response(freqs: &Array1<f64>, peq: &Peq, _sample_rate: f64) -> Array1<f64> {
if peq.is_empty() {
return Array1::zeros(freqs.len());
}
let mut response = Array1::zeros(freqs.len());
for (weight, filter) in peq {
response += &(filter.np_log_result(freqs) * *weight);
}
response
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bw2q;
use ndarray::array;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn test_bw_q_roundtrip() {
let qs = [0.5, 1.0, 2.0, 5.0];
for &q in &qs {
let bw = q2bw(q);
let q2 = bw2q(bw);
assert!(
approx_eq(q, q2, 1e-9),
"roundtrip failed: q={} -> bw={} -> q2={}",
q,
bw,
q2
);
}
}
#[test]
fn test_biquad_np_log_result_is_finite() {
let bq = Biquad::new(BiquadFilterType::Peak, 1_000.0, 48_000.0, 1.0, 6.0);
let freqs = array![20.0, 100.0, 1_000.0, 10_000.0, 20_000.0];
let resp = bq.np_log_result(&freqs);
for (i, v) in resp.iter().enumerate() {
assert!(v.is_finite(), "response at idx {} not finite: {}", i, v);
}
}
#[test]
fn peak_with_zero_q_is_safely_clamped() {
let bq = Biquad::new(BiquadFilterType::Peak, 1_000.0, 48_000.0, 0.0, 3.0);
let freqs = array![20.0, 100.0, 1_000.0, 10_000.0, 20_000.0];
let resp = bq.np_log_result(&freqs);
for (i, v) in resp.iter().enumerate() {
assert!(v.is_finite(), "response at idx {} not finite: {}", i, v);
}
}
#[test]
fn test_a_weighting() {
let w_1k = a_weighting_db(1000.0);
assert!(
(w_1k - 0.0).abs() < 1.0,
"A-weighting at 1kHz should be ~0 dB"
);
let w_100 = a_weighting_db(100.0);
assert!(w_100 < -15.0, "A-weighting at 100Hz should be < -15 dB");
let w_4k = a_weighting_db(4000.0);
assert!(w_4k > 0.0, "A-weighting at 4kHz should be positive");
}
#[test]
fn test_k_weighting() {
let w_30 = k_weighting_db(30.0);
assert!(w_30 < -5.0, "K-weighting at 30Hz should be attenuated");
let w_1k = k_weighting_db(1000.0);
assert!(
w_1k > w_30,
"K-weighting at 1kHz should be less attenuated than 30Hz"
);
let w_5k = k_weighting_db(5000.0);
assert!(
w_5k > w_1k,
"K-weighting at 5kHz should have more gain than 1kHz"
);
}
#[test]
fn test_peq_loudness_gain_flat() {
let peq: Peq = vec![];
let gain = peq_loudness_gain(&peq, "k");
assert_eq!(gain, 0.0);
}
#[test]
fn test_peq_loudness_gain_boost() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 6.0);
let peq = vec![(1.0, bq)];
let gain_k = peq_loudness_gain(&peq, "k");
let gain_a = peq_loudness_gain(&peq, "a");
println!("Test: +6 dB peak at 1kHz");
println!(" K-weighted gain: {:.2} dB", gain_k);
println!(" A-weighted gain: {:.2} dB", gain_a);
assert!(
gain_k < 0.0,
"Gain compensation for boost should be negative (K-weighting)"
);
assert!(
gain_a < 0.0,
"Gain compensation for boost should be negative (A-weighting)"
);
assert!(
gain_k > -5.0 && gain_k < 0.0,
"K-weighted gain should be between -5 and 0 dB"
);
assert!(
gain_a > -5.0 && gain_a < 0.0,
"A-weighted gain should be between -5 and 0 dB"
);
}
#[test]
fn test_peq_loudness_gain_demo() {
println!("\n=== PEQ Loudness Compensation Demo ===\n");
println!("1. +6 dB peak at 1 kHz:");
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 6.0);
let peq1 = vec![(1.0, bq1)];
println!(
" Anti-clip: {:.2} dB, K-weighted: {:.2} dB, A-weighted: {:.2} dB",
peq_preamp_gain(&peq1),
peq_loudness_gain(&peq1, "k"),
peq_loudness_gain(&peq1, "a")
);
println!("2. +6 dB bass at 100 Hz:");
let bq2 = Biquad::new(BiquadFilterType::Peak, 100.0, 48000.0, 1.0, 6.0);
let peq2 = vec![(1.0, bq2)];
println!(
" Anti-clip: {:.2} dB, K-weighted: {:.2} dB, A-weighted: {:.2} dB",
peq_preamp_gain(&peq2),
peq_loudness_gain(&peq2, "k"),
peq_loudness_gain(&peq2, "a")
);
println!("3. +6 dB treble at 8 kHz:");
let bq3 = Biquad::new(BiquadFilterType::Peak, 8000.0, 48000.0, 1.0, 6.0);
let peq3 = vec![(1.0, bq3)];
println!(
" Anti-clip: {:.2} dB, K-weighted: {:.2} dB, A-weighted: {:.2} dB",
peq_preamp_gain(&peq3),
peq_loudness_gain(&peq3, "k"),
peq_loudness_gain(&peq3, "a")
);
println!("4. V-shape: bass+4dB, mid-3dB, treble+3dB:");
let bass = Biquad::new(BiquadFilterType::Lowshelf, 150.0, 48000.0, 0.7, 4.0);
let mid = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, -3.0);
let treble = Biquad::new(BiquadFilterType::Highshelf, 8000.0, 48000.0, 0.7, 3.0);
let peq4 = vec![(1.0, bass), (1.0, mid), (1.0, treble)];
println!(
" Anti-clip: {:.2} dB, K-weighted: {:.2} dB, A-weighted: {:.2} dB",
peq_preamp_gain(&peq4),
peq_loudness_gain(&peq4, "k"),
peq_loudness_gain(&peq4, "a")
);
println!("\nNote: Anti-clip prevents clipping, K/A-weighted maintains loudness balance");
}
#[test]
fn test_peq_loudness_gain_cut() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, -6.0);
let peq = vec![(1.0, bq)];
let gain_k = peq_loudness_gain(&peq, "k");
let gain_a = peq_loudness_gain(&peq, "a");
assert!(
gain_k > 0.0,
"Gain compensation for cut should be positive (K-weighting)"
);
assert!(
gain_a > 0.0,
"Gain compensation for cut should be positive (A-weighting)"
);
}
#[test]
fn test_peq_loudness_gain_bass_boost() {
let bq = Biquad::new(BiquadFilterType::Peak, 100.0, 48000.0, 1.0, 6.0);
let peq = vec![(1.0, bq)];
let gain_k = peq_loudness_gain(&peq, "k");
let gain_a = peq_loudness_gain(&peq, "a");
assert!(gain_k < 0.0);
assert!(gain_a < 0.0);
assert!(
gain_a > gain_k,
"A-weighted gain should be less negative (bass is less perceptually important)"
);
}
}
pub fn peq_equal(left: &Peq, right: &Peq) -> bool {
if left.len() != right.len() {
return false;
}
left.iter().zip(right.iter()).all(|((w1, b1), (w2, b2))| {
(w1 - w2).abs() < f64::EPSILON &&
b1.filter_type == b2.filter_type &&
(b1.freq - b2.freq).abs() < f64::EPSILON &&
(b1.srate - b2.srate).abs() < f64::EPSILON &&
(b1.q - b2.q).abs() < f64::EPSILON &&
(b1.db_gain - b2.db_gain).abs() < f64::EPSILON
})
}
pub fn peq_spl(freq: &Array1<f64>, peq: &Peq) -> Array1<f64> {
let mut current_filter = Array1::zeros(freq.len());
for (weight, iir) in peq {
current_filter += &(iir.np_log_result(freq) * *weight);
}
current_filter
}
fn a_weighting_db(f: f64) -> f64 {
let f2 = f * f;
let f4 = f2 * f2;
let numerator = 12194.0_f64.powi(2) * f4;
let denominator = (f2 + 20.6_f64.powi(2))
* ((f2 + 107.7_f64.powi(2)) * (f2 + 737.9_f64.powi(2))).sqrt()
* (f2 + 12194.0_f64.powi(2));
let ra = numerator / denominator;
20.0 * ra.log10() + 2.0 }
fn k_weighting_db(f: f64) -> f64 {
let f_hp = 38.0;
let hp_response = if f > 1.0 {
20.0 * 4.0 * (f / f_hp).log10() } else {
-200.0
};
let hp_gain = hp_response.min(0.0);
let f_hs = 1500.0;
let hs_gain = if f > f_hs {
4.0 * (1.0 - (f_hs / f).powf(2.0).min(1.0))
} else {
0.0
};
hp_gain + hs_gain
}
pub fn peq_loudness_gain(peq: &Peq, weighting: &str) -> f64 {
if peq.is_empty() {
return 0.0;
}
let n_points = 500;
let freq = Array1::logspace(
10.0,
(2.0f64 * 10.0).log10(),
(2.0f64 * 10000.0).log10(),
n_points,
);
let peq_response_db = peq_spl(&freq, peq);
let weighted_change: f64 = freq
.iter()
.zip(peq_response_db.iter())
.map(|(f, peq_db)| {
let weight_db = match weighting {
"a" => a_weighting_db(*f),
"k" => k_weighting_db(*f),
_ => 0.0, };
let weight_linear = 10.0_f64.powf(weight_db / 20.0);
let peq_ratio = 10.0_f64.powf(*peq_db / 20.0);
weight_linear * weight_linear * (peq_ratio * peq_ratio - 1.0)
})
.sum();
let avg_energy_change = weighted_change / n_points as f64;
let loudness_change_db = 10.0 * (1.0 + avg_energy_change).log10();
-loudness_change_db
}
pub fn peq_preamp_gain(peq: &Peq) -> f64 {
let freq = Array1::logspace(
10.0,
(2.0f64 * 10.0).log10(),
(2.0f64 * 10000.0).log10(),
200,
);
let spl = peq_spl(&freq, peq);
let overall = spl
.iter()
.cloned()
.fold(0.0f64, |acc, x| acc.max(x.max(0.0)));
-overall
}
pub fn peq_preamp_gain_max(peq: &Peq) -> f64 {
if peq.is_empty() {
return 0.0;
}
let freq = Array1::logspace(
10.0,
(2.0f64 * 10.0).log10(),
(2.0f64 * 10000.0).log10(),
200,
);
let spl = peq_spl(&freq, peq);
let mut individual: f64 = 0.0;
for (_, iir) in peq {
let single_peq = vec![(1.0, iir.clone())];
let single_spl = peq_spl(&freq, &single_peq);
let single_max = single_spl.iter().cloned().fold(0.0f64, |acc, x| acc.max(x));
individual = individual.max(single_max);
}
let overall = spl
.iter()
.cloned()
.fold(0.0f64, |acc, x| acc.max(x.max(0.0)));
-(individual.max(overall) + 0.2)
}
pub fn peq_format_apo(comment: &str, peq: &Peq) -> String {
let mut res = Vec::new();
res.push(comment.to_string());
res.push(format!("Preamp: {:.1} dB", peq_preamp_gain(peq)));
res.push(String::new());
let mut sorted_peq: Vec<(f64, &Biquad)> = peq.iter().map(|(_, iir)| (iir.freq, iir)).collect();
sorted_peq.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for (i, (_, iir)) in sorted_peq.iter().enumerate() {
match iir.filter_type {
BiquadFilterType::Peak | BiquadFilterType::Notch | BiquadFilterType::Bandpass => {
res.push(format!(
"Filter {:2}: ON {:2} Fc {:5} Hz Gain {:+0.2} dB Q {:0.2}",
i + 1,
iir.filter_type.short_name(),
iir.freq as i32,
iir.db_gain,
iir.q
));
}
BiquadFilterType::Lowpass | BiquadFilterType::Highpass => {
if (iir.q - DEFAULT_Q_HIGH_LOW_PASS).abs() < f64::EPSILON {
res.push(format!(
"Filter {:2}: ON {:2} Fc {:5} Hz",
i + 1,
iir.filter_type.short_name(),
iir.freq as i32
));
} else {
res.push(format!(
"Filter {:2}: ON {:2}Q Fc {:5} Hz Q {:0.2}",
i + 1,
iir.filter_type.short_name(),
iir.freq as i32,
iir.q
));
}
}
BiquadFilterType::Lowshelf | BiquadFilterType::Highshelf => {
res.push(format!(
"Filter {:2}: ON {:2} Fc {:5} Hz Gain {:+0.2} dB Q {:.2}",
i + 1,
iir.filter_type.short_name(),
iir.freq as i32,
iir.db_gain,
iir.q
));
}
BiquadFilterType::HighpassVariableQ => {
res.push(format!(
"Filter {:2}: ON HPQ Fc {:5} Hz Q {:0.2}",
i + 1,
iir.freq as i32,
iir.q
));
}
}
}
res.push(String::new());
res.join("\n")
}
pub fn peq_butterworth_q(order: usize) -> Vec<f64> {
let odd = !order.is_multiple_of(2);
let mut q_values = Vec::new();
for i in 0..order / 2 {
let q = 2.0 * (PI / order as f64 * (i as f64 + 0.5)).sin();
q_values.push(1.0 / q);
}
if odd {
q_values.push(-1.0);
}
q_values
}
pub fn peq_butterworth_lowpass(order: usize, freq: f64, srate: f64) -> Peq {
let q_values = peq_butterworth_q(order);
q_values
.into_iter()
.map(|q| {
(
1.0,
Biquad::new(BiquadFilterType::Lowpass, freq, srate, q, 0.0),
)
})
.collect()
}
pub fn peq_butterworth_highpass(order: usize, freq: f64, srate: f64) -> Peq {
let q_values = peq_butterworth_q(order);
q_values
.into_iter()
.map(|q| {
(
1.0,
Biquad::new(BiquadFilterType::Highpass, freq, srate, q, 0.0),
)
})
.collect()
}
pub fn peq_linkwitzriley_q(order: usize) -> Vec<f64> {
let q_bw = peq_butterworth_q(order / 2);
let mut q_values = Vec::new();
if !order.is_multiple_of(4) {
q_values.extend_from_slice(&q_bw[..q_bw.len() - 1]);
q_values.extend_from_slice(&q_bw[..q_bw.len() - 1]);
q_values.push(0.5);
} else {
q_values.extend_from_slice(&q_bw);
q_values.extend_from_slice(&q_bw);
}
q_values
}
pub fn peq_linkwitzriley_lowpass(order: usize, freq: f64, srate: f64) -> Peq {
let q_values = peq_linkwitzriley_q(order);
q_values
.into_iter()
.map(|q| {
(
1.0,
Biquad::new(BiquadFilterType::Lowpass, freq, srate, q, 0.0),
)
})
.collect()
}
pub fn peq_linkwitzriley_highpass(order: usize, freq: f64, srate: f64) -> Peq {
let q_values = peq_linkwitzriley_q(order);
q_values
.into_iter()
.map(|q| {
(
1.0,
Biquad::new(BiquadFilterType::Highpass, freq, srate, q, 0.0),
)
})
.collect()
}
pub fn peq_print(peq: &Peq) {
let mut rows: Vec<FilterRow> = Vec::new();
for (_weight, filter) in peq {
rows.push(FilterRow {
freq: filter.freq,
q: filter.q,
gain: filter.db_gain,
kind: filter.filter_type.short_name(),
});
}
rows.sort_by(|a, b| {
a.freq
.partial_cmp(&b.freq)
.unwrap_or(std::cmp::Ordering::Equal)
});
println!("+-# -|-Freq (Hz)--|-Q ---------|-Gain (dB)--|-Type-----+");
for (i, r) in rows.iter().enumerate() {
println!(
"| {:<2} | {:<10.2} | {:<10.3} | {:<+10.3} | {:<8} |",
i + 1,
r.freq,
r.q,
r.gain,
r.kind
);
}
println!("+----|------------|------------|------------|----------+");
}
#[cfg(test)]
mod peq_tests {
use super::*;
use ndarray::array;
#[test]
fn test_peq_equal() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq3 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 4.0);
let peq1 = vec![(1.0, bq1.clone()), (0.5, bq2.clone())];
let peq2 = vec![(1.0, bq1), (0.5, bq2)];
let peq3 = vec![(1.0, bq3)];
assert!(peq_equal(&peq1, &peq2));
assert!(!peq_equal(&peq1, &peq3));
assert!(!peq_equal(&peq1, &vec![]));
}
#[test]
fn test_peq_spl() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 6.0);
let peq = vec![(1.0, bq)];
let freq = array![100.0, 1000.0, 10000.0];
let spl = peq_spl(&freq, &peq);
assert!(spl[1] > 5.0 && spl[1] < 7.0);
assert!(spl[0].abs() < 1.0);
assert!(spl[2].abs() < 1.0);
}
#[test]
fn test_peq_preamp_gain() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 6.0);
let peq = vec![(1.0, bq)];
let gain = peq_preamp_gain(&peq);
assert!(gain < 0.0);
assert!(gain > -7.0 && gain < -5.0);
}
#[test]
fn test_peq_format_apo() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let peq = vec![(1.0, bq)];
let apo_str = peq_format_apo("Test EQ", &peq);
assert!(apo_str.contains("Test EQ"));
assert!(apo_str.contains("Preamp:"));
assert!(apo_str.contains("Filter 1:"));
assert!(apo_str.contains("PK"));
assert!(apo_str.contains("1000 Hz"));
assert!(apo_str.contains("+3.00 dB"));
}
#[test]
fn test_butterworth_q() {
let q_values = peq_butterworth_q(4);
assert_eq!(q_values.len(), 2);
assert!((q_values[0] - 1.3065630).abs() < 1e-6);
assert!((q_values[1] - 0.5411961).abs() < 1e-6);
}
#[test]
fn test_butterworth_filters() {
let lp = peq_butterworth_lowpass(4, 1000.0, 48000.0);
let hp = peq_butterworth_highpass(4, 1000.0, 48000.0);
assert_eq!(lp.len(), 2);
assert_eq!(hp.len(), 2);
for (weight, bq) in &lp {
assert_eq!(*weight, 1.0);
assert_eq!(bq.filter_type, BiquadFilterType::Lowpass);
assert_eq!(bq.freq, 1000.0);
}
for (weight, bq) in &hp {
assert_eq!(*weight, 1.0);
assert_eq!(bq.filter_type, BiquadFilterType::Highpass);
assert_eq!(bq.freq, 1000.0);
}
}
#[test]
fn test_linkwitzriley_filters() {
let lp = peq_linkwitzriley_lowpass(4, 1000.0, 48000.0);
let hp = peq_linkwitzriley_highpass(4, 1000.0, 48000.0);
assert_eq!(lp.len(), 2);
assert_eq!(hp.len(), 2);
for (weight, _) in &lp {
assert_eq!(*weight, 1.0);
}
for (weight, _) in &hp {
assert_eq!(*weight, 1.0);
}
}
}
fn biquad_to_rme_type(filter_type: BiquadFilterType, pos: usize) -> f64 {
match filter_type {
BiquadFilterType::Peak => 0.0,
BiquadFilterType::Lowpass => {
if pos == 1 {
3.0
} else if pos == 3 || pos == 9 {
2.0
} else {
-1.0
}
}
BiquadFilterType::Highpass | BiquadFilterType::HighpassVariableQ => {
if pos == 1 {
2.0
} else if pos == 3 || pos == 9 {
3.0
} else {
-1.0
}
}
BiquadFilterType::Lowshelf | BiquadFilterType::Highshelf => {
if pos == 1 || pos == 3 || pos == 9 {
1.0
} else {
-1.0
}
}
_ => -1.0,
}
}
pub fn peq_format_rme_channel(peq: &Peq) -> String {
#[allow(clippy::vec_init_then_push)]
let mut lines = vec![
"<Preset>".to_string(),
" <Equalizer>".to_string(),
" <Params>".to_string(),
"\t<val e=\"LC Grade\" v=\"1.00,\"/>".to_string(),
"\t<val e=\"LC Freq\" v=\"20.00,\"/>".to_string(),
];
for (i, (_, biquad)) in peq.iter().enumerate() {
lines.push(format!(
" <val e=\"Band{} Freq\" v=\"{:7.2},\"/>",
i + 1,
biquad.freq
));
lines.push(format!(
" <val e=\"Band{} Q\" v=\"{:4.2},\"/>",
i + 1,
biquad.q
));
lines.push(format!(
" <val e=\"Band{} Gain\" v=\"{:4.2},\"/>",
i + 1,
biquad.db_gain
));
}
for (i, (_, biquad)) in peq.iter().enumerate() {
let rme_type = biquad_to_rme_type(biquad.filter_type, i + 1);
if rme_type >= 0.0 {
lines.push(format!(
" <val e=\"Band{} Type\" v=\"{:4.2},\"/>",
i + 1,
rme_type
));
}
}
lines.push(" </Params>".to_string());
lines.push(" </Equalizer>".to_string());
lines.push("</Preset>".to_string());
lines.join("\n")
}
#[allow(dead_code)]
fn get_filter_priority(filter_type: BiquadFilterType) -> u8 {
match filter_type {
BiquadFilterType::Lowshelf | BiquadFilterType::Highshelf => 9,
BiquadFilterType::Lowpass => 7,
BiquadFilterType::Highpass | BiquadFilterType::HighpassVariableQ => 7,
BiquadFilterType::Bandpass => 5,
BiquadFilterType::Peak => 3,
_ => 1,
}
}
#[allow(dead_code)]
fn filter_peqs_by_gain(peqs: &Peq, max_count: usize) -> Peq {
if peqs.len() <= max_count {
return peqs.clone();
}
let mut indexed_peqs: Vec<(usize, &(f64, Biquad), u8, f64)> = peqs
.iter()
.enumerate()
.map(|(i, item)| {
let priority = get_filter_priority(item.1.filter_type);
let abs_gain = item.1.db_gain.abs();
(i, item, priority, abs_gain)
})
.collect();
indexed_peqs.sort_by(|a, b| {
b.2.cmp(&a.2) .then_with(|| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal))
});
let mut selected: Vec<(usize, (f64, Biquad))> = indexed_peqs
.into_iter()
.take(max_count)
.map(|(idx, item, _, _)| (idx, item.clone()))
.collect();
selected.sort_by_key(|(idx, _)| *idx);
selected.into_iter().map(|(_, item)| item).collect()
}
fn enforce_rme_room_filter_constraints(peqs: &Peq) -> Peq {
let mut pk_filters: Vec<(f64, Biquad)> = Vec::new();
let mut non_pk_filters: Vec<(f64, Biquad)> = Vec::new();
for item in peqs {
match item.1.filter_type {
BiquadFilterType::Peak => pk_filters.push(item.clone()),
BiquadFilterType::Lowshelf
| BiquadFilterType::Highshelf
| BiquadFilterType::Lowpass
| BiquadFilterType::Highpass
| BiquadFilterType::HighpassVariableQ => non_pk_filters.push(item.clone()),
_ => {
eprintln!(
"Warning: Filter type {:?} not supported by RME room EQ, converting to PK",
item.1.filter_type
);
let mut converted = item.1.clone();
converted.filter_type = BiquadFilterType::Peak;
pk_filters.push((item.0, converted));
}
}
}
let mut selected_low: Option<(f64, Biquad)> = None;
let mut selected_high: Option<(f64, Biquad)> = None;
if non_pk_filters.len() > 2 {
eprintln!(
"Warning: RME room EQ supports at most 2 non-PK filters (positions 1 and 9). \
Found {} non-PK filters. Selecting lowest and highest frequency filters.",
non_pk_filters.len()
);
}
if !non_pk_filters.is_empty() {
let mut sorted_non_pk = non_pk_filters.clone();
sorted_non_pk.sort_by(|a, b| {
a.1.freq
.partial_cmp(&b.1.freq)
.unwrap_or(std::cmp::Ordering::Equal)
});
selected_low = Some(sorted_non_pk[0].clone());
if sorted_non_pk.len() > 1 {
selected_high = Some(sorted_non_pk[sorted_non_pk.len() - 1].clone());
}
}
let mut result: Vec<(f64, Biquad)> = Vec::new();
if let Some(low) = selected_low {
result.push(low);
} else if !pk_filters.is_empty() {
result.push(pk_filters.remove(0));
} else {
result.push((
1.0,
Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 0.0),
));
}
let mut middle_count = 0;
while middle_count < 7 {
if !pk_filters.is_empty() {
result.push(pk_filters.remove(0));
} else {
result.push((
1.0,
Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 0.0),
));
}
middle_count += 1;
}
if let Some(high) = selected_high {
result.push(high);
} else if !pk_filters.is_empty() {
result.push(pk_filters.remove(0));
} else {
result.push((
1.0,
Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 0.0),
));
}
if !pk_filters.is_empty() {
eprintln!(
"Warning: {} PK filters were dropped due to 9-band limit",
pk_filters.len()
);
}
result
}
pub fn peq_format_rme_room(left: &Peq, right: &Peq) -> String {
let left_constrained = enforce_rme_room_filter_constraints(left);
let right_constrained = if !right.is_empty() {
enforce_rme_room_filter_constraints(right)
} else {
left_constrained.clone()
};
let mut lines = Vec::new();
let process_channel = |peqs: &Peq, lines: &mut Vec<String>| {
for (i, (_, biquad)) in peqs.iter().enumerate() {
lines.push(format!(
" <val e=\"REQ Band{} Freq\" v=\"{:7.2},\"/>",
i + 1,
biquad.freq
));
lines.push(format!(
" <val e=\"REQ Band{} Q\" v=\"{:4.2},\"/>",
i + 1,
biquad.q
));
lines.push(format!(
" <val e=\"REQ Band{} Gain\" v=\"{:4.2},\"/>",
i + 1,
biquad.db_gain
));
}
for (i, (_, biquad)) in peqs.iter().enumerate() {
let rme_type = biquad_to_rme_type(biquad.filter_type, i + 1);
if rme_type >= 0.0 {
lines.push(format!(
" <val e=\"REQ Band{} Type\" v=\"{:4.2},\"/>",
i + 1,
rme_type
));
}
}
};
lines.push("<Preset>".to_string());
let preamp_gain = 0.0;
lines.push(" <Room EQ L>".to_string());
lines.push(" <Params>".to_string());
lines.push("\t<val e=\"REQ Delay\" v=\"0.00,\"/>".to_string());
process_channel(&left_constrained, &mut lines);
lines.push(format!(
"\t<val e=\"REQ Chan Gain\" v=\"{},\"/>",
preamp_gain
));
lines.push(" </Params>".to_string());
lines.push(" </Room EQ L>".to_string());
lines.push(" <Room EQ R>".to_string());
lines.push(" <Params>".to_string());
lines.push("\t<val e=\"REQ Delay\" v=\"0.00,\"/>".to_string());
if !right_constrained.is_empty() {
process_channel(&right_constrained, &mut lines);
} else {
process_channel(&left_constrained, &mut lines);
}
lines.push(format!(
"\t<val e=\"REQ Chan Gain\" v=\"{},\"/>",
preamp_gain
));
lines.push(" </Params>".to_string());
lines.push(" </Room EQ R>".to_string());
lines.push("</Preset>".to_string());
lines.join("\n")
}
const K_AUNBANDEQ_PARAM_BYPASS_BAND: i32 = 1000;
const K_AUNBANDEQ_PARAM_FILTER_TYPE: i32 = 2000;
const K_AUNBANDEQ_PARAM_FREQUENCY: i32 = 3000;
const K_AUNBANDEQ_PARAM_GAIN: i32 = 4000;
const K_AUNBANDEQ_PARAM_BANDWIDTH: i32 = 5000;
const K_AUNBANDEQ_FILTER_TYPE_PARAMETRIC: i32 = 0;
#[allow(dead_code)]
const K_AUNBANDEQ_FILTER_TYPE_2ND_ORDER_BUTTERWORTH_LOW_PASS: i32 = 1;
#[allow(dead_code)]
const K_AUNBANDEQ_FILTER_TYPE_2ND_ORDER_BUTTERWORTH_HIGH_PASS: i32 = 2;
const K_AUNBANDEQ_FILTER_TYPE_RESONANT_LOW_PASS: i32 = 3;
const K_AUNBANDEQ_FILTER_TYPE_RESONANT_HIGH_PASS: i32 = 4;
const K_AUNBANDEQ_FILTER_TYPE_BAND_PASS: i32 = 5;
const K_AUNBANDEQ_FILTER_TYPE_LOW_SHELF: i32 = 7;
const K_AUNBANDEQ_FILTER_TYPE_HIGH_SHELF: i32 = 8;
fn biquad_to_apple_type(filter_type: BiquadFilterType) -> i32 {
match filter_type {
BiquadFilterType::Peak => K_AUNBANDEQ_FILTER_TYPE_PARAMETRIC,
BiquadFilterType::Highshelf => K_AUNBANDEQ_FILTER_TYPE_HIGH_SHELF,
BiquadFilterType::Lowshelf => K_AUNBANDEQ_FILTER_TYPE_LOW_SHELF,
BiquadFilterType::Highpass | BiquadFilterType::HighpassVariableQ => {
K_AUNBANDEQ_FILTER_TYPE_RESONANT_HIGH_PASS
}
BiquadFilterType::Lowpass => K_AUNBANDEQ_FILTER_TYPE_RESONANT_LOW_PASS,
BiquadFilterType::Bandpass => K_AUNBANDEQ_FILTER_TYPE_BAND_PASS,
_ => -1,
}
}
pub fn peq_format_aupreset(peq: &Peq, name: &str) -> String {
let len_peq = peq.len().min(16); let preamp_gain = peq_preamp_gain(peq);
let mut buffer = Vec::new();
buffer.write_i32::<BigEndian>(0).unwrap();
buffer.write_i32::<BigEndian>(0).unwrap();
buffer.write_i32::<BigEndian>(81).unwrap(); buffer.write_i32::<BigEndian>(0).unwrap();
buffer.write_f32::<BigEndian>(preamp_gain as f32).unwrap();
let mut params = std::collections::BTreeMap::new();
for (i, (_, biquad)) in peq.iter().take(16).enumerate() {
let idx = i as i32;
params.insert(K_AUNBANDEQ_PARAM_BYPASS_BAND + idx, 0.0f32); params.insert(
K_AUNBANDEQ_PARAM_FILTER_TYPE + idx,
biquad_to_apple_type(biquad.filter_type) as f32,
);
params.insert(K_AUNBANDEQ_PARAM_FREQUENCY + idx, biquad.freq as f32);
params.insert(K_AUNBANDEQ_PARAM_GAIN + idx, biquad.db_gain as f32);
params.insert(K_AUNBANDEQ_PARAM_BANDWIDTH + idx, q2bw(biquad.q) as f32);
}
for i in len_peq..16 {
let idx = i as i32;
params.insert(K_AUNBANDEQ_PARAM_BYPASS_BAND + idx, 1.0f32); params.insert(K_AUNBANDEQ_PARAM_FILTER_TYPE + idx, 0.0f32);
params.insert(K_AUNBANDEQ_PARAM_FREQUENCY + idx, 0.0f32);
params.insert(K_AUNBANDEQ_PARAM_GAIN + idx, 0.0f32);
params.insert(K_AUNBANDEQ_PARAM_BANDWIDTH + idx, 0.0f32);
}
for (param_id, value) in params.iter() {
buffer.write_i32::<BigEndian>(*param_id).unwrap();
buffer.write_f32::<BigEndian>(*value).unwrap();
}
let b64_text = general_purpose::STANDARD.encode(&buffer);
let chunk_size = 68;
let mut data_lines = Vec::new();
for chunk in b64_text.as_bytes().chunks(chunk_size) {
data_lines.push(format!("\t{}", String::from_utf8_lossy(chunk)));
}
let data_section = data_lines.join("\n");
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ParametricType</key>
<integer>11</integer>
<key>data</key>
<data>
{}
</data>
<key>manufacturer</key>
<integer>1634758764</integer>
<key>name</key>
<string>{}</string>
<key>numberOfBands</key>
<integer>{}</integer>
<key>subtype</key>
<integer>1851942257</integer>
<key>type</key>
<integer>1635083896</integer>
<key>version</key>
<integer>0</integer>
</dict>
</plist>
"#,
data_section, name, len_peq
)
}
#[cfg(test)]
mod format_tests {
use super::*;
#[test]
fn test_peq_format_rme_channel_single_peak() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let peq = vec![(1.0, bq)];
let rme_str = peq_format_rme_channel(&peq);
assert!(rme_str.contains("<Preset>"));
assert!(rme_str.contains("<Equalizer>"));
assert!(rme_str.contains("<Params>"));
assert!(rme_str.contains("LC Grade"));
assert!(rme_str.contains("LC Freq"));
assert!(rme_str.contains("Band1 Freq"));
assert!(rme_str.contains("Band1 Q"));
assert!(rme_str.contains("Band1 Gain"));
assert!(rme_str.contains("Band1 Type"));
assert!(rme_str.contains("</Preset>"));
assert!(rme_str.contains("0.00"));
}
#[test]
fn test_peq_format_rme_channel_empty() {
let peq: Peq = vec![];
let rme_str = peq_format_rme_channel(&peq);
assert!(rme_str.contains("<Preset>"));
assert!(rme_str.contains("<Equalizer>"));
assert!(rme_str.contains("LC Grade"));
assert!(rme_str.contains("</Preset>"));
}
#[test]
fn test_peq_format_rme_channel_multiple_bands() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 2.0, -2.0);
let peq = vec![(1.0, bq1), (1.0, bq2)];
let rme_str = peq_format_rme_channel(&peq);
assert!(rme_str.contains("Band1 Freq"));
assert!(rme_str.contains("Band2 Freq"));
assert!(rme_str.contains("Band1 Type"));
assert!(rme_str.contains("Band2 Type"));
}
#[test]
fn test_peq_format_aupreset_single_peak() {
let bq = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let peq = vec![(1.0, bq)];
let aupreset_str = peq_format_aupreset(&peq, "Test EQ");
assert!(aupreset_str.contains("<?xml version="));
assert!(aupreset_str.contains("<!DOCTYPE plist"));
assert!(aupreset_str.contains("<plist version=\"1.0\">"));
assert!(aupreset_str.contains("<dict>"));
assert!(aupreset_str.contains("<key>ParametricType</key>"));
assert!(aupreset_str.contains("<key>data</key>"));
assert!(aupreset_str.contains("<data>"));
assert!(aupreset_str.contains("<key>name</key>"));
assert!(aupreset_str.contains("<string>Test EQ</string>"));
assert!(aupreset_str.contains("<key>numberOfBands</key>"));
assert!(aupreset_str.contains("<integer>1</integer>"));
assert!(aupreset_str.contains("</plist>"));
}
#[test]
fn test_peq_format_aupreset_empty() {
let peq: Peq = vec![];
let aupreset_str = peq_format_aupreset(&peq, "Empty EQ");
assert!(aupreset_str.contains("<?xml version="));
assert!(aupreset_str.contains("<string>Empty EQ</string>"));
assert!(aupreset_str.contains("<integer>0</integer>"));
}
#[test]
fn test_peq_format_aupreset_multiple_bands() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Highshelf, 8000.0, 48000.0, 0.7, 2.0);
let bq3 = Biquad::new(BiquadFilterType::Lowshelf, 100.0, 48000.0, 0.7, -1.0);
let peq = vec![(1.0, bq1), (1.0, bq2), (1.0, bq3)];
let aupreset_str = peq_format_aupreset(&peq, "Multi Band EQ");
assert!(aupreset_str.contains("<string>Multi Band EQ</string>"));
assert!(aupreset_str.contains("<integer>3</integer>"));
assert!(aupreset_str.contains("<data>"));
}
#[test]
fn test_peq_format_aupreset_max_bands() {
let mut peq = Vec::new();
for i in 0..20 {
let freq = 100.0 + (i as f64 * 100.0);
let bq = Biquad::new(BiquadFilterType::Peak, freq, 48000.0, 1.0, 1.0);
peq.push((1.0, bq));
}
let aupreset_str = peq_format_aupreset(&peq, "Max Bands EQ");
assert!(aupreset_str.contains("<integer>16</integer>"));
}
#[test]
fn test_biquad_to_apple_type() {
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Peak),
K_AUNBANDEQ_FILTER_TYPE_PARAMETRIC
);
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Highshelf),
K_AUNBANDEQ_FILTER_TYPE_HIGH_SHELF
);
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Lowshelf),
K_AUNBANDEQ_FILTER_TYPE_LOW_SHELF
);
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Highpass),
K_AUNBANDEQ_FILTER_TYPE_RESONANT_HIGH_PASS
);
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Lowpass),
K_AUNBANDEQ_FILTER_TYPE_RESONANT_LOW_PASS
);
assert_eq!(
biquad_to_apple_type(BiquadFilterType::Bandpass),
K_AUNBANDEQ_FILTER_TYPE_BAND_PASS
);
}
#[test]
fn test_biquad_to_rme_type() {
assert_eq!(biquad_to_rme_type(BiquadFilterType::Peak, 1), 0.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Peak, 2), 0.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Peak, 3), 0.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowpass, 1), 3.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowpass, 3), 2.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowpass, 9), 2.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowpass, 2), -1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highpass, 1), 2.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highpass, 3), 3.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highpass, 9), 3.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highpass, 2), -1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowshelf, 1), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowshelf, 3), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowshelf, 9), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Lowshelf, 2), -1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highshelf, 1), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highshelf, 3), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highshelf, 9), 1.0);
assert_eq!(biquad_to_rme_type(BiquadFilterType::Highshelf, 2), -1.0);
}
#[test]
fn test_get_filter_priority() {
assert_eq!(get_filter_priority(BiquadFilterType::Lowshelf), 9);
assert_eq!(get_filter_priority(BiquadFilterType::Highshelf), 9);
assert_eq!(get_filter_priority(BiquadFilterType::Lowpass), 7);
assert_eq!(get_filter_priority(BiquadFilterType::Highpass), 7);
assert_eq!(get_filter_priority(BiquadFilterType::HighpassVariableQ), 7);
assert_eq!(get_filter_priority(BiquadFilterType::Bandpass), 5);
assert_eq!(get_filter_priority(BiquadFilterType::Peak), 3);
assert_eq!(get_filter_priority(BiquadFilterType::Notch), 1);
}
#[test]
fn test_filter_peqs_by_gain_under_limit() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 1.0, 2.0);
let peq = vec![(1.0, bq1), (1.0, bq2)];
let filtered = filter_peqs_by_gain(&peq, 5);
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_filter_peqs_by_gain_over_limit() {
let mut peq = Vec::new();
for i in 0..12 {
let gain = (i as f64) * 0.5 + 0.5; let bq = Biquad::new(
BiquadFilterType::Peak,
1000.0 + (i as f64 * 100.0),
48000.0,
1.0,
gain,
);
peq.push((1.0, bq));
}
let filtered = filter_peqs_by_gain(&peq, 9);
assert_eq!(filtered.len(), 9);
assert!((filtered[0].1.freq - 1300.0).abs() < 1.0);
}
#[test]
fn test_filter_peqs_by_gain_priority() {
let mut peq = Vec::new();
for i in 0..6 {
let bq = Biquad::new(
BiquadFilterType::Peak,
1000.0 + (i as f64 * 100.0),
48000.0,
1.0,
5.0,
);
peq.push((1.0, bq));
}
let ls1 = Biquad::new(BiquadFilterType::Lowshelf, 100.0, 48000.0, 0.7, 2.0);
let ls2 = Biquad::new(BiquadFilterType::Lowshelf, 120.0, 48000.0, 0.7, 2.5);
peq.push((1.0, ls1));
peq.push((1.0, ls2));
for i in 6..9 {
let bq = Biquad::new(
BiquadFilterType::Peak,
1000.0 + (i as f64 * 100.0),
48000.0,
1.0,
4.0,
);
peq.push((1.0, bq));
}
let filtered = filter_peqs_by_gain(&peq, 9);
assert_eq!(filtered.len(), 9);
let lowshelf_count = filtered
.iter()
.filter(|(_, bq)| bq.filter_type == BiquadFilterType::Lowshelf)
.count();
assert_eq!(lowshelf_count, 2);
}
#[test]
fn test_enforce_rme_room_filter_constraints_empty() {
let peq: Peq = vec![];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
for (_, bq) in &result {
assert_eq!(bq.filter_type, BiquadFilterType::Peak);
assert_eq!(bq.db_gain, 0.0);
}
}
#[test]
fn test_enforce_rme_room_filter_constraints_no_shelves() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 1.0, 2.0);
let peq = vec![(1.0, bq1), (1.0, bq2)];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
assert!((result[0].1.freq - 1000.0).abs() < 1.0);
assert!((result[1].1.freq - 2000.0).abs() < 1.0);
for i in 2..9 {
assert_eq!(result[i].1.filter_type, BiquadFilterType::Peak);
assert_eq!(result[i].1.db_gain, 0.0);
}
}
#[test]
fn test_enforce_rme_room_filter_constraints_single_lowshelf() {
let ls = Biquad::new(BiquadFilterType::Lowshelf, 100.0, 48000.0, 0.7, 2.0);
let pk1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let pk2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 1.0, 2.0);
let peq = vec![(1.0, pk1), (1.0, ls.clone()), (1.0, pk2)];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
assert_eq!(result[0].1.filter_type, BiquadFilterType::Lowshelf);
assert!((result[0].1.freq - 100.0).abs() < 1.0);
for i in 1..9 {
assert_eq!(result[i].1.filter_type, BiquadFilterType::Peak);
}
}
#[test]
fn test_enforce_rme_room_filter_constraints_multiple_lowshelf() {
let ls1 = Biquad::new(BiquadFilterType::Lowshelf, 100.0, 48000.0, 0.7, 2.0);
let ls2 = Biquad::new(BiquadFilterType::Lowshelf, 120.0, 48000.0, 0.7, 4.0);
let pk = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let peq = vec![(1.0, ls1), (1.0, pk), (1.0, ls2)];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
assert_eq!(result[0].1.filter_type, BiquadFilterType::Lowshelf);
assert!((result[0].1.freq - 100.0).abs() < 1.0);
assert_eq!(result[8].1.filter_type, BiquadFilterType::Lowshelf);
assert!((result[8].1.freq - 120.0).abs() < 1.0);
}
#[test]
fn test_enforce_rme_room_filter_constraints_multiple_highshelf() {
let hs1 = Biquad::new(BiquadFilterType::Highshelf, 8000.0, 48000.0, 0.7, 1.5);
let hs2 = Biquad::new(BiquadFilterType::Highshelf, 10000.0, 48000.0, 0.7, 3.0);
let pk = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let peq = vec![(1.0, pk), (1.0, hs1), (1.0, hs2)];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
assert_eq!(result[0].1.filter_type, BiquadFilterType::Highshelf);
assert!((result[0].1.freq - 8000.0).abs() < 1.0);
assert_eq!(result[8].1.filter_type, BiquadFilterType::Highshelf);
assert!((result[8].1.freq - 10000.0).abs() < 1.0);
}
#[test]
fn test_enforce_rme_room_filter_constraints_both_shelves() {
let ls = Biquad::new(BiquadFilterType::Lowshelf, 100.0, 48000.0, 0.7, 2.0);
let hs = Biquad::new(BiquadFilterType::Highshelf, 8000.0, 48000.0, 0.7, 1.5);
let pk1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let pk2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 1.0, 2.0);
let peq = vec![(1.0, pk1), (1.0, ls), (1.0, pk2), (1.0, hs)];
let result = enforce_rme_room_filter_constraints(&peq);
assert_eq!(result.len(), 9);
assert_eq!(result[0].1.filter_type, BiquadFilterType::Lowshelf);
assert!((result[0].1.freq - 100.0).abs() < 1.0);
assert_eq!(result[8].1.filter_type, BiquadFilterType::Highshelf);
assert!((result[8].1.freq - 8000.0).abs() < 1.0);
for i in 1..8 {
assert_eq!(result[i].1.filter_type, BiquadFilterType::Peak);
}
}
#[test]
fn test_peq_format_rme_room_single_channel() {
let bq1 = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq2 = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 2.0, -2.0);
let left = vec![(1.0, bq1), (1.0, bq2)];
let right: Peq = vec![];
let rme_str = peq_format_rme_room(&left, &right);
assert!(rme_str.contains("<Preset>"));
assert!(rme_str.contains("<Room EQ L>"));
assert!(rme_str.contains("<Room EQ R>"));
assert!(rme_str.contains("<Params>"));
assert!(rme_str.contains("REQ Delay"));
assert!(rme_str.contains("REQ Band1 Freq"));
assert!(rme_str.contains("REQ Band1 Q"));
assert!(rme_str.contains("REQ Band1 Gain"));
assert!(rme_str.contains("REQ Band1 Type"));
assert!(rme_str.contains("REQ Band2 Freq"));
assert!(rme_str.contains("REQ Chan Gain"));
assert!(rme_str.contains("</Preset>"));
}
#[test]
fn test_peq_format_rme_room_dual_channel() {
let bq_left = Biquad::new(BiquadFilterType::Peak, 1000.0, 48000.0, 1.0, 3.0);
let bq_right = Biquad::new(BiquadFilterType::Peak, 2000.0, 48000.0, 2.0, -2.0);
let left = vec![(1.0, bq_left)];
let right = vec![(1.0, bq_right)];
let rme_str = peq_format_rme_room(&left, &right);
assert!(rme_str.contains("1000.00"));
assert!(rme_str.contains("2000.00"));
}
#[test]
fn test_peq_format_rme_room_max_bands() {
let mut left = Vec::new();
for i in 0..9 {
let freq = 100.0 + (i as f64 * 100.0);
let bq = Biquad::new(BiquadFilterType::Peak, freq, 48000.0, 1.0, 1.0);
left.push((1.0, bq));
}
let right: Peq = vec![];
let rme_str = peq_format_rme_room(&left, &right);
assert!(rme_str.contains("REQ Band1 Freq"));
assert!(rme_str.contains("REQ Band9 Freq"));
}
#[test]
fn test_peq_format_rme_room_over_limit() {
let mut left = Vec::new();
for i in 0..12 {
let freq = 100.0 + (i as f64 * 100.0);
let gain = (i as f64) * 0.5 + 0.5;
let bq = Biquad::new(BiquadFilterType::Peak, freq, 48000.0, 1.0, gain);
left.push((1.0, bq));
}
let right: Peq = vec![];
let rme_str = peq_format_rme_room(&left, &right);
assert!(rme_str.contains("REQ Band9 Freq"));
assert!(!rme_str.contains("REQ Band10 Freq"));
}
}