1use atomic_float::AtomicF32;
26use std::sync::atomic::{AtomicBool, Ordering};
27
28#[derive(Clone, Copy, Debug)]
34struct BiquadCoeffs {
35 b0: f64,
36 b1: f64,
37 b2: f64,
38 a1: f64,
39 a2: f64,
40}
41
42impl Default for BiquadCoeffs {
43 fn default() -> Self {
44 Self {
45 b0: 1.0,
46 b1: 0.0,
47 b2: 0.0,
48 a1: 0.0,
49 a2: 0.0,
50 }
51 }
52}
53
54#[derive(Clone, Debug, Default)]
56struct BiquadState {
57 z1: f64,
58 z2: f64,
59}
60
61#[derive(Clone, Debug)]
63struct BiquadGeometry {
64 freq: f64,
65 q: f64,
66 sample_rate: f64,
67 cos_w0: f64,
68 sin_w0: f64,
69 alpha: f64,
70}
71
72impl BiquadGeometry {
73 fn new(freq: f64, q: f64, sample_rate: f64, filter_type: FilterType) -> Self {
74 let w0 = 2.0 * std::f64::consts::PI * freq / sample_rate;
75 let cos_w0 = w0.cos();
76 let sin_w0 = w0.sin();
77 let alpha = match filter_type {
78 FilterType::Peaking => sin_w0 / (2.0 * q),
79 FilterType::LowShelf | FilterType::HighShelf => sin_w0 / std::f64::consts::SQRT_2,
80 };
81
82 Self {
83 freq,
84 q,
85 sample_rate,
86 cos_w0,
87 sin_w0,
88 alpha,
89 }
90 }
91}
92
93#[derive(Clone, Debug)]
95struct BiquadFilter {
96 geometry: BiquadGeometry,
97 coeffs: BiquadCoeffs,
98 state: BiquadState,
99 filter_type: FilterType,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq)]
103enum FilterType {
104 Peaking,
105 LowShelf,
106 HighShelf,
107}
108
109impl BiquadFilter {
110 fn peaking(freq: f64, gain_db: f64, q: f64, sample_rate: f64) -> Self {
112 let filter_type = FilterType::Peaking;
113 let geometry = BiquadGeometry::new(freq, q, sample_rate, filter_type);
114 let coeffs = Self::calc_peaking_coeffs(&geometry, gain_db);
115 Self {
116 geometry,
117 coeffs,
118 state: BiquadState::default(),
119 filter_type,
120 }
121 }
122
123 fn low_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> Self {
125 let filter_type = FilterType::LowShelf;
126 let geometry = BiquadGeometry::new(freq, 0.7, sample_rate, filter_type);
127 let coeffs = Self::calc_low_shelf_coeffs(&geometry, gain_db);
128 Self {
129 geometry,
130 coeffs,
131 state: BiquadState::default(),
132 filter_type,
133 }
134 }
135
136 fn high_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> Self {
138 let filter_type = FilterType::HighShelf;
139 let geometry = BiquadGeometry::new(freq, 0.7, sample_rate, filter_type);
140 let coeffs = Self::calc_high_shelf_coeffs(&geometry, gain_db);
141 Self {
142 geometry,
143 coeffs,
144 state: BiquadState::default(),
145 filter_type,
146 }
147 }
148
149 fn calc_peaking_coeffs(geometry: &BiquadGeometry, gain_db: f64) -> BiquadCoeffs {
152 if gain_db.abs() < 0.0001 {
153 return BiquadCoeffs::default();
155 }
156
157 let a = 10.0_f64.powf(gain_db / 40.0); let cos_w0 = geometry.cos_w0;
159 let alpha = geometry.alpha;
160
161 let b0 = 1.0 + alpha * a;
162 let b1 = -2.0 * cos_w0;
163 let b2 = 1.0 - alpha * a;
164 let a0 = 1.0 + alpha / a;
165 let a1 = -2.0 * cos_w0;
166 let a2 = 1.0 - alpha / a;
167
168 BiquadCoeffs {
169 b0: b0 / a0,
170 b1: b1 / a0,
171 b2: b2 / a0,
172 a1: a1 / a0,
173 a2: a2 / a0,
174 }
175 }
176
177 fn calc_low_shelf_coeffs(geometry: &BiquadGeometry, gain_db: f64) -> BiquadCoeffs {
180 if gain_db.abs() < 0.0001 {
181 return BiquadCoeffs::default();
182 }
183
184 let a = 10.0_f64.powf(gain_db / 40.0);
185 let cos_w0 = geometry.cos_w0;
186 let sin_w0 = geometry.sin_w0;
187
188 let alpha = geometry.alpha;
192 let beta = 2.0 * a.sqrt() * alpha;
193
194 let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + beta * sin_w0);
195 let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
196 let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - beta * sin_w0);
197 let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + beta * sin_w0;
198 let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
199 let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - beta * sin_w0;
200
201 BiquadCoeffs {
202 b0: b0 / a0,
203 b1: b1 / a0,
204 b2: b2 / a0,
205 a1: a1 / a0,
206 a2: a2 / a0,
207 }
208 }
209
210 fn calc_high_shelf_coeffs(geometry: &BiquadGeometry, gain_db: f64) -> BiquadCoeffs {
213 if gain_db.abs() < 0.0001 {
214 return BiquadCoeffs::default();
215 }
216
217 let a = 10.0_f64.powf(gain_db / 40.0);
218 let cos_w0 = geometry.cos_w0;
219 let sin_w0 = geometry.sin_w0;
220
221 let alpha = geometry.alpha;
223 let beta = 2.0 * a.sqrt() * alpha;
224
225 let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + beta * sin_w0);
226 let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
227 let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - beta * sin_w0);
228 let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + beta * sin_w0;
229 let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
230 let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - beta * sin_w0;
231
232 BiquadCoeffs {
233 b0: b0 / a0,
234 b1: b1 / a0,
235 b2: b2 / a0,
236 a1: a1 / a0,
237 a2: a2 / a0,
238 }
239 }
240
241 #[cfg(test)]
243 fn set_gain_db(&mut self, gain_db: f64) {
244 self.coeffs = match self.filter_type {
245 FilterType::Peaking => Self::calc_peaking_coeffs(&self.geometry, gain_db),
246 FilterType::LowShelf => Self::calc_low_shelf_coeffs(&self.geometry, gain_db),
247 FilterType::HighShelf => Self::calc_high_shelf_coeffs(&self.geometry, gain_db),
248 };
249 }
250
251 #[inline(always)]
253 fn process(&mut self, x: f64) -> f64 {
254 let y = self.coeffs.b0 * x + self.state.z1;
255 self.state.z1 = self.coeffs.b1 * x - self.coeffs.a1 * y + self.state.z2;
256 self.state.z2 = self.coeffs.b2 * x - self.coeffs.a2 * y;
257 #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
258 {
259 self.state.z1 = crate::runtime::flush_subnormal_sample(self.state.z1);
260 self.state.z2 = crate::runtime::flush_subnormal_sample(self.state.z2);
261 }
262 y
263 }
264
265 fn reset(&mut self) {
267 self.state = BiquadState::default();
268 }
269
270 fn set_sample_rate(&mut self, sample_rate: f64) {
272 if (self.geometry.sample_rate - sample_rate).abs() > 1.0 {
273 self.geometry = BiquadGeometry::new(
274 self.geometry.freq,
275 self.geometry.q,
276 sample_rate,
277 self.filter_type,
278 );
279 self.coeffs = match self.filter_type {
281 FilterType::Peaking => Self::calc_peaking_coeffs(&self.geometry, 0.0),
282 FilterType::LowShelf => Self::calc_low_shelf_coeffs(&self.geometry, 0.0),
283 FilterType::HighShelf => Self::calc_high_shelf_coeffs(&self.geometry, 0.0),
284 };
285 }
286 }
287}
288
289#[derive(Debug, Clone)]
295struct ParameterSmoother {
296 current: f64,
297 target: f64,
298 coeff: f64,
300 samples_remaining: usize,
302}
303
304impl ParameterSmoother {
305 fn new(smoothing_time_ms: f64, sample_rate: f64) -> Self {
307 let tau = (smoothing_time_ms / 1000.0) * sample_rate;
308 let coeff = if tau > 0.0 { (-1.0 / tau).exp() } else { 0.0 };
309
310 Self {
311 current: 0.0,
312 target: 0.0,
313 coeff,
314 samples_remaining: 0,
315 }
316 }
317
318 fn set_target(&mut self, target: f64) {
320 if (self.target - target).abs() > 0.0001 {
321 self.target = target;
322 self.samples_remaining = usize::MAX; }
324 }
325
326 fn next_block(&mut self, block_size: usize) -> f64 {
329 if self.samples_remaining > 0 {
330 let remaining_factor = self.coeff.powi(block_size as i32);
333 self.current = self.current + (self.target - self.current) * (1.0 - remaining_factor);
334
335 if (self.current - self.target).abs() < 0.0001 {
336 self.current = self.target;
337 self.samples_remaining = 0;
338 }
339 }
340 self.current
341 }
342
343 fn reset(&mut self) {
345 self.current = 0.0;
346 self.target = 0.0;
347 self.samples_remaining = 0;
348 }
349}
350
351pub const LOUDNESS_BANDS: [(f64, f64, f64); 7] = [
366 (40.0, 12.0, 0.0), (100.0, 10.0, 0.9),
368 (300.0, 4.0, 1.0),
369 (1000.0, 0.0, 1.0), (3000.0, 2.0, 0.9),
371 (8000.0, 4.0, 0.8),
372 (12000.0, 6.0, 0.0), ];
374
375pub const LOUDNESS_BANDS_N: usize = 7;
376
377const BLOCK_SIZE: usize = 64;
379const GAIN_UPDATE_EPSILON_DB: f64 = 0.01;
380const BAND_ACTIVE_EPSILON_DB: f64 = 0.0001;
381
382pub struct DynamicLoudness {
388 filters: Vec<[BiquadFilter; LOUDNESS_BANDS_N]>,
390 smoothers: Vec<ParameterSmoother>,
392 last_applied_gains: [f64; LOUDNESS_BANDS_N],
394 active_bands: [bool; LOUDNESS_BANDS_N],
396 max_gains: [f64; LOUDNESS_BANDS_N],
398 ref_volume_db: f64,
400 transition_db: f64,
402 pre_gain_linear: f64,
404 sample_rate: f64,
406 channels: usize,
408 current_loudness_factor: f64,
410 strength: f64,
412 enabled: bool,
414}
415
416impl DynamicLoudness {
417 pub fn new(channels: usize, sample_rate: f64) -> Self {
419 let filters: Vec<[BiquadFilter; LOUDNESS_BANDS_N]> = (0..channels)
420 .map(|_| Self::build_channel_filters(sample_rate))
421 .collect();
422
423 let smoothers: Vec<ParameterSmoother> = LOUDNESS_BANDS
424 .iter()
425 .map(|_| ParameterSmoother::new(50.0, sample_rate)) .collect();
427
428 let max_gains = LOUDNESS_BANDS.map(|(_, max_gain, _)| max_gain);
429
430 Self {
431 filters,
432 smoothers,
433 last_applied_gains: [f64::NAN; LOUDNESS_BANDS_N],
434 active_bands: [false; LOUDNESS_BANDS_N],
435 max_gains,
436 ref_volume_db: -15.0, transition_db: 25.0, pre_gain_linear: 10.0_f64.powf(-3.0 / 20.0),
440 sample_rate,
441 channels,
442 current_loudness_factor: 0.0,
443 strength: 1.0,
444 enabled: true,
445 }
446 }
447
448 fn build_channel_filters(sample_rate: f64) -> [BiquadFilter; LOUDNESS_BANDS_N] {
449 std::array::from_fn(|idx| {
450 let (freq, _max_gain, q) = LOUDNESS_BANDS[idx];
451 if q == 0.0 && freq < 1000.0 {
452 BiquadFilter::low_shelf(freq, 0.0, sample_rate)
453 } else if q == 0.0 {
454 BiquadFilter::high_shelf(freq, 0.0, sample_rate)
455 } else {
456 BiquadFilter::peaking(freq, 0.0, q, sample_rate)
457 }
458 })
459 }
460
461 fn calculate_band_coeffs(&self, band: usize, gain_db: f64) -> BiquadCoeffs {
462 let filter = &self.filters[0][band];
463 match filter.filter_type {
464 FilterType::Peaking => BiquadFilter::calc_peaking_coeffs(&filter.geometry, gain_db),
465 FilterType::LowShelf => BiquadFilter::calc_low_shelf_coeffs(&filter.geometry, gain_db),
466 FilterType::HighShelf => {
467 BiquadFilter::calc_high_shelf_coeffs(&filter.geometry, gain_db)
468 }
469 }
470 }
471
472 fn apply_band_gain_if_changed(&mut self, band: usize, gain_db: f64) {
473 let should_be_active = gain_db.abs() >= BAND_ACTIVE_EPSILON_DB;
474 if (gain_db - self.last_applied_gains[band]).abs() < GAIN_UPDATE_EPSILON_DB
475 && self.active_bands[band] == should_be_active
476 {
477 return;
478 }
479
480 let coeffs = self.calculate_band_coeffs(band, gain_db);
481 for ch_filters in &mut self.filters {
482 ch_filters[band].coeffs = coeffs;
483 }
484 self.last_applied_gains[band] = gain_db;
485 self.active_bands[band] = should_be_active;
486 }
487
488 fn refresh_smoother_targets(&mut self) {
489 for (i, smoother) in self.smoothers.iter_mut().enumerate() {
490 let target_gain = self.max_gains[i] * self.current_loudness_factor * self.strength;
491 smoother.set_target(target_gain);
492 }
493 }
494
495 fn can_bypass_for_zero_strength(&self) -> bool {
496 self.strength < 0.0001
497 && self.active_bands.iter().all(|&active| !active)
498 && self
499 .smoothers
500 .iter()
501 .all(|smoother| smoother.samples_remaining == 0)
502 }
503
504 pub fn set_volume(&mut self, linear_volume: f64) {
507 let volume_db = if linear_volume > 0.0 {
508 20.0 * linear_volume.log10()
509 } else {
510 f64::NEG_INFINITY
511 };
512
513 self.update_loudness_factor(volume_db);
514 }
515
516 pub fn set_volume_percent(&mut self, percent: f64) {
518 self.set_volume(percent / 100.0);
519 }
520
521 pub fn set_volume_db(&mut self, volume_db: f64) {
523 self.update_loudness_factor(volume_db);
524 }
525
526 fn update_loudness_factor(&mut self, volume_db: f64) {
528 let factor = if volume_db >= self.ref_volume_db {
530 0.0
531 } else {
532 ((self.ref_volume_db - volume_db) / self.transition_db).min(1.0)
533 };
534
535 if (self.current_loudness_factor - factor).abs() > 0.0001 {
537 self.current_loudness_factor = factor;
538 self.refresh_smoother_targets();
539 }
540 }
541
542 pub fn set_strength(&mut self, strength: f64) {
544 let strength = strength.clamp(0.0, 1.0);
545 if (self.strength - strength).abs() > 0.0001 {
546 self.strength = strength;
547 self.refresh_smoother_targets();
548 }
549 }
550
551 pub fn set_reference_volume_db(&mut self, ref_db: f64) {
553 self.ref_volume_db = ref_db.clamp(-30.0, 0.0);
554 }
555
556 pub fn set_transition_db(&mut self, transition_db: f64) {
558 self.transition_db = transition_db.clamp(10.0, 40.0);
559 }
560
561 pub fn set_enabled(&mut self, enabled: bool) {
563 if self.enabled && !enabled {
564 for ch_filters in &mut self.filters {
566 for filter in ch_filters {
567 filter.reset();
568 }
569 }
570 for smoother in &mut self.smoothers {
571 smoother.reset();
572 }
573 self.active_bands = [false; LOUDNESS_BANDS_N];
574 self.last_applied_gains = [f64::NAN; LOUDNESS_BANDS_N];
575 }
576 self.enabled = enabled;
577 }
578
579 pub fn set_sample_rate(&mut self, sample_rate: f64) {
581 if (self.sample_rate - sample_rate).abs() > 1.0 {
582 self.sample_rate = sample_rate;
583
584 for ch_filters in &mut self.filters {
586 for filter in ch_filters {
587 filter.set_sample_rate(sample_rate);
588 }
589 }
590 self.last_applied_gains = [f64::NAN; LOUDNESS_BANDS_N];
591 self.active_bands = [false; LOUDNESS_BANDS_N];
592
593 for smoother in &mut self.smoothers {
595 *smoother = ParameterSmoother::new(50.0, sample_rate);
596 }
597 }
598 }
599
600 pub fn process(&mut self, buffer: &mut [f64]) {
610 if !self.enabled || self.can_bypass_for_zero_strength() {
611 return;
612 }
613
614 let frames = buffer.len() / self.channels;
615 if frames == 0 {
616 return;
617 }
618
619 for chunk_start in (0..frames).step_by(BLOCK_SIZE) {
622 let chunk_end = (chunk_start + BLOCK_SIZE).min(frames);
623 let chunk_frames = chunk_end - chunk_start;
624
625 for i in 0..self.smoothers.len() {
626 let gain = self.smoothers[i].next_block(chunk_frames);
627 self.apply_band_gain_if_changed(i, gain);
628 }
629
630 self.process_samples_range(buffer, chunk_start, chunk_end);
631 }
632 }
633
634 fn process_samples_range(&mut self, buffer: &mut [f64], start_frame: usize, end_frame: usize) {
639 for frame in start_frame..end_frame {
640 for ch in 0..self.channels {
641 let idx = frame * self.channels + ch;
642 let mut sample = buffer[idx] * self.pre_gain_linear;
643
644 let ch_filters = &mut self.filters[ch];
645 for (band, filter) in ch_filters.iter_mut().enumerate() {
646 if self.active_bands[band] {
647 sample = filter.process(sample);
648 }
649 }
650
651 buffer[idx] = sample;
652 }
653 }
654 }
655
656 pub fn reset(&mut self) {
658 for ch_filters in &mut self.filters {
659 for filter in ch_filters {
660 filter.reset();
661 }
662 }
663 for smoother in &mut self.smoothers {
664 smoother.reset();
665 }
666 self.current_loudness_factor = 0.0;
667 self.last_applied_gains = [f64::NAN; LOUDNESS_BANDS_N];
668 self.active_bands = [false; LOUDNESS_BANDS_N];
669 }
670
671 pub fn loudness_factor(&self) -> f64 {
673 self.current_loudness_factor
674 }
675
676 pub fn get_band_gains(&self) -> [f64; LOUDNESS_BANDS_N] {
678 let mut gains = [0.0; LOUDNESS_BANDS_N];
679 for (i, smoother) in self.smoothers.iter().enumerate() {
680 gains[i] = smoother.current;
681 }
682 gains
683 }
684
685 pub fn is_enabled(&self) -> bool {
687 self.enabled
688 }
689
690 pub fn strength(&self) -> f64 {
692 self.strength
693 }
694}
695
696pub struct AtomicDynamicLoudnessState {
702 pub volume: AtomicF32,
704 pub strength: AtomicF32,
706 pub enabled: AtomicBool,
708}
709
710impl AtomicDynamicLoudnessState {
711 pub fn new() -> Self {
712 Self {
713 volume: AtomicF32::new(1.0),
714 strength: AtomicF32::new(1.0),
715 enabled: AtomicBool::new(true),
716 }
717 }
718
719 pub fn set_volume(&self, volume: f32) {
721 self.volume.store(volume.clamp(0.0, 1.0), Ordering::Relaxed);
722 }
723
724 pub fn set_strength(&self, strength: f32) {
726 self.strength
727 .store(strength.clamp(0.0, 1.0), Ordering::Relaxed);
728 }
729
730 pub fn set_enabled(&self, enabled: bool) {
732 self.enabled.store(enabled, Ordering::Relaxed);
733 }
734
735 pub fn sync_to_processor(&self, processor: &mut DynamicLoudness) {
737 let volume = self.volume.load(Ordering::Relaxed) as f64;
738 let strength = self.strength.load(Ordering::Relaxed) as f64;
739 let enabled = self.enabled.load(Ordering::Relaxed);
740
741 processor.set_volume(volume);
742 processor.set_strength(strength);
743 processor.set_enabled(enabled);
744 }
745}
746
747impl Default for AtomicDynamicLoudnessState {
748 fn default() -> Self {
749 Self::new()
750 }
751}
752
753#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn process_tracks_block_coefficient_ramp_within_buffer() {
763 let make = || {
764 let mut dl = DynamicLoudness::new(2, 44_100.0);
765 dl.set_strength(1.0);
766 dl.set_volume(0.05);
767 dl
768 };
769 let frames = BLOCK_SIZE * 4 + 17;
770 let input: Vec<f64> = (0..frames * 2)
771 .map(|i| ((i as f64) * 0.013).sin() * 0.3)
772 .collect();
773 let mut whole = make();
774 let mut wbuf = input.clone();
775 whole.process(&mut wbuf);
776 let mut chunked = make();
777 let mut cbuf = input.clone();
778 for cs in (0..frames).step_by(BLOCK_SIZE) {
779 let ce = (cs + BLOCK_SIZE).min(frames);
780 chunked.process(&mut cbuf[cs * 2..ce * 2]);
781 }
782 for (w, c) in wbuf.iter().zip(&cbuf) {
783 assert!((w - c).abs() < 1e-9, "{} vs {}", w, c);
784 }
785 }
786
787 fn legacy_peaking_coeffs(freq: f64, gain_db: f64, q: f64, sample_rate: f64) -> BiquadCoeffs {
788 if gain_db.abs() < 0.0001 {
789 return BiquadCoeffs::default();
790 }
791
792 let a = 10.0_f64.powf(gain_db / 40.0);
793 let w0 = 2.0 * std::f64::consts::PI * freq / sample_rate;
794 let cos_w0 = w0.cos();
795 let sin_w0 = w0.sin();
796 let alpha = sin_w0 / (2.0 * q);
797
798 let b0 = 1.0 + alpha * a;
799 let b1 = -2.0 * cos_w0;
800 let b2 = 1.0 - alpha * a;
801 let a0 = 1.0 + alpha / a;
802 let a1 = -2.0 * cos_w0;
803 let a2 = 1.0 - alpha / a;
804
805 BiquadCoeffs {
806 b0: b0 / a0,
807 b1: b1 / a0,
808 b2: b2 / a0,
809 a1: a1 / a0,
810 a2: a2 / a0,
811 }
812 }
813
814 fn legacy_low_shelf_coeffs(freq: f64, gain_db: f64, sample_rate: f64) -> BiquadCoeffs {
815 if gain_db.abs() < 0.0001 {
816 return BiquadCoeffs::default();
817 }
818
819 let a = 10.0_f64.powf(gain_db / 40.0);
820 let w0 = 2.0 * std::f64::consts::PI * freq / sample_rate;
821 let cos_w0 = w0.cos();
822 let sin_w0 = w0.sin();
823 let alpha = sin_w0 / std::f64::consts::SQRT_2;
824 let beta = 2.0 * a.sqrt() * alpha;
825
826 let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + beta * sin_w0);
827 let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
828 let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - beta * sin_w0);
829 let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + beta * sin_w0;
830 let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
831 let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - beta * sin_w0;
832
833 BiquadCoeffs {
834 b0: b0 / a0,
835 b1: b1 / a0,
836 b2: b2 / a0,
837 a1: a1 / a0,
838 a2: a2 / a0,
839 }
840 }
841
842 fn legacy_high_shelf_coeffs(freq: f64, gain_db: f64, sample_rate: f64) -> BiquadCoeffs {
843 if gain_db.abs() < 0.0001 {
844 return BiquadCoeffs::default();
845 }
846
847 let a = 10.0_f64.powf(gain_db / 40.0);
848 let w0 = 2.0 * std::f64::consts::PI * freq / sample_rate;
849 let cos_w0 = w0.cos();
850 let sin_w0 = w0.sin();
851 let alpha = sin_w0 / std::f64::consts::SQRT_2;
852 let beta = 2.0 * a.sqrt() * alpha;
853
854 let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + beta * sin_w0);
855 let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
856 let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - beta * sin_w0);
857 let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + beta * sin_w0;
858 let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
859 let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - beta * sin_w0;
860
861 BiquadCoeffs {
862 b0: b0 / a0,
863 b1: b1 / a0,
864 b2: b2 / a0,
865 a1: a1 / a0,
866 a2: a2 / a0,
867 }
868 }
869
870 fn assert_coeffs_bit_equal(actual: &BiquadCoeffs, expected: &BiquadCoeffs) {
871 assert_eq!(actual.b0.to_bits(), expected.b0.to_bits(), "b0");
872 assert_eq!(actual.b1.to_bits(), expected.b1.to_bits(), "b1");
873 assert_eq!(actual.b2.to_bits(), expected.b2.to_bits(), "b2");
874 assert_eq!(actual.a1.to_bits(), expected.a1.to_bits(), "a1");
875 assert_eq!(actual.a2.to_bits(), expected.a2.to_bits(), "a2");
876 }
877
878 #[test]
879 fn test_cached_geometry_coefficients_match_legacy_formulas() {
880 let cases = [
881 (FilterType::LowShelf, 40.0, 0.7, 12.0, 192_000.0),
882 (FilterType::Peaking, 100.0, 0.9, -12.0, 44_100.0),
883 (FilterType::Peaking, 3000.0, 0.9, 20.0, 48_000.0),
884 (FilterType::HighShelf, 12000.0, 0.7, -20.0, 44_100.0),
885 ];
886
887 for (filter_type, freq, q, gain, sample_rate) in cases {
888 let mut filter = match filter_type {
889 FilterType::Peaking => BiquadFilter::peaking(freq, 0.0, q, sample_rate),
890 FilterType::LowShelf => BiquadFilter::low_shelf(freq, 0.0, sample_rate),
891 FilterType::HighShelf => BiquadFilter::high_shelf(freq, 0.0, sample_rate),
892 };
893 filter.set_gain_db(gain);
894
895 let expected = match filter_type {
896 FilterType::Peaking => legacy_peaking_coeffs(freq, gain, q, sample_rate),
897 FilterType::LowShelf => legacy_low_shelf_coeffs(freq, gain, sample_rate),
898 FilterType::HighShelf => legacy_high_shelf_coeffs(freq, gain, sample_rate),
899 };
900 assert_coeffs_bit_equal(&filter.coeffs, &expected);
901 }
902 }
903
904 #[test]
905 fn test_cached_geometry_rebuilds_on_sample_rate_change() {
906 let mut filter = BiquadFilter::peaking(1000.0, 6.0, 1.0, 44_100.0);
907 filter.set_sample_rate(96_000.0);
908 filter.set_gain_db(6.0);
909
910 let expected = legacy_peaking_coeffs(1000.0, 6.0, 1.0, 96_000.0);
911 assert_coeffs_bit_equal(&filter.coeffs, &expected);
912 assert_eq!(filter.geometry.sample_rate, 96_000.0);
913 }
914
915 #[test]
916 fn test_cached_geometry_extreme_gains_stay_finite() {
917 for gain in [-20.0, -12.0, 0.0, 12.0, 20.0] {
918 for mut filter in [
919 BiquadFilter::low_shelf(40.0, 0.0, 192_000.0),
920 BiquadFilter::peaking(1000.0, 0.0, 1.0, 48_000.0),
921 BiquadFilter::high_shelf(12000.0, 0.0, 44_100.0),
922 ] {
923 filter.set_gain_db(gain);
924 assert!(filter.coeffs.b0.is_finite());
925 assert!(filter.coeffs.b1.is_finite());
926 assert!(filter.coeffs.b2.is_finite());
927 assert!(filter.coeffs.a1.is_finite());
928 assert!(filter.coeffs.a2.is_finite());
929 }
930 }
931 }
932
933 #[test]
934 fn test_band_gain_update_uses_last_applied_epsilon() {
935 let mut dl = DynamicLoudness::new(2, 48_000.0);
936
937 dl.apply_band_gain_if_changed(0, GAIN_UPDATE_EPSILON_DB * 2.0);
938 assert_eq!(dl.last_applied_gains[0], GAIN_UPDATE_EPSILON_DB * 2.0);
939
940 dl.apply_band_gain_if_changed(0, GAIN_UPDATE_EPSILON_DB * 2.5);
941 assert_eq!(dl.last_applied_gains[0], GAIN_UPDATE_EPSILON_DB * 2.0);
942
943 dl.apply_band_gain_if_changed(0, GAIN_UPDATE_EPSILON_DB * 3.5);
944 assert_eq!(dl.last_applied_gains[0], GAIN_UPDATE_EPSILON_DB * 3.5);
945 }
946
947 #[test]
948 fn test_band_gain_update_broadcasts_coefficients_to_channels() {
949 let mut dl = DynamicLoudness::new(2, 48_000.0);
950 dl.apply_band_gain_if_changed(0, 3.0);
951
952 let left = dl.filters[0][0].coeffs;
953 let right = dl.filters[1][0].coeffs;
954 assert_coeffs_bit_equal(&left, &right);
955 }
956
957 #[test]
958 fn test_identity_bands_are_inactive_and_skipped() {
959 let mut dl = DynamicLoudness::new(2, 48_000.0);
960 dl.set_volume_db(-40.0);
961 let mut buffer = vec![0.25; BLOCK_SIZE * 2];
962
963 dl.process(&mut buffer);
964
965 assert!(dl.active_bands[0]);
966 assert!(!dl.active_bands[3]);
967 assert_eq!(dl.filters[0][3].state.z1, 0.0);
968 assert_eq!(dl.filters[0][3].state.z2, 0.0);
969 assert_eq!(dl.filters[1][3].state.z1, 0.0);
970 assert_eq!(dl.filters[1][3].state.z2, 0.0);
971 }
972
973 #[test]
974 fn test_first_process_applies_band_activity_state() {
975 let mut dl = DynamicLoudness::new(2, 48_000.0);
976 dl.set_volume_db(-40.0);
977 let mut buffer = vec![0.25; BLOCK_SIZE * 2];
978
979 dl.process(&mut buffer);
980
981 assert!(dl.last_applied_gains.iter().all(|gain| gain.is_finite()));
982 assert!(!dl.active_bands[3]);
983 assert!(dl
984 .active_bands
985 .iter()
986 .enumerate()
987 .any(|(band, &active)| band != 3 && active));
988 }
989
990 #[test]
991 fn test_identity_path_applies_pregain_without_touching_filters() {
992 let mut dl = DynamicLoudness::new(2, 48_000.0);
993 dl.set_volume_db(-15.0);
994 let input = vec![0.25, -0.5, 0.125, -0.25];
995 let mut buffer = input.clone();
996
997 dl.process(&mut buffer);
998
999 for (actual, original) in buffer.iter().zip(input.iter()) {
1000 assert!((actual - original * dl.pre_gain_linear).abs() < 1.0e-12);
1001 }
1002 assert!(dl.active_bands.iter().all(|&active| !active));
1003 assert!(dl
1004 .filters
1005 .iter()
1006 .flatten()
1007 .all(|filter| filter.state.z1 == 0.0 && filter.state.z2 == 0.0));
1008 }
1009
1010 #[test]
1011 fn test_strength_zero_lets_active_bands_decay_to_inactive() {
1012 let mut dl = DynamicLoudness::new(2, 48_000.0);
1013 dl.set_volume_db(-40.0);
1014 let mut buffer = vec![0.25; BLOCK_SIZE * 2];
1015
1016 dl.process(&mut buffer);
1017 assert!(dl.active_bands[0]);
1018
1019 dl.set_strength(0.0);
1020 dl.process(&mut buffer);
1021 assert!(
1022 dl.active_bands[0],
1023 "strength changes should not clear active filters before smoothing catches up"
1024 );
1025
1026 for _ in 0..512 {
1027 dl.process(&mut buffer);
1028 }
1029
1030 assert!(dl.active_bands.iter().all(|&active| !active));
1031 assert!(dl.get_band_gains().iter().all(|gain| gain.abs() < 0.0001));
1032 }
1033
1034 #[test]
1035 fn test_biquad_peaking() {
1036 let mut filter = BiquadFilter::peaking(1000.0, 6.0, 1.0, 44100.0);
1037
1038 let input = vec![0.5; 100];
1040 let mut output: Vec<f64> = Vec::new();
1041
1042 for &sample in &input {
1043 output.push(filter.process(sample));
1044 }
1045
1046 let steady_state = output.last().unwrap();
1049 assert!(steady_state > &0.5, "Peaking filter should boost");
1050 }
1051
1052 #[test]
1053 fn test_loudness_factor_calculation() {
1054 let mut dl = DynamicLoudness::new(2, 44100.0);
1055
1056 dl.set_volume_db(-15.0);
1058 assert!((dl.loudness_factor() - 0.0).abs() < 0.01);
1059
1060 dl.set_volume_db(-25.0); assert!((dl.loudness_factor() - 0.4).abs() < 0.05);
1063
1064 dl.set_volume_db(-50.0);
1066 assert!((dl.loudness_factor() - 1.0).abs() < 0.01);
1067
1068 dl.set_volume_db(-10.0);
1070 assert!((dl.loudness_factor() - 0.0).abs() < 0.01);
1071 }
1072
1073 #[test]
1074 fn test_strength_scaling() {
1075 let mut dl = DynamicLoudness::new(2, 44100.0);
1076 dl.set_strength(0.5);
1077 dl.set_volume_db(-40.0); let mut buffer = vec![0.25; BLOCK_SIZE * 2];
1079 dl.process(&mut buffer);
1080
1081 let gains = dl.get_band_gains();
1083 assert!(
1084 gains[0] > 0.0,
1085 "Expected smoother to start moving, got {}",
1086 gains[0]
1087 );
1088 assert!(
1089 gains[0] <= 6.0 + 0.1,
1090 "Expected gain to stay within target, got {}",
1091 gains[0]
1092 );
1093 }
1094
1095 #[test]
1096 fn test_process_no_crash() {
1097 let mut dl = DynamicLoudness::new(2, 44100.0);
1098 dl.set_volume(0.1); let mut buffer = vec![0.5; 1024];
1102 dl.process(&mut buffer);
1103
1104 for &sample in &buffer {
1106 assert!(sample.is_finite());
1107 }
1108 }
1109
1110 #[test]
1111 fn test_parameter_smoother() {
1112 let mut smoother = ParameterSmoother::new(50.0, 44100.0);
1113
1114 smoother.set_target(10.0);
1115
1116 let mut current = 0.0_f64;
1118 for _ in 0..20000 {
1119 current = smoother.next_block(1);
1120 }
1121
1122 assert!((current - 10.0).abs() < 0.5);
1124 }
1125
1126 #[test]
1127 fn test_disabled_bypass() {
1128 let mut dl = DynamicLoudness::new(2, 44100.0);
1129 dl.set_enabled(false);
1130 dl.set_volume(0.1);
1131
1132 let input = vec![0.5; 100];
1133 let mut buffer = input.clone();
1134 dl.process(&mut buffer);
1135
1136 for (i, o) in input.iter().zip(buffer.iter()) {
1138 assert!((i - o).abs() < 0.0001);
1139 }
1140 }
1141
1142 #[test]
1143 fn test_fixed_filter_banks_are_allocated_per_channel() {
1144 for channels in [1, 2, 6, 8] {
1145 let dl = DynamicLoudness::new(channels, 48_000.0);
1146 assert_eq!(dl.filters.len(), channels);
1147 assert!(dl.filters.iter().all(|bank| bank.len() == LOUDNESS_BANDS_N));
1148 }
1149 }
1150
1151 #[test]
1152 fn test_reset_clears_all_filter_bank_state() {
1153 let mut dl = DynamicLoudness::new(2, 48_000.0);
1154 dl.set_volume(0.1);
1155
1156 let mut buffer = vec![0.25; 256];
1157 dl.process(&mut buffer);
1158
1159 assert!(dl
1160 .filters
1161 .iter()
1162 .flatten()
1163 .any(|filter| filter.state.z1 != 0.0 || filter.state.z2 != 0.0));
1164
1165 dl.reset();
1166
1167 assert!(dl
1168 .filters
1169 .iter()
1170 .flatten()
1171 .all(|filter| filter.state.z1 == 0.0 && filter.state.z2 == 0.0));
1172 }
1173
1174 #[test]
1175 fn test_biquad_flushes_denormals_with_audio_thread_init() {
1176 crate::runtime::audio_thread_init();
1177 if !crate::runtime::audio_thread_float_mode_is_enabled() {
1178 return;
1179 }
1180
1181 let mut filter = BiquadFilter::peaking(1000.0, 0.0, 1.0, 44100.0);
1182 let subnormal = f64::from_bits(1);
1183 filter.state.z1 = subnormal;
1184 filter.state.z2 = -subnormal;
1185 let _ = filter.process(0.0);
1186 assert_eq!(filter.state.z1, 0.0);
1187 assert_eq!(filter.state.z2, 0.0);
1188 }
1189
1190 #[test]
1191 fn test_biquad_sustained_subnormal_input_flushes_to_zero() {
1192 crate::runtime::audio_thread_init();
1193 if !crate::runtime::audio_thread_float_mode_is_enabled() {
1194 return;
1195 }
1196
1197 let mut filter = BiquadFilter::peaking(1000.0, 6.0, 1.0, 44100.0);
1198 let subnormal = f64::from_bits(1);
1199
1200 for _ in 0..1024 {
1201 assert_eq!(filter.process(subnormal), 0.0);
1202 assert_eq!(filter.process(-subnormal), 0.0);
1203 }
1204
1205 assert_eq!(filter.state.z1, 0.0);
1206 assert_eq!(filter.state.z2, 0.0);
1207 }
1208}