use serde::{Deserialize, Serialize};
use crate::buffer::AudioBuffer;
use crate::dsp::eq::{BandType, EqBandConfig, ParametricEq};
pub const ISO_BANDS: [f32; 10] = [
31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
];
const BAND_NAMES: [&str; 10] = [
"31 Hz", "62 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz",
];
const GRAPHIC_EQ_Q: f32 = 1.4;
const MAX_GAIN_DB: f32 = 12.0;
#[must_use]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(default)]
pub struct GraphicEqSettings {
pub bands: [f32; 10],
pub enabled: bool,
}
impl Default for GraphicEqSettings {
fn default() -> Self {
Self {
bands: [0.0; 10],
enabled: false,
}
}
}
impl GraphicEqSettings {
pub fn flat() -> Self {
Self::default()
}
pub fn is_flat(&self) -> bool {
!self.enabled || self.bands.iter().all(|b| b.abs() < 0.01)
}
pub fn set_band(&mut self, band: usize, gain_db: f32) {
if band < 10 {
self.bands[band] = gain_db.clamp(-MAX_GAIN_DB, MAX_GAIN_DB);
}
}
pub fn preset(name: &str) -> Self {
let bands = match name {
"rock" => [4.0, 3.0, 1.0, -1.0, -2.0, 0.0, 2.0, 3.0, 4.0, 4.0],
"pop" => [-1.0, 1.0, 3.0, 4.0, 3.0, 0.0, -1.0, 0.0, 1.0, 2.0],
"jazz" => [2.0, 1.0, 0.0, 1.0, -1.0, -1.0, 0.0, 1.0, 2.0, 3.0],
"classical" => [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, -3.0, -2.0, 0.0],
"bass" => [6.0, 5.0, 4.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"treble" => [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 4.0, 5.0, 6.0],
"vocal" => [-2.0, -1.0, 0.0, 2.0, 4.0, 4.0, 3.0, 1.0, 0.0, -1.0],
"electronic" => [5.0, 4.0, 1.0, 0.0, -2.0, 0.0, 1.0, 3.0, 4.0, 5.0],
"acoustic" => [2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 1.0],
_ => [0.0; 10],
};
Self {
bands,
enabled: true,
}
}
pub fn preset_names() -> &'static [&'static str] {
&[
"flat",
"rock",
"pop",
"jazz",
"classical",
"bass",
"treble",
"vocal",
"electronic",
"acoustic",
]
}
pub fn band_name(band: usize) -> &'static str {
BAND_NAMES.get(band).copied().unwrap_or("?")
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct GraphicEq {
inner: ParametricEq,
settings: GraphicEqSettings,
sample_rate: u32,
channels: u32,
}
impl GraphicEq {
pub fn new(sample_rate: u32, channels: u32) -> Self {
tracing::debug!(sample_rate, channels, "GraphicEq::new");
let settings = GraphicEqSettings::default();
let inner = Self::build_eq(&settings, sample_rate, channels);
Self {
inner,
settings,
sample_rate,
channels,
}
}
#[inline]
pub fn process(&mut self, buf: &mut AudioBuffer) {
if self.settings.is_flat() {
return;
}
if buf.sample_rate != self.sample_rate {
self.sample_rate = buf.sample_rate;
self.rebuild();
}
self.inner.process(buf);
}
pub fn load_preset(&mut self, name: &str) {
self.settings = GraphicEqSettings::preset(name);
self.rebuild();
}
pub fn set_band(&mut self, band: usize, gain_db: f32) {
self.settings.set_band(band, gain_db);
self.rebuild();
}
pub fn set_settings(&mut self, settings: GraphicEqSettings) {
self.settings = settings;
self.rebuild();
}
pub fn settings(&self) -> &GraphicEqSettings {
&self.settings
}
pub fn set_enabled(&mut self, enabled: bool) {
self.settings.enabled = enabled;
}
pub fn reset(&mut self) {
self.inner.reset();
}
fn rebuild(&mut self) {
self.inner = Self::build_eq(&self.settings, self.sample_rate, self.channels);
}
fn build_eq(settings: &GraphicEqSettings, sample_rate: u32, channels: u32) -> ParametricEq {
let bands: Vec<EqBandConfig> = ISO_BANDS
.iter()
.zip(settings.bands.iter())
.map(|(&freq, &gain_db)| EqBandConfig {
band_type: BandType::Peaking,
freq_hz: freq,
gain_db,
q: GRAPHIC_EQ_Q,
enabled: gain_db.abs() >= 0.01,
})
.collect();
ParametricEq::new(bands, sample_rate, channels)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_is_passthrough() {
let mut eq = GraphicEq::new(44100, 2);
eq.set_enabled(true);
let samples: Vec<f32> = (0..4096)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / 44100.0).sin())
.collect();
let mut buf = AudioBuffer::from_interleaved(samples.clone(), 1, 44100).unwrap();
eq.process(&mut buf);
assert_eq!(buf.samples, samples);
}
#[test]
fn boost_increases_energy() {
let mut eq = GraphicEq::new(44100, 1);
eq.set_enabled(true);
eq.set_band(5, 12.0);
let samples: Vec<f32> = (0..4096)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let original_rms = {
let sum: f64 = samples.iter().map(|s| (*s as f64).powi(2)).sum();
(sum / samples.len() as f64).sqrt()
};
let mut buf = AudioBuffer::from_interleaved(samples, 1, 44100).unwrap();
eq.process(&mut buf);
let boosted_rms = buf.rms() as f64;
assert!(boosted_rms > original_rms, "boost should increase energy");
}
#[test]
fn cut_decreases_energy() {
let mut eq = GraphicEq::new(44100, 1);
eq.set_enabled(true);
eq.set_band(5, -12.0);
let samples: Vec<f32> = (0..4096)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let original_rms = {
let sum: f64 = samples.iter().map(|s| (*s as f64).powi(2)).sum();
(sum / samples.len() as f64).sqrt()
};
let mut buf = AudioBuffer::from_interleaved(samples, 1, 44100).unwrap();
eq.process(&mut buf);
let cut_rms = buf.rms() as f64;
assert!(cut_rms < original_rms, "cut should decrease energy");
}
#[test]
fn preset_loading() {
let mut eq = GraphicEq::new(48000, 2);
eq.load_preset("rock");
assert!(eq.settings().enabled);
assert!(!eq.settings().is_flat());
assert!(eq.settings().bands[0] > 0.0); }
#[test]
fn all_presets_valid_range() {
for name in GraphicEqSettings::preset_names() {
let settings = GraphicEqSettings::preset(name);
for &b in &settings.bands {
assert!(
(-MAX_GAIN_DB..=MAX_GAIN_DB).contains(&b),
"preset '{name}' band out of range: {b}"
);
}
}
}
#[test]
fn unknown_preset_is_flat() {
let settings = GraphicEqSettings::preset("nonexistent");
assert!(settings.bands.iter().all(|b| *b == 0.0));
}
#[test]
fn band_names() {
assert_eq!(GraphicEqSettings::band_name(0), "31 Hz");
assert_eq!(GraphicEqSettings::band_name(5), "1 kHz");
assert_eq!(GraphicEqSettings::band_name(9), "16 kHz");
assert_eq!(GraphicEqSettings::band_name(10), "?");
}
#[test]
fn set_band_clamps() {
let mut s = GraphicEqSettings::default();
s.set_band(0, 20.0);
assert_eq!(s.bands[0], 12.0);
s.set_band(0, -20.0);
assert_eq!(s.bands[0], -12.0);
}
#[test]
fn set_band_out_of_range_ignored() {
let mut s = GraphicEqSettings::default();
s.set_band(99, 6.0); assert!(s.is_flat());
}
#[test]
fn disabled_is_passthrough() {
let mut eq = GraphicEq::new(44100, 1);
eq.load_preset("rock"); eq.set_enabled(false);
let samples: Vec<f32> = (0..1024)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
.collect();
let mut buf = AudioBuffer::from_interleaved(samples.clone(), 1, 44100).unwrap();
eq.process(&mut buf);
assert_eq!(buf.samples, samples);
}
#[test]
fn stereo_processing() {
let mut eq = GraphicEq::new(48000, 2);
eq.set_enabled(true);
eq.set_band(5, 6.0);
let samples: Vec<f32> = (0..4096)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * (i / 2) as f32 / 48000.0).sin() * 0.5)
.collect();
let mut buf = AudioBuffer::from_interleaved(samples, 2, 48000).unwrap();
eq.process(&mut buf);
assert_eq!(buf.channels, 2);
assert!(buf.samples.iter().all(|s| s.is_finite()));
}
#[test]
fn serde_roundtrip() {
let settings = GraphicEqSettings::preset("jazz");
let json = serde_json::to_string(&settings).unwrap();
let back: GraphicEqSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.bands, settings.bands);
assert_eq!(back.enabled, settings.enabled);
}
#[test]
fn reset_clears_state() {
let mut eq = GraphicEq::new(44100, 1);
eq.set_enabled(true);
eq.set_band(5, 6.0);
let mut buf = AudioBuffer::from_interleaved(vec![0.5; 1024], 1, 44100).unwrap();
eq.process(&mut buf);
eq.reset();
}
}