1use std::f32::consts::PI;
7
8use serde::{Deserialize, Serialize};
9
10use super::EffectContext;
11
12const DEFAULT_LOW_FREQ_HZ: u32 = 120;
13const DEFAULT_MID_FREQ_HZ: u32 = 1_000;
14const DEFAULT_HIGH_FREQ_HZ: u32 = 8_000;
15const DEFAULT_Q: f32 = 0.8;
16const DEFAULT_GAIN_DB: f32 = 0.0;
17const MIN_Q: f32 = 0.1;
18const MAX_Q: f32 = 10.0;
19const MIN_GAIN_DB: f32 = -24.0;
20const MAX_GAIN_DB: f32 = 24.0;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(default)]
25pub struct EqPointSettings {
26 pub freq_hz: u32,
27 pub q: f32,
28 pub gain_db: f32,
29}
30
31impl EqPointSettings {
32 pub fn new(freq_hz: u32, q: f32, gain_db: f32) -> Self {
34 Self {
35 freq_hz,
36 q,
37 gain_db,
38 }
39 }
40}
41
42impl Default for EqPointSettings {
43 fn default() -> Self {
44 Self {
45 freq_hz: DEFAULT_MID_FREQ_HZ,
46 q: DEFAULT_Q,
47 gain_db: DEFAULT_GAIN_DB,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum LowEdgeFilterSettings {
59 HighPass { freq_hz: u32, q: f32 },
60 LowShelf { freq_hz: u32, q: f32, gain_db: f32 },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum HighEdgeFilterSettings {
70 LowPass { freq_hz: u32, q: f32 },
71 HighShelf { freq_hz: u32, q: f32, gain_db: f32 },
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(default)]
77pub struct MultibandEqSettings {
78 #[serde(alias = "bands", alias = "eq_points")]
79 pub points: Vec<EqPointSettings>,
80 pub low_edge: Option<LowEdgeFilterSettings>,
81 pub high_edge: Option<HighEdgeFilterSettings>,
82}
83
84impl MultibandEqSettings {
85 pub fn new(
87 points: Vec<EqPointSettings>,
88 low_edge: Option<LowEdgeFilterSettings>,
89 high_edge: Option<HighEdgeFilterSettings>,
90 ) -> Self {
91 Self {
92 points,
93 low_edge,
94 high_edge,
95 }
96 }
97}
98
99impl Default for MultibandEqSettings {
100 fn default() -> Self {
101 Self {
102 points: vec![
103 EqPointSettings::new(DEFAULT_LOW_FREQ_HZ, DEFAULT_Q, DEFAULT_GAIN_DB),
104 EqPointSettings::new(DEFAULT_MID_FREQ_HZ, DEFAULT_Q, DEFAULT_GAIN_DB),
105 EqPointSettings::new(DEFAULT_HIGH_FREQ_HZ, DEFAULT_Q, DEFAULT_GAIN_DB),
106 ],
107 low_edge: None,
108 high_edge: None,
109 }
110 }
111}
112
113#[derive(Clone, Serialize, Deserialize)]
115#[serde(default)]
116pub struct MultibandEqEffect {
117 pub enabled: bool,
118 #[serde(flatten)]
119 pub settings: MultibandEqSettings,
120 #[serde(skip)]
121 state: Option<MultibandEqState>,
122}
123
124impl std::fmt::Debug for MultibandEqEffect {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("MultibandEqEffect")
127 .field("enabled", &self.enabled)
128 .field("settings", &self.settings)
129 .finish()
130 }
131}
132
133impl Default for MultibandEqEffect {
134 fn default() -> Self {
135 Self {
136 enabled: false,
137 settings: MultibandEqSettings::default(),
138 state: None,
139 }
140 }
141}
142
143impl MultibandEqEffect {
144 pub fn process(&mut self, samples: &[f32], context: &EffectContext, _drain: bool) -> Vec<f32> {
154 if !self.enabled {
155 return samples.to_vec();
156 }
157
158 self.ensure_state(context);
159 let Some(state) = self.state.as_mut() else {
160 return samples.to_vec();
161 };
162
163 if samples.is_empty() {
164 return Vec::new();
165 }
166
167 let channels = state.channels;
168 let mut output = Vec::with_capacity(samples.len());
169
170 for (idx, &sample) in samples.iter().enumerate() {
171 let ch = idx % channels;
172 let mut y = sample;
173
174 if let Some(filter) = state.low_edge.as_mut() {
175 y = filter.process_sample(ch, y);
176 }
177
178 for point in &mut state.points {
179 y = point.process_sample(ch, y);
180 }
181
182 if let Some(filter) = state.high_edge.as_mut() {
183 y = filter.process_sample(ch, y);
184 }
185
186 output.push(y);
187 }
188
189 output
190 }
191
192 pub fn reset_state(&mut self) {
194 if let Some(state) = self.state.as_mut() {
195 state.reset();
196 }
197 self.state = None;
198 }
199
200 fn ensure_state(&mut self, context: &EffectContext) {
201 let channels = context.channels.max(1);
202 let points = self
203 .settings
204 .points
205 .iter()
206 .map(|point| EqPointParams {
207 freq_hz: sanitize_freq(point.freq_hz, context.sample_rate),
208 q: sanitize_q(point.q),
209 gain_db: sanitize_gain_db(point.gain_db),
210 })
211 .collect::<Vec<_>>();
212
213 let low_edge = self
214 .settings
215 .low_edge
216 .as_ref()
217 .map(|edge| sanitize_low_edge(edge, context.sample_rate));
218 let high_edge = self
219 .settings
220 .high_edge
221 .as_ref()
222 .map(|edge| sanitize_high_edge(edge, context.sample_rate));
223
224 let needs_reset = self
225 .state
226 .as_ref()
227 .map(|state| {
228 state.matches(
229 context.sample_rate,
230 channels,
231 &points,
232 &low_edge,
233 &high_edge,
234 )
235 })
236 .map(|matches| !matches)
237 .unwrap_or(true);
238
239 if needs_reset {
240 self.state = Some(MultibandEqState::new(
241 context.sample_rate,
242 channels,
243 points,
244 low_edge,
245 high_edge,
246 ));
247 }
248 }
249}
250
251#[derive(Clone, Copy, Debug)]
252struct EqPointParams {
253 freq_hz: u32,
254 q: f32,
255 gain_db: f32,
256}
257
258#[derive(Clone, Copy, Debug)]
259enum LowEdgeParams {
260 HighPass { freq_hz: u32, q: f32 },
261 LowShelf { freq_hz: u32, q: f32, gain_db: f32 },
262}
263
264#[derive(Clone, Copy, Debug)]
265enum HighEdgeParams {
266 LowPass { freq_hz: u32, q: f32 },
267 HighShelf { freq_hz: u32, q: f32, gain_db: f32 },
268}
269
270#[derive(Clone, Debug)]
271struct MultibandEqState {
272 sample_rate: u32,
273 channels: usize,
274 points_params: Vec<EqPointParams>,
275 low_edge_params: Option<LowEdgeParams>,
276 high_edge_params: Option<HighEdgeParams>,
277 points: Vec<Biquad>,
278 low_edge: Option<Biquad>,
279 high_edge: Option<Biquad>,
280}
281
282impl MultibandEqState {
283 fn new(
284 sample_rate: u32,
285 channels: usize,
286 points_params: Vec<EqPointParams>,
287 low_edge_params: Option<LowEdgeParams>,
288 high_edge_params: Option<HighEdgeParams>,
289 ) -> Self {
290 let low_edge = low_edge_params.map(|params| match params {
291 LowEdgeParams::HighPass { freq_hz, q } => {
292 Biquad::new(sample_rate, channels, BiquadDesign::HighPass { freq_hz, q })
293 }
294 LowEdgeParams::LowShelf {
295 freq_hz,
296 q,
297 gain_db,
298 } => Biquad::new(
299 sample_rate,
300 channels,
301 BiquadDesign::LowShelf {
302 freq_hz,
303 q,
304 gain_db,
305 },
306 ),
307 });
308
309 let points = points_params
310 .iter()
311 .map(|params| {
312 Biquad::new(
313 sample_rate,
314 channels,
315 BiquadDesign::Peaking {
316 freq_hz: params.freq_hz,
317 q: params.q,
318 gain_db: params.gain_db,
319 },
320 )
321 })
322 .collect();
323
324 let high_edge = high_edge_params.map(|params| match params {
325 HighEdgeParams::LowPass { freq_hz, q } => {
326 Biquad::new(sample_rate, channels, BiquadDesign::LowPass { freq_hz, q })
327 }
328 HighEdgeParams::HighShelf {
329 freq_hz,
330 q,
331 gain_db,
332 } => Biquad::new(
333 sample_rate,
334 channels,
335 BiquadDesign::HighShelf {
336 freq_hz,
337 q,
338 gain_db,
339 },
340 ),
341 });
342
343 Self {
344 sample_rate,
345 channels,
346 points_params,
347 low_edge_params,
348 high_edge_params,
349 points,
350 low_edge,
351 high_edge,
352 }
353 }
354
355 fn matches(
356 &self,
357 sample_rate: u32,
358 channels: usize,
359 points_params: &[EqPointParams],
360 low_edge_params: &Option<LowEdgeParams>,
361 high_edge_params: &Option<HighEdgeParams>,
362 ) -> bool {
363 self.sample_rate == sample_rate
364 && self.channels == channels
365 && eq_point_params_vec_equal(&self.points_params, points_params)
366 && low_edge_params_equal(self.low_edge_params, *low_edge_params)
367 && high_edge_params_equal(self.high_edge_params, *high_edge_params)
368 }
369
370 fn reset(&mut self) {
371 for point in &mut self.points {
372 point.reset();
373 }
374 if let Some(filter) = self.low_edge.as_mut() {
375 filter.reset();
376 }
377 if let Some(filter) = self.high_edge.as_mut() {
378 filter.reset();
379 }
380 }
381}
382
383#[derive(Clone, Copy, Debug)]
384struct BiquadCoefficients {
385 b0: f32,
386 b1: f32,
387 b2: f32,
388 a1: f32,
389 a2: f32,
390}
391
392#[derive(Clone, Copy, Debug)]
393enum BiquadDesign {
394 Peaking { freq_hz: u32, q: f32, gain_db: f32 },
395 LowPass { freq_hz: u32, q: f32 },
396 HighPass { freq_hz: u32, q: f32 },
397 LowShelf { freq_hz: u32, q: f32, gain_db: f32 },
398 HighShelf { freq_hz: u32, q: f32, gain_db: f32 },
399}
400
401#[derive(Clone, Debug)]
402struct Biquad {
403 coeffs: BiquadCoefficients,
404 x_n1: Vec<f32>,
405 x_n2: Vec<f32>,
406 y_n1: Vec<f32>,
407 y_n2: Vec<f32>,
408}
409
410impl Biquad {
411 fn new(sample_rate: u32, channels: usize, design: BiquadDesign) -> Self {
412 let channels = channels.max(1);
413 Self {
414 coeffs: coefficients(sample_rate, design),
415 x_n1: vec![0.0; channels],
416 x_n2: vec![0.0; channels],
417 y_n1: vec![0.0; channels],
418 y_n2: vec![0.0; channels],
419 }
420 }
421
422 fn process_sample(&mut self, channel: usize, sample: f32) -> f32 {
423 let y = self.coeffs.b0 * sample
424 + self.coeffs.b1 * self.x_n1[channel]
425 + self.coeffs.b2 * self.x_n2[channel]
426 - self.coeffs.a1 * self.y_n1[channel]
427 - self.coeffs.a2 * self.y_n2[channel];
428
429 self.x_n2[channel] = self.x_n1[channel];
430 self.x_n1[channel] = sample;
431 self.y_n2[channel] = self.y_n1[channel];
432 self.y_n1[channel] = y;
433
434 y
435 }
436
437 fn reset(&mut self) {
438 self.x_n1.fill(0.0);
439 self.x_n2.fill(0.0);
440 self.y_n1.fill(0.0);
441 self.y_n2.fill(0.0);
442 }
443}
444
445fn coefficients(sample_rate: u32, design: BiquadDesign) -> BiquadCoefficients {
446 match design {
447 BiquadDesign::Peaking {
448 freq_hz,
449 q,
450 gain_db,
451 } => peaking_coefficients(sample_rate, freq_hz, q, gain_db),
452 BiquadDesign::LowPass { freq_hz, q } => low_pass_coefficients(sample_rate, freq_hz, q),
453 BiquadDesign::HighPass { freq_hz, q } => high_pass_coefficients(sample_rate, freq_hz, q),
454 BiquadDesign::LowShelf {
455 freq_hz,
456 q,
457 gain_db,
458 } => low_shelf_coefficients(sample_rate, freq_hz, q, gain_db),
459 BiquadDesign::HighShelf {
460 freq_hz,
461 q,
462 gain_db,
463 } => high_shelf_coefficients(sample_rate, freq_hz, q, gain_db),
464 }
465}
466
467fn peaking_coefficients(
468 sample_rate: u32,
469 freq_hz: u32,
470 q: f32,
471 gain_db: f32,
472) -> BiquadCoefficients {
473 let w0 = 2.0 * PI * freq_hz as f32 / sample_rate as f32;
474 let cos_w0 = w0.cos();
475 let alpha = w0.sin() / (2.0 * q);
476 let amplitude = 10.0_f32.powf(gain_db / 40.0);
477
478 let b0 = 1.0 + alpha * amplitude;
479 let b1 = -2.0 * cos_w0;
480 let b2 = 1.0 - alpha * amplitude;
481 let a0 = 1.0 + alpha / amplitude;
482 let a1 = -2.0 * cos_w0;
483 let a2 = 1.0 - alpha / amplitude;
484
485 normalized_coefficients(b0, b1, b2, a0, a1, a2)
486}
487
488fn low_pass_coefficients(sample_rate: u32, freq_hz: u32, q: f32) -> BiquadCoefficients {
489 let w0 = 2.0 * PI * freq_hz as f32 / sample_rate as f32;
490 let cos_w0 = w0.cos();
491 let alpha = w0.sin() / (2.0 * q);
492
493 let b1 = 1.0 - cos_w0;
494 let b0 = b1 / 2.0;
495 let b2 = b0;
496 let a0 = 1.0 + alpha;
497 let a1 = -2.0 * cos_w0;
498 let a2 = 1.0 - alpha;
499
500 normalized_coefficients(b0, b1, b2, a0, a1, a2)
501}
502
503fn high_pass_coefficients(sample_rate: u32, freq_hz: u32, q: f32) -> BiquadCoefficients {
504 let w0 = 2.0 * PI * freq_hz as f32 / sample_rate as f32;
505 let cos_w0 = w0.cos();
506 let alpha = w0.sin() / (2.0 * q);
507
508 let b0 = (1.0 + cos_w0) / 2.0;
509 let b1 = -1.0 - cos_w0;
510 let b2 = b0;
511 let a0 = 1.0 + alpha;
512 let a1 = -2.0 * cos_w0;
513 let a2 = 1.0 - alpha;
514
515 normalized_coefficients(b0, b1, b2, a0, a1, a2)
516}
517
518fn low_shelf_coefficients(
519 sample_rate: u32,
520 freq_hz: u32,
521 q: f32,
522 gain_db: f32,
523) -> BiquadCoefficients {
524 let w0 = 2.0 * PI * freq_hz as f32 / sample_rate as f32;
525 let cos_w0 = w0.cos();
526 let alpha = w0.sin() / (2.0 * q);
527 let amplitude = 10.0_f32.powf(gain_db / 40.0);
528 let sqrt_amplitude = amplitude.sqrt();
529
530 let b0 =
531 amplitude * ((amplitude + 1.0) - (amplitude - 1.0) * cos_w0 + 2.0 * sqrt_amplitude * alpha);
532 let b1 = 2.0 * amplitude * ((amplitude - 1.0) - (amplitude + 1.0) * cos_w0);
533 let b2 =
534 amplitude * ((amplitude + 1.0) - (amplitude - 1.0) * cos_w0 - 2.0 * sqrt_amplitude * alpha);
535 let a0 = (amplitude + 1.0) + (amplitude - 1.0) * cos_w0 + 2.0 * sqrt_amplitude * alpha;
536 let a1 = -2.0 * ((amplitude - 1.0) + (amplitude + 1.0) * cos_w0);
537 let a2 = (amplitude + 1.0) + (amplitude - 1.0) * cos_w0 - 2.0 * sqrt_amplitude * alpha;
538
539 normalized_coefficients(b0, b1, b2, a0, a1, a2)
540}
541
542fn high_shelf_coefficients(
543 sample_rate: u32,
544 freq_hz: u32,
545 q: f32,
546 gain_db: f32,
547) -> BiquadCoefficients {
548 let w0 = 2.0 * PI * freq_hz as f32 / sample_rate as f32;
549 let cos_w0 = w0.cos();
550 let alpha = w0.sin() / (2.0 * q);
551 let amplitude = 10.0_f32.powf(gain_db / 40.0);
552 let sqrt_amplitude = amplitude.sqrt();
553
554 let b0 =
555 amplitude * ((amplitude + 1.0) + (amplitude - 1.0) * cos_w0 + 2.0 * sqrt_amplitude * alpha);
556 let b1 = -2.0 * amplitude * ((amplitude - 1.0) + (amplitude + 1.0) * cos_w0);
557 let b2 =
558 amplitude * ((amplitude + 1.0) + (amplitude - 1.0) * cos_w0 - 2.0 * sqrt_amplitude * alpha);
559 let a0 = (amplitude + 1.0) - (amplitude - 1.0) * cos_w0 + 2.0 * sqrt_amplitude * alpha;
560 let a1 = 2.0 * ((amplitude - 1.0) - (amplitude + 1.0) * cos_w0);
561 let a2 = (amplitude + 1.0) - (amplitude - 1.0) * cos_w0 - 2.0 * sqrt_amplitude * alpha;
562
563 normalized_coefficients(b0, b1, b2, a0, a1, a2)
564}
565
566fn normalized_coefficients(
567 b0: f32,
568 b1: f32,
569 b2: f32,
570 a0: f32,
571 a1: f32,
572 a2: f32,
573) -> BiquadCoefficients {
574 BiquadCoefficients {
575 b0: b0 / a0,
576 b1: b1 / a0,
577 b2: b2 / a0,
578 a1: a1 / a0,
579 a2: a2 / a0,
580 }
581}
582
583fn sanitize_low_edge(edge: &LowEdgeFilterSettings, sample_rate: u32) -> LowEdgeParams {
584 match edge {
585 LowEdgeFilterSettings::HighPass { freq_hz, q } => LowEdgeParams::HighPass {
586 freq_hz: sanitize_freq(*freq_hz, sample_rate),
587 q: sanitize_q(*q),
588 },
589 LowEdgeFilterSettings::LowShelf {
590 freq_hz,
591 q,
592 gain_db,
593 } => LowEdgeParams::LowShelf {
594 freq_hz: sanitize_freq(*freq_hz, sample_rate),
595 q: sanitize_q(*q),
596 gain_db: sanitize_gain_db(*gain_db),
597 },
598 }
599}
600
601fn sanitize_high_edge(edge: &HighEdgeFilterSettings, sample_rate: u32) -> HighEdgeParams {
602 match edge {
603 HighEdgeFilterSettings::LowPass { freq_hz, q } => HighEdgeParams::LowPass {
604 freq_hz: sanitize_freq(*freq_hz, sample_rate),
605 q: sanitize_q(*q),
606 },
607 HighEdgeFilterSettings::HighShelf {
608 freq_hz,
609 q,
610 gain_db,
611 } => HighEdgeParams::HighShelf {
612 freq_hz: sanitize_freq(*freq_hz, sample_rate),
613 q: sanitize_q(*q),
614 gain_db: sanitize_gain_db(*gain_db),
615 },
616 }
617}
618
619fn sanitize_freq(freq_hz: u32, sample_rate: u32) -> u32 {
620 let nyquist = sample_rate / 2;
621 if nyquist <= 1 {
622 return 1;
623 }
624 freq_hz.clamp(1, nyquist.saturating_sub(1).max(1))
625}
626
627fn sanitize_q(q: f32) -> f32 {
628 if !q.is_finite() {
629 return DEFAULT_Q;
630 }
631 q.clamp(MIN_Q, MAX_Q)
632}
633
634fn sanitize_gain_db(gain_db: f32) -> f32 {
635 if !gain_db.is_finite() {
636 return DEFAULT_GAIN_DB;
637 }
638 gain_db.clamp(MIN_GAIN_DB, MAX_GAIN_DB)
639}
640
641fn eq_point_params_vec_equal(left: &[EqPointParams], right: &[EqPointParams]) -> bool {
642 left.len() == right.len()
643 && left
644 .iter()
645 .zip(right.iter())
646 .all(|(l, r)| eq_point_params_equal(*l, *r))
647}
648
649fn eq_point_params_equal(left: EqPointParams, right: EqPointParams) -> bool {
650 left.freq_hz == right.freq_hz
651 && (left.q - right.q).abs() < f32::EPSILON
652 && (left.gain_db - right.gain_db).abs() < f32::EPSILON
653}
654
655fn low_edge_params_equal(left: Option<LowEdgeParams>, right: Option<LowEdgeParams>) -> bool {
656 match (left, right) {
657 (None, None) => true,
658 (
659 Some(LowEdgeParams::HighPass { freq_hz: lf, q: lq }),
660 Some(LowEdgeParams::HighPass { freq_hz: rf, q: rq }),
661 ) => lf == rf && (lq - rq).abs() < f32::EPSILON,
662 (
663 Some(LowEdgeParams::LowShelf {
664 freq_hz: lf,
665 q: lq,
666 gain_db: lg,
667 }),
668 Some(LowEdgeParams::LowShelf {
669 freq_hz: rf,
670 q: rq,
671 gain_db: rg,
672 }),
673 ) => lf == rf && (lq - rq).abs() < f32::EPSILON && (lg - rg).abs() < f32::EPSILON,
674 _ => false,
675 }
676}
677
678fn high_edge_params_equal(left: Option<HighEdgeParams>, right: Option<HighEdgeParams>) -> bool {
679 match (left, right) {
680 (None, None) => true,
681 (
682 Some(HighEdgeParams::LowPass { freq_hz: lf, q: lq }),
683 Some(HighEdgeParams::LowPass { freq_hz: rf, q: rq }),
684 ) => lf == rf && (lq - rq).abs() < f32::EPSILON,
685 (
686 Some(HighEdgeParams::HighShelf {
687 freq_hz: lf,
688 q: lq,
689 gain_db: lg,
690 }),
691 Some(HighEdgeParams::HighShelf {
692 freq_hz: rf,
693 q: rq,
694 gain_db: rg,
695 }),
696 ) => lf == rf && (lq - rq).abs() < f32::EPSILON && (lg - rg).abs() < f32::EPSILON,
697 _ => false,
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 fn context() -> EffectContext {
706 EffectContext {
707 sample_rate: 48_000,
708 channels: 2,
709 container_path: None,
710 impulse_response_spec: None,
711 impulse_response_tail_db: -60.0,
712 }
713 }
714
715 #[test]
716 fn multiband_eq_disabled_passthrough() {
717 let mut effect = MultibandEqEffect::default();
718 let samples = vec![0.25_f32, -0.25, 0.5, -0.5];
719 let output = effect.process(&samples, &context(), false);
720 assert_eq!(output, samples);
721 }
722
723 #[test]
724 fn multiband_eq_points_and_edges_change_signal() {
725 let mut effect = MultibandEqEffect::default();
726 effect.enabled = true;
727 effect.settings.points = vec![
728 EqPointSettings::new(120, 0.8, 6.0),
729 EqPointSettings::new(1_000, 1.2, -4.0),
730 EqPointSettings::new(8_000, 0.9, 3.0),
731 EqPointSettings::new(12_000, 0.7, -2.0),
732 ];
733 effect.settings.low_edge = Some(LowEdgeFilterSettings::HighPass {
734 freq_hz: 40,
735 q: 0.7,
736 });
737 effect.settings.high_edge = Some(HighEdgeFilterSettings::HighShelf {
738 freq_hz: 10_000,
739 q: 0.8,
740 gain_db: 2.0,
741 });
742
743 let samples = vec![0.1_f32, -0.1, 0.2, -0.2, 0.15, -0.15, 0.3, -0.3];
744 let output = effect.process(&samples, &context(), false);
745
746 assert_eq!(output.len(), samples.len());
747 assert!(output.iter().all(|value| value.is_finite()));
748 assert!(output
749 .iter()
750 .zip(samples.iter())
751 .any(|(out, input)| (*out - *input).abs() > 1e-6));
752 }
753
754 #[test]
755 fn multiband_eq_deserializes_vec_points_and_edge_variants() {
756 let json = r#"{
757 "enabled": true,
758 "points": [
759 {"freq_hz": 120, "q": 0.8, "gain_db": 4.5},
760 {"freq_hz": 800, "q": 1.1, "gain_db": -3.0}
761 ],
762 "low_edge": {"type": "low_shelf", "freq_hz": 100, "q": 0.8, "gain_db": 2.0},
763 "high_edge": {"type": "low_pass", "freq_hz": 14000, "q": 0.7}
764 }"#;
765
766 let effect: MultibandEqEffect =
767 serde_json::from_str(json).expect("deserialize multiband eq");
768 assert_eq!(effect.settings.points.len(), 2);
769 assert!(matches!(
770 effect.settings.low_edge,
771 Some(LowEdgeFilterSettings::LowShelf { .. })
772 ));
773 assert!(matches!(
774 effect.settings.high_edge,
775 Some(HighEdgeFilterSettings::LowPass { .. })
776 ));
777 }
778}