rustriff_lib/services/analyzers/
spectrum_tap.rs1use atomic_float::AtomicF32;
2use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
3
4pub const SPECTRUM_WINDOW_SIZE: usize = 512;
8
9pub struct SpectrumTap {
14 sample_rate_hz: AtomicU32,
15 write_index: AtomicUsize,
16 samples: Vec<AtomicF32>,
17}
18
19impl SpectrumTap {
20 pub fn new(sample_rate_hz: u32) -> Self {
22 Self {
23 sample_rate_hz: AtomicU32::new(sample_rate_hz.max(1)),
24 write_index: AtomicUsize::new(0),
25 samples: (0..SPECTRUM_WINDOW_SIZE)
26 .map(|_| AtomicF32::new(0.0))
27 .collect(),
28 }
29 }
30
31 pub fn set_sample_rate_hz(&self, sample_rate_hz: u32) {
33 self.sample_rate_hz
34 .store(sample_rate_hz.max(1), Ordering::Relaxed);
35 }
36
37 pub fn sample_rate_hz(&self) -> u32 {
39 self.sample_rate_hz.load(Ordering::Relaxed)
40 }
41
42 pub fn push_sample(&self, sample: f32) {
44 let index = self.write_index.fetch_add(1, Ordering::Relaxed) % self.samples.len();
45 self.samples[index].store(sample, Ordering::Relaxed);
46 }
47
48 pub fn snapshot_window(&self) -> Vec<f32> {
50 let len = self.samples.len();
51 let newest_index = self.write_index.load(Ordering::Relaxed) % len;
52
53 (0..len)
54 .map(|i| {
55 let idx = (newest_index + i) % len;
56 self.samples[idx].load(Ordering::Relaxed)
57 })
58 .collect()
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[cfg(test)]
67 mod success_path {
68 use super::*;
69
70 #[test]
71 fn new_initializes_zeroed_window_and_sample_rate() {
72 let tap = SpectrumTap::new(48_000);
73
74 assert_eq!(tap.sample_rate_hz(), 48_000);
75 let snapshot = tap.snapshot_window();
76 assert_eq!(snapshot.len(), SPECTRUM_WINDOW_SIZE);
77 assert!(snapshot.iter().all(|sample| *sample == 0.0));
78 }
79
80 #[test]
81 fn set_sample_rate_hz_updates_metadata() {
82 let tap = SpectrumTap::new(44_100);
83
84 tap.set_sample_rate_hz(96_000);
85
86 assert_eq!(tap.sample_rate_hz(), 96_000);
87 }
88
89 #[test]
90 fn snapshot_window_preserves_chronological_order_after_wraparound() {
91 let tap = SpectrumTap::new(48_000);
92
93 for i in 0..(SPECTRUM_WINDOW_SIZE + 16) {
95 tap.push_sample(i as f32);
96 }
97
98 let snapshot = tap.snapshot_window();
99 assert_eq!(snapshot.len(), SPECTRUM_WINDOW_SIZE);
100
101 let expected_start = 16_f32;
102 let expected_end = (SPECTRUM_WINDOW_SIZE + 15) as f32;
103 assert_eq!(snapshot.first().copied(), Some(expected_start));
104 assert_eq!(snapshot.last().copied(), Some(expected_end));
105
106 for pair in snapshot.windows(2) {
107 assert!(pair[1] >= pair[0]);
108 }
109 }
110 }
111
112 #[cfg(test)]
113 mod failure_path {
114 use super::*;
115
116 #[test]
117 fn new_with_zero_sample_rate_clamps_to_one() {
118 let tap = SpectrumTap::new(0);
119 assert_eq!(tap.sample_rate_hz(), 1);
120 }
121
122 #[test]
123 fn set_sample_rate_hz_with_zero_clamps_to_one() {
124 let tap = SpectrumTap::new(48_000);
125 tap.set_sample_rate_hz(0);
126 assert_eq!(tap.sample_rate_hz(), 1);
127 }
128 }
129}