1use serde::{Deserialize, Serialize};
7
8const VOLUME_SMOOTHING_TIME_MS: f64 = 20.0;
9const INV_U64_MAX: f64 = 1.0 / u64::MAX as f64;
10
11#[inline(always)]
17pub fn db_to_linear(db: f64) -> f64 {
18 10.0_f64.powf(db / 20.0)
19}
20
21#[inline(always)]
23pub fn linear_to_db(linear: f64) -> f64 {
24 if linear > 0.0 {
25 20.0 * linear.log10()
26 } else {
27 f64::NEG_INFINITY
28 }
29}
30
31pub struct VolumeController {
36 current: f64,
37 target: f64,
38 smoothing: f64,
39 one_minus_smoothing: f64,
40 sample_rate: u32,
41}
42
43impl VolumeController {
44 pub fn new() -> Self {
46 Self::with_sample_rate(44100)
47 }
48
49 pub fn with_sample_rate(sample_rate: u32) -> Self {
54 let smoothing_samples = (VOLUME_SMOOTHING_TIME_MS / 1000.0) * sample_rate as f64;
57 let smoothing = (-1.0 / smoothing_samples).exp();
58 let one_minus_smoothing = 1.0 - smoothing;
59
60 Self {
61 current: 1.0,
62 target: 1.0,
63 smoothing,
64 one_minus_smoothing,
65 sample_rate,
66 }
67 }
68
69 pub fn set_sample_rate(&mut self, sample_rate: u32) {
71 if sample_rate != self.sample_rate {
72 self.sample_rate = sample_rate;
73 let smoothing_samples = (VOLUME_SMOOTHING_TIME_MS / 1000.0) * sample_rate as f64;
74 self.smoothing = (-1.0 / smoothing_samples).exp();
75 self.one_minus_smoothing = 1.0 - self.smoothing;
76 }
77 }
78
79 pub fn set_target(&mut self, volume: f64) {
80 self.target = volume.clamp(0.0, 1.0);
81 }
82
83 #[inline(always)]
84 pub fn next_volume(&mut self) -> f64 {
85 self.current += (self.target - self.current) * self.one_minus_smoothing;
86 self.current
87 }
88
89 #[inline]
90 pub fn process(&mut self, buffer: &mut [f64], channels: usize) {
91 let frames = buffer.len() / channels;
92 for frame in 0..frames {
93 let vol = self.next_volume();
94 for ch in 0..channels {
95 buffer[frame * channels + ch] *= vol;
96 }
97 }
98 }
99}
100
101impl Default for VolumeController {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
110pub enum NoiseShaperCurve {
111 #[default]
114 Lipshitz5,
115
116 FWeighted9,
120
121 ModifiedE9,
124
125 ImprovedE9,
128
129 TpdfOnly,
132}
133
134impl NoiseShaperCurve {
135 pub fn auto_select(sample_rate: u32) -> Self {
140 if sample_rate <= 50_000 {
141 Self::Lipshitz5
142 } else {
143 Self::TpdfOnly
144 }
145 }
146
147 pub fn coeffs(&self) -> [f64; 9] {
150 match self {
151 Self::Lipshitz5 => [2.033, -2.165, 1.959, -1.590, 0.6149, 0.0, 0.0, 0.0, 0.0],
154
155 Self::FWeighted9 => [
158 2.412, -3.370, 3.937, -4.174, 3.353, -2.205, 1.281, -0.569, 0.0847,
159 ],
160
161 Self::ModifiedE9 => [
163 1.662, -1.263, 0.4827, -0.2913, 0.1268, -0.1124, 0.03252, -0.01265, -0.03524,
164 ],
165
166 Self::ImprovedE9 => [
168 2.847, -4.685, 6.214, -7.184, 6.639, -5.032, 3.263, -1.632, 0.4191,
169 ],
170
171 Self::TpdfOnly => [0.0; 9],
173 }
174 }
175
176 #[inline]
177 fn active_taps(&self) -> usize {
178 match self {
179 Self::Lipshitz5 => 5,
180 Self::TpdfOnly => 0,
181 Self::FWeighted9 | Self::ModifiedE9 | Self::ImprovedE9 => 9,
182 }
183 }
184
185 pub fn is_recommended_for(&self, sample_rate: u32) -> bool {
191 match self {
192 Self::TpdfOnly => true, _ => sample_rate <= 50_000, }
195 }
196}
197
198pub struct NoiseShaper {
207 error_history: Vec<[f64; 9]>,
209 error_history_9tap: Vec<[f64; 18]>,
211 error_history_9tap_heads: Vec<usize>,
213 coeffs: [f64; 9],
215 active_taps: usize,
217 bits: u32,
219 cached_scale: f64,
221 cached_lsb: f64,
223 enabled: bool,
225 curve: NoiseShaperCurve,
227 sample_rate: u32,
229 rng_state: Vec<u64>,
234}
235
236const NOISE_SHAPER_RNG_SEED: u64 = 0x1234_5678_9ABC_DEF0;
238
239impl NoiseShaper {
240 pub fn new(channels: usize, sample_rate: u32, bits: u32) -> Self {
242 let curve = NoiseShaperCurve::auto_select(sample_rate);
243 let coeffs = curve.coeffs();
244 let bits = bits.clamp(8, 32);
245 let (cached_scale, cached_lsb) = Self::scale_for_bits(bits);
246
247 Self {
248 error_history: vec![[0.0; 9]; channels],
249 error_history_9tap: vec![[0.0; 18]; channels],
250 error_history_9tap_heads: vec![0; channels],
251 coeffs,
252 active_taps: curve.active_taps(),
253 bits,
254 cached_scale,
255 cached_lsb,
256 enabled: true,
257 curve,
258 sample_rate,
259 rng_state: (0..channels).map(Self::channel_seed).collect(),
260 }
261 }
262
263 fn channel_seed(ch: usize) -> u64 {
267 let mut z =
268 NOISE_SHAPER_RNG_SEED.wrapping_add((ch as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
269 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
270 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
271 z ^= z >> 31;
272 if z == 0 {
274 NOISE_SHAPER_RNG_SEED
275 } else {
276 z
277 }
278 }
279
280 pub fn set_enabled(&mut self, enabled: bool) {
282 self.enabled = enabled;
283 }
284
285 pub fn set_bits(&mut self, bits: u32) {
290 if bits != self.bits && (8..=32).contains(&bits) {
291 self.bits = bits;
292 let (cached_scale, cached_lsb) = Self::scale_for_bits(bits);
293 self.cached_scale = cached_scale;
294 self.cached_lsb = cached_lsb;
295 }
296 }
297
298 #[inline]
299 fn scale_for_bits(bits: u32) -> (f64, f64) {
300 let scale = 2.0_f64.powi(bits as i32 - 1);
301 (scale, 1.0 / scale)
302 }
303
304 pub fn curve(&self) -> NoiseShaperCurve {
306 self.curve
307 }
308
309 pub fn sample_rate(&self) -> u32 {
311 self.sample_rate
312 }
313
314 pub fn bits(&self) -> u32 {
316 self.bits
317 }
318
319 pub fn set_curve(&mut self, curve: NoiseShaperCurve) {
327 self.curve = curve;
328 self.coeffs = curve.coeffs();
329 self.active_taps = curve.active_taps();
330
331 for h in &mut self.error_history {
333 *h = [0.0; 9];
334 }
335 for h in &mut self.error_history_9tap {
336 *h = [0.0; 18];
337 }
338 self.error_history_9tap_heads.fill(0);
339 }
340
341 pub fn set_sample_rate(&mut self, sample_rate: u32) {
343 if sample_rate != self.sample_rate {
344 self.sample_rate = sample_rate;
345 let new_curve = NoiseShaperCurve::auto_select(sample_rate);
346 self.set_curve(new_curve);
347 }
348 }
349
350 #[inline(always)]
352 fn next_u64(&mut self, ch: usize) -> u64 {
353 let s = &mut self.rng_state[ch];
355 *s ^= *s << 13;
356 *s ^= *s >> 7;
357 *s ^= *s << 17;
358 *s
359 }
360
361 #[inline(always)]
365 fn tpdf(&mut self, ch: usize) -> f64 {
366 let r1 = self.next_u64(ch) as f64 * INV_U64_MAX;
368 let r2 = self.next_u64(ch) as f64 * INV_U64_MAX;
369 r1 - r2
371 }
372
373 #[inline(always)]
382 pub fn process_sample(&mut self, sample: f64, ch: usize) -> f64 {
383 match self.active_taps {
384 0 => self.process_sample_with_taps::<0>(sample, ch),
385 5 => self.process_sample_with_taps::<5>(sample, ch),
386 _ => self.process_sample_9tap_ring(sample, ch),
387 }
388 }
389
390 #[inline(always)]
391 fn process_sample_with_taps<const TAPS: usize>(&mut self, sample: f64, ch: usize) -> f64 {
392 if !self.enabled || ch >= self.error_history.len() {
393 return sample;
394 }
395
396 const SILENCE_THRESHOLD: f64 = 1e-6; if sample.abs() < SILENCE_THRESHOLD {
403 self.error_history[ch] = [0.0; 9];
407 return sample;
408 }
409
410 let dither = self.tpdf(ch);
414
415 let e = &mut self.error_history[ch];
417 let feedback = if TAPS == 0 {
418 0.0
419 } else if TAPS == 5 {
420 self.coeffs[0] * e[0]
421 + self.coeffs[1] * e[1]
422 + self.coeffs[2] * e[2]
423 + self.coeffs[3] * e[3]
424 + self.coeffs[4] * e[4]
425 } else {
426 self.coeffs[0] * e[0]
427 + self.coeffs[1] * e[1]
428 + self.coeffs[2] * e[2]
429 + self.coeffs[3] * e[3]
430 + self.coeffs[4] * e[4]
431 + self.coeffs[5] * e[5]
432 + self.coeffs[6] * e[6]
433 + self.coeffs[7] * e[7]
434 + self.coeffs[8] * e[8]
435 };
436
437 let x = sample * self.cached_scale + feedback;
441 let quantized = (x + dither).round();
442
443 let raw_error = x - quantized;
447 let clamped_error = raw_error.clamp(-2.0, 2.0);
448
449 if TAPS == 5 {
451 e[4] = e[3];
452 e[3] = e[2];
453 e[2] = e[1];
454 e[1] = e[0];
455 e[0] = clamped_error;
456 } else if TAPS == 9 {
457 e[8] = e[7];
458 e[7] = e[6];
459 e[6] = e[5];
460 e[5] = e[4];
461 e[4] = e[3];
462 e[3] = e[2];
463 e[2] = e[1];
464 e[1] = e[0];
465 e[0] = clamped_error;
466 }
467
468 quantized * self.cached_lsb
469 }
470
471 #[inline(always)]
472 fn process_sample_lipshitz5(&mut self, sample: f64, ch: usize) -> f64 {
473 self.process_sample_with_taps::<5>(sample, ch)
474 }
475
476 #[inline(always)]
477 fn process_sample_tpdf_only(&mut self, sample: f64, ch: usize) -> f64 {
478 self.process_sample_with_taps::<0>(sample, ch)
479 }
480
481 #[inline(always)]
482 fn process_sample_9tap(&mut self, sample: f64, ch: usize) -> f64 {
483 self.process_sample_9tap_ring(sample, ch)
484 }
485
486 #[inline(always)]
487 fn process_sample_9tap_ring(&mut self, sample: f64, ch: usize) -> f64 {
488 if !self.enabled || ch >= self.error_history_9tap.len() {
489 return sample;
490 }
491
492 const SILENCE_THRESHOLD: f64 = 1e-6;
493
494 if sample.abs() < SILENCE_THRESHOLD {
495 self.error_history_9tap[ch] = [0.0; 18];
496 self.error_history_9tap_heads[ch] = 0;
497 return sample;
498 }
499
500 let dither = self.tpdf(ch);
501 let head = self.error_history_9tap_heads[ch];
502 let e = &mut self.error_history_9tap[ch];
503 let feedback = self.coeffs[0] * e[head]
504 + self.coeffs[1] * e[head + 1]
505 + self.coeffs[2] * e[head + 2]
506 + self.coeffs[3] * e[head + 3]
507 + self.coeffs[4] * e[head + 4]
508 + self.coeffs[5] * e[head + 5]
509 + self.coeffs[6] * e[head + 6]
510 + self.coeffs[7] * e[head + 7]
511 + self.coeffs[8] * e[head + 8];
512 let x = sample * self.cached_scale + feedback;
513 let quantized = (x + dither).round();
514 let clamped_error = (x - quantized).clamp(-2.0, 2.0);
515 let next_head = if head == 0 { 8 } else { head - 1 };
516 e[next_head] = clamped_error;
517 e[next_head + 9] = clamped_error;
518 self.error_history_9tap_heads[ch] = next_head;
519
520 quantized * self.cached_lsb
521 }
522
523 pub fn process(&mut self, buffer: &mut [f64], channels: usize) {
525 if !self.enabled {
526 return;
527 }
528
529 let frames = buffer.len() / channels;
530 match self.active_taps {
531 0 => {
532 for frame in 0..frames {
533 for ch in 0..channels {
534 let idx = frame * channels + ch;
535 buffer[idx] = self.process_sample_tpdf_only(buffer[idx], ch);
536 }
537 }
538 }
539 5 => {
540 for frame in 0..frames {
541 for ch in 0..channels {
542 let idx = frame * channels + ch;
543 buffer[idx] = self.process_sample_lipshitz5(buffer[idx], ch);
544 }
545 }
546 }
547 _ => {
548 for frame in 0..frames {
549 for ch in 0..channels {
550 let idx = frame * channels + ch;
551 buffer[idx] = self.process_sample_9tap(buffer[idx], ch);
552 }
553 }
554 }
555 }
556 }
557
558 pub fn reset(&mut self) {
560 for h in &mut self.error_history {
561 *h = [0.0; 9];
562 }
563 for h in &mut self.error_history_9tap {
564 *h = [0.0; 18];
565 }
566 self.error_history_9tap_heads.fill(0);
567 for (ch, state) in self.rng_state.iter_mut().enumerate() {
569 *state = Self::channel_seed(ch);
570 }
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 fn active_history(ns: &NoiseShaper, ch: usize) -> Vec<f64> {
579 match ns.active_taps {
580 0 => Vec::new(),
581 5 => ns.error_history[ch][..5].to_vec(),
582 _ => {
583 let head = ns.error_history_9tap_heads[ch];
584 ns.error_history_9tap[ch][head..head + 9].to_vec()
585 }
586 }
587 }
588
589 fn legacy_process_sample(ns: &mut NoiseShaper, sample: f64, ch: usize) -> f64 {
590 if !ns.enabled || ch >= ns.error_history.len() {
591 return sample;
592 }
593
594 const SILENCE_THRESHOLD: f64 = 1e-6;
595
596 if sample.abs() < SILENCE_THRESHOLD {
597 ns.error_history[ch] = [0.0; 9];
598 return sample;
599 }
600
601 let dither = ns.tpdf(ch);
602 let e = &mut ns.error_history[ch];
603 let feedback: f64 = ns.coeffs.iter().zip(e.iter()).map(|(c, ei)| c * ei).sum();
604 let x = sample * ns.cached_scale + feedback;
605 let quantized = (x + dither).round();
606 let raw_error = x - quantized;
607 let clamped_error = raw_error.clamp(-2.0, 2.0);
608
609 e.copy_within(0..8, 1);
610 e[0] = clamped_error;
611
612 quantized * ns.cached_lsb
613 }
614
615 #[test]
616 fn test_tpdf_distribution() {
617 let mut ns = NoiseShaper::new(1, 44100, 24);
619 let n_samples = 100_000;
620 let mut sum = 0.0;
621 let mut sum_sq = 0.0;
622 let mut min = f64::MAX;
623 let mut max = f64::MIN;
624
625 for _ in 0..n_samples {
626 let t = ns.tpdf(0);
627 sum += t;
628 sum_sq += t * t;
629 min = min.min(t);
630 max = max.max(t);
631 }
632
633 let mean = sum / n_samples as f64;
634 let variance = sum_sq / n_samples as f64 - mean * mean;
635
636 assert!(mean.abs() < 0.01, "TPDF mean should be ~0, got {}", mean);
638 assert!(
639 (variance - 1.0 / 6.0).abs() < 0.01,
640 "TPDF variance should be ~0.1667, got {}",
641 variance
642 );
643 assert!(
644 min > -1.01 && max < 1.01,
645 "TPDF range should be (-1, 1), got [{}, {}]",
646 min,
647 max
648 );
649 }
650
651 #[test]
652 fn test_stability_with_full_scale() {
653 let mut ns = NoiseShaper::new(1, 44100, 24);
655
656 for i in 0..44100 {
657 let sample = if i % 2 == 0 { 1.0 } else { -1.0 };
659 let out = ns.process_sample(sample, 0);
660
661 assert!(out.abs() <= 1.001, "Output diverged: {}", out);
663
664 let e = &ns.error_history[0];
666 for &ei in e.iter() {
667 assert!(ei.abs() <= 2.0, "Error history exceeds clamp: {}", ei);
668 }
669 }
670 }
671
672 #[test]
673 fn test_curve_switch_clears_history() {
674 let mut ns = NoiseShaper::new(1, 44100, 24);
675
676 for i in 0..100 {
678 ns.process_sample(0.5 * (i as f64 / 100.0).sin(), 0);
679 }
680
681 let has_nonzero = ns.error_history[0].iter().any(|&e| e != 0.0);
683 assert!(has_nonzero, "Error history should have non-zero values");
684
685 ns.set_curve(NoiseShaperCurve::FWeighted9);
687
688 for &e in ns.error_history[0].iter() {
690 assert_eq!(e, 0.0, "Error history should be cleared after curve switch");
691 }
692 }
693
694 #[test]
695 fn test_curve_switch_clears_9tap_ring_history() {
696 let mut ns = NoiseShaper::new(1, 44100, 24);
697 ns.set_curve(NoiseShaperCurve::FWeighted9);
698
699 for i in 0..100 {
700 ns.process_sample(0.5 * (i as f64 / 100.0).sin(), 0);
701 }
702
703 assert!(ns.error_history_9tap[0].iter().any(|&e| e != 0.0));
704
705 ns.set_curve(NoiseShaperCurve::ImprovedE9);
706
707 assert!(ns.error_history_9tap[0].iter().all(|&e| e == 0.0));
708 assert_eq!(ns.error_history_9tap_heads[0], 0);
709 }
710
711 #[test]
712 fn test_idle_tone_free() {
713 let mut ns = NoiseShaper::new(1, 44100, 24);
716 let n_samples = 44100;
717 let mut samples = Vec::with_capacity(n_samples);
718
719 let above_threshold = 2e-6; for _ in 0..n_samples {
723 samples.push(ns.process_sample(above_threshold, 0));
724 }
725
726 let non_zero_count = samples.iter().filter(|&&x| x != 0.0).count();
728 assert!(
729 non_zero_count > n_samples / 2,
730 "Dither not working: only {}/{} samples non-zero",
731 non_zero_count,
732 n_samples
733 );
734
735 let mean = samples.iter().sum::<f64>() / n_samples as f64;
737 let variance = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n_samples as f64;
738
739 let lsb = 1.0 / 2.0_f64.powi(23);
741 assert!(
742 variance > lsb * lsb * 0.01,
743 "Variance too low ({:.2e}), possible idle tone or stuck output",
744 variance
745 );
746 }
747
748 #[test]
749 fn test_adaptive_dither_silence() {
750 let mut ns = NoiseShaper::new(1, 44100, 24);
752
753 assert_eq!(ns.process_sample(0.0, 0), 0.0);
755
756 let below_threshold = 0.5e-6; assert_eq!(ns.process_sample(below_threshold, 0), below_threshold);
759
760 ns.process_sample(1e-3, 0); let has_nonzero = ns.error_history[0].iter().any(|&e| e != 0.0);
763 assert!(has_nonzero, "Error history should be non-zero after signal");
764
765 ns.process_sample(0.0, 0);
767
768 for &e in ns.error_history[0].iter() {
770 assert_eq!(e, 0.0, "Error history should be cleared after silence");
771 }
772 }
773
774 #[test]
775 fn test_noise_shaper_unrolled_history_matches_legacy_update() {
776 for curve in [
777 NoiseShaperCurve::Lipshitz5,
778 NoiseShaperCurve::FWeighted9,
779 NoiseShaperCurve::ModifiedE9,
780 NoiseShaperCurve::ImprovedE9,
781 NoiseShaperCurve::TpdfOnly,
782 ] {
783 let mut optimized = NoiseShaper::new(2, 44100, 16);
784 let mut legacy = NoiseShaper::new(2, 44100, 16);
785 optimized.set_curve(curve);
786 legacy.set_curve(curve);
787
788 for frame in 0..256 {
789 for ch in 0..2 {
790 let sample = ((frame * 2 + ch + 1) as f64 * 0.037).sin() * 0.4;
791 let optimized_out = optimized.process_sample(sample, ch);
792 let legacy_out = legacy_process_sample(&mut legacy, sample, ch);
793
794 assert_eq!(
795 optimized_out.to_bits(),
796 legacy_out.to_bits(),
797 "curve {:?}, frame {}, channel {} output mismatch",
798 curve,
799 frame,
800 ch
801 );
802 assert_eq!(
803 active_history(&optimized, ch),
804 legacy.error_history[ch][..curve.active_taps()].to_vec(),
805 "curve {:?}, frame {}, channel {} active history mismatch",
806 curve,
807 frame,
808 ch
809 );
810 }
811 }
812 }
813 }
814
815 #[test]
816 fn test_tpdf_only_does_not_update_error_history() {
817 let mut ns = NoiseShaper::new(1, 96_000, 24);
818 ns.set_curve(NoiseShaperCurve::TpdfOnly);
819
820 for i in 0..128 {
821 let sample = ((i + 1) as f64 * 0.031).sin() * 0.5;
822 let _ = ns.process_sample(sample, 0);
823 }
824
825 assert!(ns.error_history[0].iter().all(|&e| e == 0.0));
826 }
827
828 #[test]
829 fn test_noise_shaper_silence_reset_is_channel_local() {
830 let mut ns = NoiseShaper::new(2, 44100, 24);
831
832 for _ in 0..16 {
833 ns.process_sample(0.25, 0);
834 ns.process_sample(-0.25, 1);
835 }
836 assert!(ns.error_history[0].iter().any(|&e| e != 0.0));
837 assert!(ns.error_history[1].iter().any(|&e| e != 0.0));
838
839 ns.process_sample(0.0, 0);
840
841 assert!(ns.error_history[0].iter().all(|&e| e == 0.0));
842 assert!(ns.error_history[1].iter().any(|&e| e != 0.0));
843 }
844
845 #[test]
846 fn test_noise_shaper_9tap_ring_silence_reset_is_channel_local() {
847 let mut ns = NoiseShaper::new(2, 44100, 24);
848 ns.set_curve(NoiseShaperCurve::FWeighted9);
849
850 for _ in 0..16 {
851 ns.process_sample(0.25, 0);
852 ns.process_sample(-0.25, 1);
853 }
854 assert!(ns.error_history_9tap[0].iter().any(|&e| e != 0.0));
855 assert!(ns.error_history_9tap[1].iter().any(|&e| e != 0.0));
856
857 ns.process_sample(0.0, 0);
858
859 assert!(ns.error_history_9tap[0].iter().all(|&e| e == 0.0));
860 assert_eq!(ns.error_history_9tap_heads[0], 0);
861 assert!(ns.error_history_9tap[1].iter().any(|&e| e != 0.0));
862 }
863
864 #[test]
865 fn test_noise_shaper_cached_scale_updates_with_bits() {
866 let mut ns = NoiseShaper::new(1, 44100, 24);
867 assert_eq!(ns.cached_scale, 2.0_f64.powi(23));
868 assert_eq!(ns.cached_lsb, 1.0 / 2.0_f64.powi(23));
869
870 ns.set_bits(16);
871 assert_eq!(ns.bits(), 16);
872 assert_eq!(ns.cached_scale, 2.0_f64.powi(15));
873 assert_eq!(ns.cached_lsb, 1.0 / 2.0_f64.powi(15));
874
875 ns.set_bits(64);
876 assert_eq!(ns.bits(), 16);
877 assert_eq!(ns.cached_scale, 2.0_f64.powi(15));
878 assert_eq!(ns.cached_lsb, 1.0 / 2.0_f64.powi(15));
879 }
880
881 #[test]
882 fn test_channel_dither_streams_use_independent_seeds() {
883 let mut a = NoiseShaper::new(2, 44_100, 24);
885 let mut b = NoiseShaper::new(2, 44_100, 24);
886 assert_eq!(a.tpdf(0).to_bits(), b.tpdf(0).to_bits());
887
888 let mut c = NoiseShaper::new(2, 44_100, 24);
892 let ch0_first = c.tpdf(0);
893 let mut d = NoiseShaper::new(2, 44_100, 24);
894 let ch1_first = d.tpdf(1);
895 assert!(
896 (ch0_first - ch1_first).abs() > 1e-12,
897 "channel 0 and 1 must use independent seeds (got identical first draws)"
898 );
899 }
900
901 #[test]
902 fn test_volume_controller_one_minus_smoothing_updates_with_sample_rate() {
903 let mut volume = VolumeController::with_sample_rate(44_100);
904 let initial = volume.one_minus_smoothing;
905
906 volume.set_sample_rate(96_000);
907
908 assert_ne!(volume.one_minus_smoothing, initial);
909 assert_eq!(volume.one_minus_smoothing, 1.0 - volume.smoothing);
910 }
911
912 #[test]
913 fn test_all_curves_stable() {
914 for curve in [
916 NoiseShaperCurve::Lipshitz5,
917 NoiseShaperCurve::FWeighted9,
918 NoiseShaperCurve::ModifiedE9,
919 NoiseShaperCurve::ImprovedE9,
920 NoiseShaperCurve::TpdfOnly,
921 ] {
922 let mut ns = NoiseShaper::new(1, 44100, 24);
923 ns.set_curve(curve);
924
925 for i in 0..44100 {
927 let t = i as f64 / 44100.0;
928 let sample = 0.9 * (2.0 * std::f64::consts::PI * 440.0 * t).sin();
929 let out = ns.process_sample(sample, 0);
930 assert!(out.abs() <= 1.0, "Curve {:?} diverged: {}", curve, out);
931 }
932 }
933 }
934}