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}