audio_engine_core/processor/
eq.rs1pub use super::lockfree_params::EQ_BANDS;
4
5#[derive(Clone)]
7pub struct BiquadSection {
8 b0: f64,
9 b1: f64,
10 b2: f64,
11 a1: f64,
12 a2: f64,
13 z1: f64,
14 z2: f64,
15}
16
17impl BiquadSection {
18 pub fn peaking_eq(freq: f64, gain_db: f64, q: f64, sample_rate: f64) -> Self {
19 let a = 10.0_f64.powf(gain_db / 40.0);
20 let w0 = 2.0 * std::f64::consts::PI * freq / sample_rate;
21 let cos_w0 = w0.cos();
22 let sin_w0 = w0.sin();
23 let alpha = sin_w0 / (2.0 * q);
24
25 let b0 = 1.0 + alpha * a;
26 let b1 = -2.0 * cos_w0;
27 let b2 = 1.0 - alpha * a;
28 let a0 = 1.0 + alpha / a;
29 let a1 = -2.0 * cos_w0;
30 let a2 = 1.0 - alpha / a;
31
32 Self {
33 b0: b0 / a0,
34 b1: b1 / a0,
35 b2: b2 / a0,
36 a1: a1 / a0,
37 a2: a2 / a0,
38 z1: 0.0,
39 z2: 0.0,
40 }
41 }
42
43 #[inline]
44 pub fn process(&mut self, x: f64) -> f64 {
45 let y = self.b0 * x + self.z1;
46 self.z1 = self.b1 * x - self.a1 * y + self.z2;
47 self.z2 = self.b2 * x - self.a2 * y;
48 y
49 }
50
51 pub fn reset(&mut self) {
52 self.z1 = 0.0;
53 self.z2 = 0.0;
54 }
55
56 pub fn copy_coefficients_from(&mut self, other: &Self) {
59 self.b0 = other.b0;
60 self.b1 = other.b1;
61 self.b2 = other.b2;
62 self.a1 = other.a1;
63 self.a2 = other.a2;
64 }
66}
67
68pub struct Equalizer {
70 bands: Vec<[BiquadSection; EQ_BANDS]>, target_bands: Vec<[BiquadSection; EQ_BANDS]>, target_gains: Vec<f64>, smooth_counter: Vec<u32>, channels: usize,
75 enabled: bool,
76}
77
78const EQ_SMOOTH_SAMPLES: u32 = 1024; const INV_EQ_SMOOTH: f64 = 1.0 / EQ_SMOOTH_SAMPLES as f64;
80
81impl Equalizer {
82 const FREQUENCIES: [f64; EQ_BANDS] = [
83 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
84 ];
85 const Q: f64 = 1.41;
86
87 pub fn new(channels: usize, sample_rate: f64) -> Self {
88 let bands: Vec<[BiquadSection; EQ_BANDS]> = (0..channels)
89 .map(|_| Self::build_channel_bank(sample_rate))
90 .collect();
91 let target_bands = bands.clone();
92
93 Self {
94 bands,
95 target_bands,
96 target_gains: vec![0.0; EQ_BANDS],
97 smooth_counter: vec![0u32; EQ_BANDS],
98 channels,
99 enabled: false,
100 }
101 }
102
103 fn build_channel_bank(sample_rate: f64) -> [BiquadSection; EQ_BANDS] {
104 std::array::from_fn(|idx| {
105 BiquadSection::peaking_eq(Self::FREQUENCIES[idx], 0.0, Self::Q, sample_rate)
106 })
107 }
108
109 pub fn set_band_gain(&mut self, band_idx: usize, gain_db: f64, sample_rate: f64) {
110 if band_idx >= EQ_BANDS {
111 return;
112 }
113 let gain_db = gain_db.clamp(-15.0, 15.0);
114 let freq = Self::FREQUENCIES[band_idx];
115 for ch in 0..self.channels {
117 self.target_bands[ch][band_idx] =
118 BiquadSection::peaking_eq(freq, gain_db, Self::Q, sample_rate);
119 }
120 self.target_gains[band_idx] = gain_db;
121 self.smooth_counter[band_idx] = EQ_SMOOTH_SAMPLES;
123 }
124
125 pub fn set_all_bands(&mut self, gains: &[f64; EQ_BANDS], sample_rate: f64) {
126 for (idx, &gain) in gains.iter().enumerate() {
127 self.set_band_gain(idx, gain, sample_rate);
128 }
129 }
130
131 pub fn set_enabled(&mut self, enabled: bool) {
132 self.enabled = enabled;
133 }
134
135 pub fn is_enabled(&self) -> bool {
136 self.enabled
137 }
138
139 pub fn process(&mut self, buffer: &mut [f64]) {
140 if !self.enabled {
141 return;
142 }
143 debug_assert!(self.bands.len() >= self.channels);
144 debug_assert!(self.target_bands.len() >= self.channels);
145 let frames = buffer.len() / self.channels;
146
147 if self.channels == 2 && self.smooth_counter.iter().all(|&counter| counter == 0) {
148 self.process_settled_stereo(buffer);
149 return;
150 }
151
152 for frame in 0..frames {
153 for ch in 0..self.channels {
155 let idx = frame * self.channels + ch;
156 buffer[idx] = self.process_sample_no_counter_update(buffer[idx], ch);
157 }
158
159 for b in 0..EQ_BANDS {
162 if self.smooth_counter[b] > 0 {
163 self.smooth_counter[b] -= 1;
164 if self.smooth_counter[b] == 0 {
166 for c in 0..self.channels {
167 self.bands[c][b].copy_coefficients_from(&self.target_bands[c][b]);
168 }
169 }
170 }
171 }
172 }
173 }
174
175 fn process_settled_stereo(&mut self, buffer: &mut [f64]) {
176 debug_assert_eq!(self.channels, 2);
177 debug_assert!(self.smooth_counter.iter().all(|&counter| counter == 0));
178
179 let (left_banks, right_banks) = self.bands.split_at_mut(1);
180 let left_bands = &mut left_banks[0];
181 let right_bands = &mut right_banks[0];
182
183 for frame in buffer.chunks_exact_mut(2) {
184 let mut left = frame[0];
185 for band in left_bands.iter_mut() {
186 left = band.process(left);
187 }
188 frame[0] = left;
189
190 let mut right = frame[1];
191 for band in right_bands.iter_mut() {
192 right = band.process(right);
193 }
194 frame[1] = right;
195 }
196 }
197
198 #[inline]
201 fn process_sample_no_counter_update(&mut self, mut sample: f64, ch: usize) -> f64 {
202 debug_assert!(ch < self.channels);
203 for b in 0..EQ_BANDS {
204 if self.smooth_counter[b] > 0 {
205 let current_out = self.bands[ch][b].process(sample);
207 let target_out = self.target_bands[ch][b].process(sample);
208 let t = self.smooth_counter[b] as f64 * INV_EQ_SMOOTH;
209 sample = current_out * t + target_out * (1.0 - t);
210 } else {
211 sample = self.bands[ch][b].process(sample);
212 }
213 }
214 sample
215 }
216
217 pub fn reset(&mut self) {
222 for ch in &mut self.bands {
223 for band in ch {
224 band.reset();
225 }
226 }
227 for ch in &mut self.target_bands {
228 for band in ch {
229 band.reset();
230 }
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn fixed_band_banks_are_allocated_per_channel() {
241 for channels in [1, 2, 6, 8] {
242 let eq = Equalizer::new(channels, 48_000.0);
243 assert_eq!(eq.bands.len(), channels);
244 assert_eq!(eq.target_bands.len(), channels);
245 assert!(eq.bands.iter().all(|bank| bank.len() == EQ_BANDS));
246 assert!(eq.target_bands.iter().all(|bank| bank.len() == EQ_BANDS));
247 }
248 }
249
250 #[test]
251 fn reset_clears_current_and_target_bank_state() {
252 let mut eq = Equalizer::new(2, 48_000.0);
253 eq.set_enabled(true);
254 eq.set_band_gain(0, 6.0, 48_000.0);
255
256 let mut buffer = vec![0.25; 256];
257 eq.process(&mut buffer);
258
259 assert!(eq
260 .bands
261 .iter()
262 .flatten()
263 .chain(eq.target_bands.iter().flatten())
264 .any(|band| band.z1 != 0.0 || band.z2 != 0.0));
265
266 eq.reset();
267
268 assert!(eq
269 .bands
270 .iter()
271 .flatten()
272 .chain(eq.target_bands.iter().flatten())
273 .all(|band| band.z1 == 0.0 && band.z2 == 0.0));
274 }
275
276 #[test]
277 fn settled_stereo_fast_path_matches_regular_path() {
278 let gains = [12.0, 9.0, 6.0, 3.0, -3.0, -6.0, -9.0, -12.0, 6.0, -6.0];
279 let mut regular = Equalizer::new(2, 48_000.0);
280 let mut fast = Equalizer::new(2, 48_000.0);
281 regular.set_enabled(true);
282 fast.set_enabled(true);
283 regular.set_all_bands(&gains, 48_000.0);
284 fast.set_all_bands(&gains, 48_000.0);
285
286 let mut silence = vec![0.0; 2 * (EQ_SMOOTH_SAMPLES as usize + 1)];
287 regular.process(&mut silence);
288 fast.process(&mut silence);
289 assert!(regular.smooth_counter.iter().all(|&counter| counter == 0));
290 assert!(fast.smooth_counter.iter().all(|&counter| counter == 0));
291
292 let mut regular_buffer = (0..2048)
293 .map(|sample| {
294 let t = sample as f64 / 48_000.0;
295 (2.0 * std::f64::consts::PI * 997.0 * t).sin() * 0.25
296 })
297 .collect::<Vec<_>>();
298 let mut fast_buffer = regular_buffer.clone();
299
300 regular.process_sample_by_sample_for_test(&mut regular_buffer);
301 fast.process(&mut fast_buffer);
302
303 let max_abs = regular_buffer
304 .iter()
305 .zip(&fast_buffer)
306 .map(|(a, b)| (a - b).abs())
307 .fold(0.0, f64::max);
308 assert!(max_abs <= 1.0e-12, "max_abs={max_abs:.3e}");
309 }
310
311 impl Equalizer {
312 fn process_sample_by_sample_for_test(&mut self, buffer: &mut [f64]) {
313 let frames = buffer.len() / self.channels;
314 for frame in 0..frames {
315 for ch in 0..self.channels {
316 let idx = frame * self.channels + ch;
317 buffer[idx] = self.process_sample_no_counter_update(buffer[idx], ch);
318 }
319 }
320 }
321 }
322}