Skip to main content

player/
eq.rs

1use config::EqualizerSettings;
2
3const BAND_FREQUENCIES: [f32; 5] = [60.0, 250.0, 1_000.0, 4_000.0, 12_000.0];
4const BAND_Q: [f32; 5] = [0.9, 1.0, 1.0, 0.9, 0.8];
5
6#[derive(Clone, Copy)]
7struct Coefficients {
8    b0: f32,
9    b1: f32,
10    b2: f32,
11    a1: f32,
12    a2: f32,
13}
14
15impl Coefficients {
16    const fn identity() -> Self {
17        Self {
18            b0: 1.0,
19            b1: 0.0,
20            b2: 0.0,
21            a1: 0.0,
22            a2: 0.0,
23        }
24    }
25}
26
27#[derive(Clone)]
28struct Biquad {
29    coeffs: Coefficients,
30    z1: f32,
31    z2: f32,
32}
33
34impl Biquad {
35    fn new(coeffs: Coefficients) -> Self {
36        Self {
37            coeffs,
38            z1: 0.0,
39            z2: 0.0,
40        }
41    }
42
43    fn set_coefficients(&mut self, coeffs: Coefficients) {
44        self.coeffs = coeffs;
45    }
46
47    fn process(&mut self, sample: f32) -> f32 {
48        let output = self.coeffs.b0 * sample + self.z1;
49        self.z1 = self.coeffs.b1 * sample - self.coeffs.a1 * output + self.z2;
50        self.z2 = self.coeffs.b2 * sample - self.coeffs.a2 * output;
51        output
52    }
53
54    fn reset(&mut self) {
55        self.z1 = 0.0;
56        self.z2 = 0.0;
57    }
58}
59
60#[derive(Clone)]
61struct Band {
62    frequency: f32,
63    q: f32,
64    gain_db: f32,
65    filters: Vec<Biquad>,
66}
67
68impl Band {
69    fn new(channels: usize, frequency: f32, q: f32) -> Self {
70        Self {
71            frequency,
72            q,
73            gain_db: 0.0,
74            filters: vec![Biquad::new(Coefficients::identity()); channels.max(1)],
75        }
76    }
77
78    fn ensure_channels(&mut self, channels: usize, sample_rate: u32) {
79        let channels = channels.max(1);
80        if self.filters.len() == channels {
81            return;
82        }
83
84        let coeffs = self.current_coefficients(sample_rate);
85        self.filters = vec![Biquad::new(coeffs); channels];
86    }
87
88    fn update_gain(&mut self, gain_db: f32, sample_rate: u32) {
89        self.gain_db = gain_db;
90        let coeffs = peaking_coefficients(sample_rate, self.frequency, self.q, gain_db);
91        for filter in &mut self.filters {
92            filter.set_coefficients(coeffs);
93        }
94    }
95
96    fn reset_filters(&mut self) {
97        for filter in &mut self.filters {
98            filter.reset();
99        }
100    }
101
102    fn process(&mut self, channel: usize, sample: f32) -> f32 {
103        let index = if channel < self.filters.len() {
104            channel
105        } else {
106            self.filters.len().saturating_sub(1)
107        };
108        self.filters[index].process(sample)
109    }
110
111    fn current_coefficients(&self, sample_rate: u32) -> Coefficients {
112        if self.gain_db.abs() < 0.01 {
113            Coefficients::identity()
114        } else {
115            peaking_coefficients(sample_rate, self.frequency, self.q, self.gain_db)
116        }
117    }
118}
119
120pub struct Equalizer {
121    settings: EqualizerSettings,
122    sample_rate: u32,
123    channels: usize,
124    bands: [Band; 5],
125    output_gain: f32,
126}
127
128impl Equalizer {
129    pub fn new(sample_rate: u32, channels: usize) -> Self {
130        let mut equalizer = Self {
131            settings: EqualizerSettings::default(),
132            sample_rate: sample_rate.max(1),
133            channels: channels.max(1),
134            bands: std::array::from_fn(|index| {
135                Band::new(channels.max(1), BAND_FREQUENCIES[index], BAND_Q[index])
136            }),
137            output_gain: 1.0,
138        };
139        equalizer.rebuild(false);
140        equalizer
141    }
142
143    pub fn set_settings(&mut self, settings: EqualizerSettings) {
144        self.settings = settings;
145        self.rebuild(false);
146    }
147
148    pub fn update_output_format(&mut self, sample_rate: u32, channels: usize) {
149        self.sample_rate = sample_rate.max(1);
150        self.channels = channels.max(1);
151        self.rebuild(true);
152    }
153
154    pub fn process_in_place(&mut self, samples: &mut [f32]) {
155        if !self.settings.enabled {
156            return;
157        }
158
159        for frame in samples.chunks_exact_mut(self.channels.max(1)) {
160            for (channel, sample) in frame.iter_mut().enumerate() {
161                let mut value = *sample * self.output_gain;
162                for band in &mut self.bands {
163                    value = band.process(channel, value);
164                }
165                if value.is_nan() {
166                    value = 0.0;
167                }
168                *sample = value.clamp(-1.0, 1.0);
169            }
170        }
171    }
172
173    fn rebuild(&mut self, reset_filter_state: bool) {
174        let resolved_bands = self.settings.resolved_bands();
175        let max_boost = resolved_bands
176            .iter()
177            .copied()
178            .fold(0.0_f32, f32::max)
179            .max(0.0);
180
181        self.output_gain = db_to_linear(self.settings.preamp_db - max_boost);
182
183        for (band, gain) in self.bands.iter_mut().zip(resolved_bands) {
184            band.ensure_channels(self.channels, self.sample_rate);
185            band.update_gain(gain, self.sample_rate);
186            if reset_filter_state {
187                band.reset_filters();
188            }
189        }
190    }
191}
192
193fn peaking_coefficients(sample_rate: u32, frequency: f32, q: f32, gain_db: f32) -> Coefficients {
194    if gain_db.abs() < 0.01 || sample_rate == 0 {
195        return Coefficients::identity();
196    }
197
198    let a = 10.0_f32.powf(gain_db / 40.0);
199    let omega = 2.0 * std::f32::consts::PI * frequency / sample_rate as f32;
200    let alpha = omega.sin() / (2.0 * q.max(0.001));
201    let cos_omega = omega.cos();
202
203    let b0 = 1.0 + alpha * a;
204    let b1 = -2.0 * cos_omega;
205    let b2 = 1.0 - alpha * a;
206    let mut a0 = 1.0 + alpha / a;
207    if a0.abs() < 1e-6 {
208        a0 = 1e-6;
209    }
210    let a1 = -2.0 * cos_omega;
211    let a2 = 1.0 - alpha / a;
212
213    Coefficients {
214        b0: b0 / a0,
215        b1: b1 / a0,
216        b2: b2 / a0,
217        a1: a1 / a0,
218        a2: a2 / a0,
219    }
220}
221
222fn db_to_linear(db: f32) -> f32 {
223    10.0_f32.powf(db / 20.0)
224}
225
226#[cfg(test)]
227mod tests {
228    use super::Equalizer;
229    use config::{EqPreset, EqualizerSettings};
230
231    #[test]
232    fn disabled_equalizer_leaves_samples_unchanged() {
233        let mut equalizer = Equalizer::new(48_000, 2);
234        equalizer.set_settings(EqualizerSettings {
235            enabled: false,
236            ..Default::default()
237        });
238
239        let mut samples = vec![0.25, -0.25, 0.1, -0.1];
240        let original = samples.clone();
241        equalizer.process_in_place(&mut samples);
242
243        assert_eq!(samples, original);
244    }
245
246    #[test]
247    fn preset_with_boost_changes_non_silent_audio() {
248        let mut equalizer = Equalizer::new(48_000, 2);
249        equalizer.set_settings(EqualizerSettings {
250            enabled: true,
251            preset: EqPreset::BassBoost,
252            ..Default::default()
253        });
254
255        let mut samples = vec![0.2, 0.2, 0.15, 0.15, 0.1, 0.1];
256        let original = samples.clone();
257        equalizer.process_in_place(&mut samples);
258
259        assert_ne!(samples, original);
260    }
261}