1use std::sync::{
16 atomic::{AtomicU64, Ordering},
17 Arc,
18};
19
20use arc_swap::{ArcSwap, Guard};
21use atomic_float::AtomicF64;
22
23struct SharedParams<T> {
24 current: ArcSwap<T>,
25 generation: AtomicU64,
26}
27
28impl<T: Default> SharedParams<T> {
29 fn new() -> Self {
30 Self::from_snapshot(T::default())
31 }
32}
33
34impl<T> SharedParams<T> {
35 fn from_snapshot(snapshot: T) -> Self {
36 Self {
37 current: ArcSwap::new(Arc::new(snapshot)),
38 generation: AtomicU64::new(0),
39 }
40 }
41
42 #[inline]
43 fn load(&self) -> Arc<T> {
44 self.current.load_full()
45 }
46
47 #[inline]
48 fn load_with_generation(&self) -> (Arc<T>, u64) {
49 loop {
50 let before = self.generation.load(Ordering::Acquire);
51 let current = self.current.load_full();
52 let after = self.generation.load(Ordering::Acquire);
53 if before == after {
54 return (current, after);
55 }
56 }
57 }
58
59 #[inline]
60 fn load_if_changed(&self, cached: &Arc<T>) -> Option<Arc<T>> {
61 let current = self.current.load();
62 if std::ptr::eq(&**current, Arc::as_ref(cached)) {
63 None
64 } else {
65 Some(Guard::into_inner(current))
66 }
67 }
68
69 #[inline]
70 fn load_if_changed_since(&self, cached_generation: u64) -> Option<(Arc<T>, u64)> {
71 let generation = self.generation.load(Ordering::Acquire);
72 if generation == cached_generation {
73 None
74 } else {
75 Some((self.current.load_full(), generation))
76 }
77 }
78
79 #[inline]
80 fn publish(&self, snapshot: T) {
81 self.current.store(Arc::new(snapshot));
82 self.generation.fetch_add(1, Ordering::Release);
83 }
84}
85
86impl<T: Clone> SharedParams<T> {
87 #[inline]
88 fn read(&self) -> T {
89 (*self.current.load_full()).clone()
90 }
91
92 #[inline]
93 fn update(&self, mut f: impl FnMut(&mut T)) {
94 self.current.rcu(|current| {
95 let mut snapshot = T::clone(current);
96 f(&mut snapshot);
97 snapshot
98 });
99 self.generation.fetch_add(1, Ordering::Release);
100 }
101}
102
103macro_rules! impl_default_via_new {
104 ($type:ty) => {
105 impl Default for $type {
106 fn default() -> Self {
107 Self::new()
108 }
109 }
110 };
111}
112
113macro_rules! impl_snapshot_accessors {
114 ($snapshot:ty) => {
115 #[inline]
116 pub fn load(&self) -> Arc<$snapshot> {
117 self.shared.load()
118 }
119
120 #[inline]
121 pub fn load_with_generation(&self) -> (Arc<$snapshot>, u64) {
122 self.shared.load_with_generation()
123 }
124
125 #[inline]
126 pub fn load_if_changed(&self, cached: &Arc<$snapshot>) -> Option<Arc<$snapshot>> {
127 self.shared.load_if_changed(cached)
128 }
129
130 #[inline]
131 pub fn load_if_changed_since(
132 &self,
133 cached_generation: u64,
134 ) -> Option<(Arc<$snapshot>, u64)> {
135 self.shared.load_if_changed_since(cached_generation)
136 }
137 };
138}
139
140macro_rules! impl_set_enabled_accessor {
141 () => {
142 #[inline]
143 pub fn set_enabled(&self, enabled: bool) {
144 self.shared.update(|snapshot| {
145 snapshot.enabled = enabled;
146 });
147 }
148 };
149}
150
151macro_rules! impl_enabled_reader {
152 () => {
153 #[inline]
154 pub fn is_enabled(&self) -> bool {
155 self.read().enabled
156 }
157 };
158}
159
160pub const EQ_BANDS: usize = 10;
166
167#[derive(Debug, Clone, Copy)]
169pub struct EqParamsSnapshot {
170 pub gains: [f64; EQ_BANDS],
172 pub enabled: bool,
174}
175
176impl Default for EqParamsSnapshot {
177 fn default() -> Self {
178 Self {
179 gains: [0.0; EQ_BANDS],
180 enabled: false,
181 }
182 }
183}
184
185pub struct AtomicEqParams {
187 shared: SharedParams<EqParamsSnapshot>,
188}
189
190impl AtomicEqParams {
191 pub fn new() -> Self {
193 Self {
194 shared: SharedParams::new(),
195 }
196 }
197
198 pub fn write(&self, gains: &[f64; EQ_BANDS], enabled: bool) {
200 self.shared.publish(EqParamsSnapshot {
201 gains: *gains,
202 enabled,
203 });
204 }
205
206 pub fn read(&self) -> EqParamsSnapshot {
208 self.shared.read()
209 }
210
211 impl_snapshot_accessors!(EqParamsSnapshot);
212
213 pub fn set_band_gain(&self, band: usize, gain_db: f64) {
215 if band >= EQ_BANDS {
216 return;
217 }
218 self.shared.update(|snap| {
219 snap.gains[band] = gain_db.clamp(-15.0, 15.0);
220 });
221 }
222
223 pub fn set_enabled(&self, enabled: bool) {
225 self.shared.update(|snap| {
226 snap.enabled = enabled;
227 });
228 }
229
230 impl_enabled_reader!();
232}
233
234impl_default_via_new!(AtomicEqParams);
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245#[repr(u8)]
246pub enum SaturationTypeValue {
247 #[default]
248 Tape = 0,
249 Tube = 1,
250 Transistor = 2,
251}
252
253impl From<u8> for SaturationTypeValue {
254 fn from(v: u8) -> Self {
255 match v {
256 0 => Self::Tape,
257 1 => Self::Tube,
258 2 => Self::Transistor,
259 _ => Self::default(),
260 }
261 }
262}
263
264impl From<crate::processor::SaturationType> for SaturationTypeValue {
265 fn from(st: crate::processor::SaturationType) -> Self {
266 match st {
267 crate::processor::SaturationType::Tape => Self::Tape,
268 crate::processor::SaturationType::Tube => Self::Tube,
269 crate::processor::SaturationType::Transistor => Self::Transistor,
270 }
271 }
272}
273
274impl From<SaturationTypeValue> for crate::processor::SaturationType {
275 fn from(v: SaturationTypeValue) -> Self {
276 match v {
277 SaturationTypeValue::Tape => Self::Tape,
278 SaturationTypeValue::Tube => Self::Tube,
279 SaturationTypeValue::Transistor => Self::Transistor,
280 }
281 }
282}
283
284impl From<super::dsp::NoiseShaperCurve> for u8 {
285 fn from(curve: super::dsp::NoiseShaperCurve) -> Self {
286 match curve {
287 super::dsp::NoiseShaperCurve::Lipshitz5 => 0,
288 super::dsp::NoiseShaperCurve::FWeighted9 => 1,
289 super::dsp::NoiseShaperCurve::ModifiedE9 => 2,
290 super::dsp::NoiseShaperCurve::ImprovedE9 => 3,
291 super::dsp::NoiseShaperCurve::TpdfOnly => 4,
292 }
293 }
294}
295
296impl From<u8> for super::dsp::NoiseShaperCurve {
297 fn from(value: u8) -> Self {
298 match value {
299 0 => super::dsp::NoiseShaperCurve::Lipshitz5,
300 1 => super::dsp::NoiseShaperCurve::FWeighted9,
301 2 => super::dsp::NoiseShaperCurve::ModifiedE9,
302 3 => super::dsp::NoiseShaperCurve::ImprovedE9,
303 4 => super::dsp::NoiseShaperCurve::TpdfOnly,
304 _ => super::dsp::NoiseShaperCurve::Lipshitz5,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy)]
311pub struct SaturationParamsSnapshot {
312 pub drive: f64,
313 pub threshold: f64,
314 pub mix: f64,
315 pub sat_type: SaturationTypeValue,
316 pub input_gain_db: f64,
317 pub output_gain_db: f64,
318 pub highpass_mode: bool,
319 pub highpass_cutoff: f64,
320 pub enabled: bool,
321}
322
323impl Default for SaturationParamsSnapshot {
324 fn default() -> Self {
325 Self {
326 drive: 0.25,
327 threshold: 0.88,
328 mix: 0.2,
329 sat_type: SaturationTypeValue::Tube,
330 input_gain_db: 0.0,
331 output_gain_db: 0.0,
332 highpass_mode: false,
333 highpass_cutoff: 4000.0,
334 enabled: true,
335 }
336 }
337}
338
339pub struct AtomicSaturationParams {
341 shared: SharedParams<SaturationParamsSnapshot>,
342}
343
344impl AtomicSaturationParams {
345 pub fn new() -> Self {
346 Self {
347 shared: SharedParams::new(),
348 }
349 }
350
351 #[inline]
353 pub fn set_drive(&self, drive: f64) {
354 self.shared.update(|snapshot| {
355 snapshot.drive = drive.clamp(0.0, 2.0);
356 });
357 }
358
359 #[inline]
361 pub fn set_threshold(&self, threshold: f64) {
362 self.shared.update(|snapshot| {
363 snapshot.threshold = threshold.clamp(0.0, 1.0);
364 });
365 }
366
367 #[inline]
369 pub fn set_mix(&self, mix: f64) {
370 self.shared.update(|snapshot| {
371 snapshot.mix = mix.clamp(0.0, 1.0);
372 });
373 }
374
375 #[inline]
377 pub fn set_sat_type(&self, sat_type: SaturationTypeValue) {
378 self.shared.update(|snapshot| {
379 snapshot.sat_type = sat_type;
380 });
381 }
382
383 #[inline]
385 pub fn set_input_gain(&self, gain_db: f64) {
386 self.shared.update(|snapshot| {
387 snapshot.input_gain_db = gain_db;
388 });
389 }
390
391 #[inline]
393 pub fn set_output_gain(&self, gain_db: f64) {
394 self.shared.update(|snapshot| {
395 snapshot.output_gain_db = gain_db;
396 });
397 }
398
399 #[inline]
401 pub fn set_highpass_mode(&self, enabled: bool) {
402 self.shared.update(|snapshot| {
403 snapshot.highpass_mode = enabled;
404 });
405 }
406
407 #[inline]
409 pub fn set_highpass_cutoff(&self, hz: f64) {
410 self.shared.update(|snapshot| {
411 snapshot.highpass_cutoff = hz.clamp(1000.0, 12000.0);
412 });
413 }
414
415 impl_set_enabled_accessor!();
416
417 #[inline]
419 pub fn read(&self) -> SaturationParamsSnapshot {
420 self.shared.read()
421 }
422
423 impl_snapshot_accessors!(SaturationParamsSnapshot);
424
425 impl_enabled_reader!();
427}
428
429impl_default_via_new!(AtomicSaturationParams);
430
431#[derive(Debug, Clone, Copy)]
437pub struct CrossfeedParamsSnapshot {
438 pub mix: f64,
439 pub cutoff_hz: f64,
440 pub enabled: bool,
441}
442
443impl Default for CrossfeedParamsSnapshot {
444 fn default() -> Self {
445 Self {
446 mix: 0.35,
447 cutoff_hz: 700.0,
448 enabled: true,
449 }
450 }
451}
452
453pub struct AtomicCrossfeedParams {
455 shared: SharedParams<CrossfeedParamsSnapshot>,
456}
457
458impl AtomicCrossfeedParams {
459 pub fn new() -> Self {
460 Self {
461 shared: SharedParams::new(),
462 }
463 }
464
465 #[inline]
466 pub fn set_mix(&self, mix: f64) {
467 self.shared.update(|snapshot| {
468 snapshot.mix = mix.clamp(0.0, 1.0);
469 });
470 }
471
472 #[inline]
473 pub fn set_cutoff(&self, hz: f64) {
474 self.shared.update(|snapshot| {
475 snapshot.cutoff_hz = hz.clamp(200.0, 2000.0);
476 });
477 }
478
479 impl_set_enabled_accessor!();
480
481 #[inline]
482 pub fn read(&self) -> CrossfeedParamsSnapshot {
483 self.shared.read()
484 }
485
486 impl_snapshot_accessors!(CrossfeedParamsSnapshot);
487
488 impl_enabled_reader!();
489}
490
491impl_default_via_new!(AtomicCrossfeedParams);
492
493#[derive(Debug, Clone, Copy)]
499pub struct PeakLimiterParamsSnapshot {
500 pub threshold_db: f64,
501 pub release_ms: f64,
502 pub enabled: bool,
503}
504
505impl Default for PeakLimiterParamsSnapshot {
506 fn default() -> Self {
507 Self {
508 threshold_db: -1.0,
509 release_ms: 150.0,
510 enabled: true,
511 }
512 }
513}
514
515pub struct AtomicPeakLimiterParams {
517 shared: SharedParams<PeakLimiterParamsSnapshot>,
518}
519
520impl AtomicPeakLimiterParams {
521 pub fn new() -> Self {
522 Self {
523 shared: SharedParams::new(),
524 }
525 }
526
527 #[inline]
528 pub fn set_threshold(&self, db: f64) {
529 self.shared.update(|snapshot| {
530 snapshot.threshold_db = db.clamp(-20.0, 0.0);
531 });
532 }
533
534 #[inline]
535 pub fn set_release(&self, ms: f64) {
536 self.shared.update(|snapshot| {
537 snapshot.release_ms = ms.clamp(10.0, 1000.0);
538 });
539 }
540
541 impl_set_enabled_accessor!();
542
543 #[inline]
544 pub fn read(&self) -> PeakLimiterParamsSnapshot {
545 self.shared.read()
546 }
547
548 impl_snapshot_accessors!(PeakLimiterParamsSnapshot);
549
550 impl_enabled_reader!();
551}
552
553impl_default_via_new!(AtomicPeakLimiterParams);
554
555#[derive(Debug, Clone, Copy)]
561pub struct VolumeParamsSnapshot {
562 pub volume: f64, pub muted: bool,
564}
565
566impl Default for VolumeParamsSnapshot {
567 fn default() -> Self {
568 Self {
569 volume: 1.0,
570 muted: false,
571 }
572 }
573}
574
575pub struct AtomicVolumeParams {
577 shared: SharedParams<VolumeParamsSnapshot>,
578}
579
580impl AtomicVolumeParams {
581 pub fn new() -> Self {
582 Self {
583 shared: SharedParams::new(),
584 }
585 }
586
587 #[inline]
589 pub fn set_volume(&self, vol: f64) {
590 self.shared.update(|snapshot| {
591 snapshot.volume = vol.clamp(0.0, 1.0);
592 });
593 }
594
595 #[inline]
597 pub fn set_muted(&self, muted: bool) {
598 self.shared.update(|snapshot| {
599 snapshot.muted = muted;
600 });
601 }
602
603 #[inline]
605 pub fn read(&self) -> VolumeParamsSnapshot {
606 self.shared.read()
607 }
608
609 impl_snapshot_accessors!(VolumeParamsSnapshot);
610
611 #[inline]
613 pub fn effective_volume(&self) -> f64 {
614 let snapshot = self.read();
615 if snapshot.muted {
616 0.0
617 } else {
618 snapshot.volume
619 }
620 }
621}
622
623impl_default_via_new!(AtomicVolumeParams);
624
625#[derive(Debug, Clone, Copy)]
631pub struct NoiseShaperParamsSnapshot {
632 pub enabled: bool,
633 pub bits: u32,
634 pub curve: super::dsp::NoiseShaperCurve,
635}
636
637impl Default for NoiseShaperParamsSnapshot {
638 fn default() -> Self {
639 Self {
640 enabled: true,
641 bits: 24,
642 curve: super::dsp::NoiseShaperCurve::Lipshitz5,
643 }
644 }
645}
646
647pub struct AtomicNoiseShaperParams {
649 shared: SharedParams<NoiseShaperParamsSnapshot>,
650}
651
652impl AtomicNoiseShaperParams {
653 pub fn new() -> Self {
654 Self {
655 shared: SharedParams::new(),
656 }
657 }
658
659 impl_set_enabled_accessor!();
660
661 #[inline]
662 pub fn set_bits(&self, bits: u32) {
663 self.shared.update(|snapshot| {
664 snapshot.bits = bits.clamp(8, 32);
665 });
666 }
667
668 #[inline]
669 pub fn set_curve(&self, curve: super::dsp::NoiseShaperCurve) {
670 self.shared.update(|snapshot| {
671 snapshot.curve = curve;
672 });
673 }
674
675 #[inline]
676 pub fn read(&self) -> NoiseShaperParamsSnapshot {
677 self.shared.read()
678 }
679
680 impl_snapshot_accessors!(NoiseShaperParamsSnapshot);
681
682 impl_enabled_reader!();
683
684 #[inline]
685 pub fn bits(&self) -> u32 {
686 self.read().bits
687 }
688
689 #[inline]
690 pub fn curve(&self) -> super::dsp::NoiseShaperCurve {
691 self.read().curve
692 }
693}
694
695impl_default_via_new!(AtomicNoiseShaperParams);
696
697#[derive(Debug, Clone, Copy)]
703pub struct DynamicLoudnessParamsSnapshot {
704 pub enabled: bool,
705 pub volume: f64,
706 pub strength: f64,
707 pub ref_volume_db: Option<f64>,
708}
709
710impl Default for DynamicLoudnessParamsSnapshot {
711 fn default() -> Self {
712 Self {
713 enabled: true,
714 volume: 1.0,
715 strength: 1.0,
716 ref_volume_db: None,
717 }
718 }
719}
720
721pub struct AtomicDynamicLoudnessParams {
723 shared: SharedParams<DynamicLoudnessParamsSnapshot>,
724}
725
726impl AtomicDynamicLoudnessParams {
727 pub fn new() -> Self {
728 Self {
729 shared: SharedParams::new(),
730 }
731 }
732
733 impl_set_enabled_accessor!();
734
735 #[inline]
736 pub fn set_volume(&self, vol: f64) {
737 self.shared.update(|snapshot| {
738 snapshot.volume = vol.clamp(0.0, 1.0);
739 snapshot.ref_volume_db = None;
740 });
741 }
742
743 #[inline]
745 pub fn set_ref_volume_db(&self, db: f64) {
746 let mut snapshot = self.shared.read();
747 if snapshot.ref_volume_db == Some(db) {
748 return;
749 }
750 snapshot.ref_volume_db = Some(db);
751 snapshot.volume = 10f64.powf(db / 20.0).clamp(0.0, 1.0);
753 self.shared.publish(snapshot);
754 }
755
756 #[inline]
758 pub fn set_strength(&self, strength: f64) {
759 self.shared.update(|snapshot| {
760 snapshot.strength = strength.clamp(0.0, 1.0);
761 });
762 }
763
764 #[inline]
765 pub fn read(&self) -> DynamicLoudnessParamsSnapshot {
766 self.shared.read()
767 }
768
769 impl_snapshot_accessors!(DynamicLoudnessParamsSnapshot);
770
771 impl_enabled_reader!();
772
773 #[inline]
775 pub fn strength(&self) -> f64 {
776 self.read().strength
777 }
778}
779
780impl_default_via_new!(AtomicDynamicLoudnessParams);
781
782pub struct AtomicDynamicLoudnessTelemetry {
787 factor: AtomicF64,
788 band_gains: [AtomicF64; 7],
789}
790
791impl AtomicDynamicLoudnessTelemetry {
792 pub fn new() -> Self {
793 Self {
794 factor: AtomicF64::new(0.0),
795 band_gains: std::array::from_fn(|_| AtomicF64::new(0.0)),
796 }
797 }
798
799 #[inline]
800 pub fn update(&self, factor: f64, band_gains: [f64; 7]) {
801 self.factor.store(factor, Ordering::Release);
802 for (dst, gain) in self.band_gains.iter().zip(band_gains.iter().copied()) {
803 dst.store(gain, Ordering::Release);
804 }
805 }
806
807 #[inline]
808 pub fn factor(&self) -> f64 {
809 self.factor.load(Ordering::Acquire)
810 }
811
812 #[inline]
813 pub fn band_gains(&self) -> [f64; 7] {
814 let _ = self.factor.load(Ordering::Acquire);
815 std::array::from_fn(|i| self.band_gains[i].load(Ordering::Relaxed))
816 }
817}
818
819impl_default_via_new!(AtomicDynamicLoudnessTelemetry);
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 #[test]
826 fn test_eq_params_write_read() {
827 let params = AtomicEqParams::new();
828 let gains = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
829
830 params.write(&gains, true);
831
832 let snapshot = params.read();
833 for (i, &g) in gains.iter().enumerate() {
834 assert!((snapshot.gains[i] - g).abs() < 1e-10);
835 }
836 assert!(snapshot.enabled);
837 }
838
839 #[test]
840 fn test_saturation_params() {
841 let params = AtomicSaturationParams::new();
842
843 params.set_drive(1.5);
844 params.set_mix(0.7);
845 params.set_enabled(true);
846
847 let snapshot = params.read();
848 assert!((snapshot.drive - 1.5).abs() < 1e-10);
849 assert!((snapshot.mix - 0.7).abs() < 1e-10);
850 assert!(snapshot.enabled);
851 }
852
853 #[test]
854 fn test_simple_param_burst_final_state_visible() {
855 let params = AtomicDynamicLoudnessParams::new();
856 for i in 0..100 {
857 params.set_volume(i as f64 / 100.0);
858 params.set_strength(1.0 - i as f64 / 100.0);
859 }
860
861 let snapshot = params.read();
862 assert!((snapshot.volume - 0.99).abs() < 1e-10);
863 assert!((snapshot.strength - 0.01).abs() < 1e-10);
864 assert!(snapshot.enabled);
865 }
866
867 #[test]
868 fn test_eq_snapshot_publication_keeps_old_and_new_consistent() {
869 let params = AtomicEqParams::new();
870 let old = params.load();
871
872 params.set_band_gain(3, 6.0);
873 let new = params.load();
874
875 assert!(!Arc::ptr_eq(&old, &new));
876 assert_eq!(old.gains, [0.0; EQ_BANDS]);
877 assert!((new.gains[3] - 6.0).abs() < 1e-10);
878 for (index, gain) in new.gains.iter().enumerate() {
879 if index != 3 {
880 assert!((*gain - 0.0).abs() < 1e-10);
881 }
882 }
883 }
884
885 #[test]
886 fn test_dynamic_loudness_ref_volume_db_skips_unchanged_publish() {
887 let params = AtomicDynamicLoudnessParams::new();
888
889 params.set_ref_volume_db(-6.0);
890 let first = params.load();
891
892 params.set_ref_volume_db(-6.0);
893 let second = params.load();
894
895 assert!(Arc::ptr_eq(&first, &second));
896 }
897
898 #[test]
899 fn test_telemetry_band_gains_round_trip() {
900 let telemetry = AtomicDynamicLoudnessTelemetry::new();
901 let gains = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
902
903 telemetry.update(0.5, gains);
904
905 assert!((telemetry.factor() - 0.5).abs() < 1e-10);
906 assert_eq!(telemetry.band_gains(), gains);
907 }
908
909 #[test]
910 fn test_volume_params_muted() {
911 let params = AtomicVolumeParams::new();
912
913 params.set_volume(0.5);
914 assert!((params.effective_volume() - 0.5).abs() < 1e-10);
915
916 params.set_muted(true);
917 assert!((params.effective_volume() - 0.0).abs() < 1e-10);
918 }
919}