Skip to main content

rustriff_lib/services/analyzers/
spectrum_tap.rs

1use atomic_float::AtomicF32;
2use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
3
4/// Number of most-recent samples retained for analyzer FFT snapshots.
5///
6/// Smaller values lower latency and CPU cost, but reduce low-frequency detail.
7pub const SPECTRUM_WINDOW_SIZE: usize = 512;
8
9/// Lock-free sample tap used by the analyzer view.
10///
11/// The audio worker writes samples here without taking locks. UI-side commands can
12/// snapshot the latest window and compute FFT data off the audio path.
13pub struct SpectrumTap {
14    sample_rate_hz: AtomicU32,
15    write_index: AtomicUsize,
16    samples: Vec<AtomicF32>,
17}
18
19impl SpectrumTap {
20    /// Creates a new lock-free tap with preallocated sample storage.
21    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    /// Updates sample rate metadata read by analyzer commands.
32    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    /// Returns the latest sample rate metadata.
38    pub fn sample_rate_hz(&self) -> u32 {
39        self.sample_rate_hz.load(Ordering::Relaxed)
40    }
41
42    /// Writes one processed sample into the circular tap buffer.
43    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    /// Returns a chronologically ordered snapshot of the current ring buffer.
49    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            // Push more samples than the buffer length to force at least one full wrap.
94            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}