Skip to main content

koan_core/audio/
viz.rs

1use std::sync::Arc;
2
3use parking_lot::{Mutex, RwLock};
4
5/// Default buffer size: 4096 samples covers ~93ms at 44.1kHz,
6/// enough for a 2048-point FFT window with room to spare.
7const DEFAULT_BUFFER_SIZE: usize = 4096;
8
9/// Number of spectrum bars produced by the analyzer.
10pub const NUM_BARS: usize = 48;
11
12// ── Analysis output types (used by both analyzer.rs and visualizer.rs) ───────
13
14/// The output of one analysis pass: spectrum bars, peak holds, and VU levels.
15/// Written by `VizAnalyzer` on its background thread; read by the TUI thread.
16#[derive(Clone)]
17pub struct AnalysisOutput {
18    /// Spectrum bar heights (0.0..1.0), one per bar.
19    pub spectrum: [f32; NUM_BARS],
20    /// Peak hold values (slowly decaying maxima), one per bar.
21    pub peaks: [f32; NUM_BARS],
22    /// RMS VU levels: [left, right], each 0.0..1.0.
23    pub vu_levels: [f32; 2],
24}
25
26impl Default for AnalysisOutput {
27    fn default() -> Self {
28        Self {
29            spectrum: [0.0; NUM_BARS],
30            peaks: [0.0; NUM_BARS],
31            vu_levels: [0.0; 2],
32        }
33    }
34}
35
36/// Shared, lock-protected analysis output.
37/// The background analysis thread writes here; the TUI reads a clone each frame.
38pub type SharedAnalysisOutput = Arc<Mutex<AnalysisOutput>>;
39
40// ── VizFrame / VizSnapshot (high-level UI-facing snapshot API) ────────────────
41
42/// A single frame of analysis output, ready for the UI thread.
43///
44/// Held inside `VizSnapshot` under an RwLock. The UI thread clones this in
45/// <1us (memcpy of 48 floats + 2 floats + Instant) while holding the read lock.
46#[derive(Clone)]
47pub struct VizFrame {
48    /// Spectrum bar heights (0.0..1.0), one per bar.
49    pub spectrum: [f32; NUM_BARS],
50    /// RMS VU levels: [left, right], each 0.0..1.0.
51    pub vu_levels: [f32; 2],
52    /// When this frame was computed.
53    pub timestamp: std::time::Instant,
54}
55
56impl Default for VizFrame {
57    fn default() -> Self {
58        Self {
59            spectrum: [0.0; NUM_BARS],
60            vu_levels: [0.0; 2],
61            timestamp: std::time::Instant::now(),
62        }
63    }
64}
65
66/// Thread-safe snapshot of the latest analysis frame.
67///
68/// Written by the analysis thread (~60fps), read by the UI thread every frame.
69///
70/// Lock discipline:
71/// - Writer: compute everything in thread-local scratch, then acquire write lock,
72///   swap the frame (~200B memcpy), release. Hold time <1us.
73/// - Reader (UI): acquire read lock, clone frame, release. Hold time <1us.
74///   All decay/smoothing happens on the local clone with no lock held.
75pub struct VizSnapshot {
76    inner: RwLock<VizFrame>,
77}
78
79impl VizSnapshot {
80    /// Create a new snapshot with a zeroed initial frame.
81    pub fn new() -> Arc<Self> {
82        Arc::new(Self {
83            inner: RwLock::new(VizFrame::default()),
84        })
85    }
86
87    /// Read the latest frame. Acquires read lock, clones, releases — <1us.
88    pub fn read(&self) -> VizFrame {
89        self.inner.read().clone()
90    }
91
92    /// Write a new frame. Acquires write lock, swaps, releases — <1us.
93    /// MUST only be called after all FFT computation is finished (never hold lock during FFT).
94    pub fn write(&self, frame: VizFrame) {
95        *self.inner.write() = frame;
96    }
97}
98
99impl Default for VizSnapshot {
100    fn default() -> Self {
101        Self {
102            inner: RwLock::new(VizFrame::default()),
103        }
104    }
105}
106
107// ── Raw sample snapshot (used internally by VizBuffer and VizAnalyzer) ────────
108
109/// A point-in-time snapshot of VizBuffer contents, bundling raw samples with
110/// the metadata needed to interpret them. Produced by `VizBuffer::snapshot_with_meta`.
111pub struct RawVizSnapshot {
112    /// Interleaved f32 samples, oldest first.
113    pub samples: Vec<f32>,
114    /// Channel count for de-interleaving.
115    pub channels: u16,
116    /// Sample rate in Hz.
117    pub sample_rate: u32,
118}
119
120// ── VizBuffer ────────────────────────────────────────────────────────────────
121
122/// Internal sample storage for the visualization buffer.
123struct VizSamples {
124    /// Circular buffer of interleaved f32 samples.
125    buf: Vec<f32>,
126    /// Current write position (wraps around).
127    write_pos: usize,
128    /// Channel count for de-interleaving.
129    channels: u16,
130    /// Sample rate for frequency calculations.
131    sample_rate: u32,
132}
133
134/// Shared visualization sample buffer.
135///
136/// Written by the decode thread, read by the analysis thread at ~60fps.
137/// Uses `parking_lot::Mutex` — contention is near-zero because the decode
138/// thread holds the lock for <50us per write and the analysis thread reads
139/// at 16ms intervals.
140pub struct VizBuffer {
141    samples: Mutex<VizSamples>,
142}
143
144impl VizBuffer {
145    /// Create a new visualization buffer with the default size.
146    pub fn new() -> Arc<Self> {
147        Arc::new(Self {
148            samples: Mutex::new(VizSamples {
149                buf: vec![0.0; DEFAULT_BUFFER_SIZE],
150                write_pos: 0,
151                channels: 2,
152                sample_rate: 44100,
153            }),
154        })
155    }
156
157    /// Push interleaved samples into the circular buffer.
158    ///
159    /// Called by the decode thread after each packet decode.
160    /// Updates channel count and sample rate if they differ from the
161    /// current values (happens on track boundaries).
162    pub fn push_samples(&self, samples: &[f32], channels: u16, sample_rate: u32) {
163        let mut inner = self.samples.lock();
164        inner.channels = channels;
165        inner.sample_rate = sample_rate;
166
167        let buf_len = inner.buf.len();
168        if samples.len() >= buf_len {
169            // More samples than buffer size — just copy the tail.
170            let start = samples.len() - buf_len;
171            inner.buf.copy_from_slice(&samples[start..]);
172            inner.write_pos = 0;
173        } else {
174            let pos = inner.write_pos;
175            let first = buf_len - pos;
176            if samples.len() <= first {
177                inner.buf[pos..pos + samples.len()].copy_from_slice(samples);
178                inner.write_pos = (pos + samples.len()) % buf_len;
179            } else {
180                inner.buf[pos..].copy_from_slice(&samples[..first]);
181                let remaining = samples.len() - first;
182                inner.buf[..remaining].copy_from_slice(&samples[first..]);
183                inner.write_pos = remaining;
184            }
185        }
186    }
187
188    /// Take a snapshot of the current buffer contents, ordered oldest to newest.
189    ///
190    /// Returns a contiguous `Vec<f32>` with the most recent samples in chronological order.
191    ///
192    /// Allocates a new Vec on every call. For hot paths (e.g. 60fps analysis),
193    /// prefer `snapshot_into` to reuse an existing buffer.
194    pub fn snapshot(&self) -> Vec<f32> {
195        let mut out = Vec::new();
196        self.snapshot_into(&mut out);
197        out
198    }
199
200    /// Take a snapshot into a caller-provided buffer, avoiding allocation when
201    /// the buffer already has sufficient capacity.
202    ///
203    /// The buffer is cleared and filled with the most recent samples in
204    /// chronological order (oldest to newest).
205    pub fn snapshot_into(&self, out: &mut Vec<f32>) {
206        let inner = self.samples.lock();
207        let buf_len = inner.buf.len();
208        let pos = inner.write_pos;
209        out.clear();
210        out.reserve(buf_len);
211        // Write position is where the *next* sample goes, so the oldest
212        // sample is at write_pos and the newest is at write_pos - 1.
213        out.extend_from_slice(&inner.buf[pos..]);
214        out.extend_from_slice(&inner.buf[..pos]);
215    }
216
217    /// Take a snapshot bundled with metadata (channels, sample_rate).
218    ///
219    /// Acquires the lock once to copy both samples and metadata atomically,
220    /// so the caller never sees mismatched channel/rate values.
221    pub fn snapshot_with_meta(&self) -> RawVizSnapshot {
222        let inner = self.samples.lock();
223        let buf_len = inner.buf.len();
224        let pos = inner.write_pos;
225        let mut samples = Vec::with_capacity(buf_len);
226        samples.extend_from_slice(&inner.buf[pos..]);
227        samples.extend_from_slice(&inner.buf[..pos]);
228        RawVizSnapshot {
229            samples,
230            channels: inner.channels,
231            sample_rate: inner.sample_rate,
232        }
233    }
234
235    /// Current channel count.
236    pub fn channels(&self) -> u16 {
237        self.samples.lock().channels
238    }
239
240    /// Current sample rate.
241    pub fn sample_rate(&self) -> u32 {
242        self.samples.lock().sample_rate
243    }
244}
245
246impl Default for VizBuffer {
247    fn default() -> Self {
248        // Cannot return Arc<Self> from Default, so this creates the inner value.
249        // Callers should prefer VizBuffer::new() which returns Arc<VizBuffer>.
250        Self {
251            samples: Mutex::new(VizSamples {
252                buf: vec![0.0; DEFAULT_BUFFER_SIZE],
253                write_pos: 0,
254                channels: 2,
255                sample_rate: 44100,
256            }),
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn push_and_snapshot_basic() {
267        let buf = VizBuffer::new();
268        let samples: Vec<f32> = (0..100).map(|i| i as f32).collect();
269        buf.push_samples(&samples, 2, 44100);
270
271        let snap = buf.snapshot();
272        assert_eq!(snap.len(), DEFAULT_BUFFER_SIZE);
273        // Last 100 samples should be 0..100, preceded by zeros.
274        let tail = &snap[DEFAULT_BUFFER_SIZE - 100..];
275        for (i, &val) in tail.iter().enumerate() {
276            assert_eq!(val, i as f32);
277        }
278    }
279
280    #[test]
281    fn push_wraps_around() {
282        let buf = VizBuffer::new();
283        // Fill the buffer completely.
284        let samples: Vec<f32> = (0..DEFAULT_BUFFER_SIZE as u32).map(|i| i as f32).collect();
285        buf.push_samples(&samples, 2, 44100);
286
287        // Push more to wrap.
288        let extra: Vec<f32> = (0..10).map(|i| (i + 1000) as f32).collect();
289        buf.push_samples(&extra, 2, 44100);
290
291        let snap = buf.snapshot();
292        // Newest 10 samples should be 1000..1010.
293        let tail = &snap[DEFAULT_BUFFER_SIZE - 10..];
294        for (i, &val) in tail.iter().enumerate() {
295            assert_eq!(val, (i + 1000) as f32);
296        }
297    }
298
299    #[test]
300    fn push_larger_than_buffer() {
301        let buf = VizBuffer::new();
302        let big: Vec<f32> = (0..(DEFAULT_BUFFER_SIZE + 500) as u32)
303            .map(|i| i as f32)
304            .collect();
305        buf.push_samples(&big, 2, 48000);
306
307        let snap = buf.snapshot();
308        assert_eq!(snap.len(), DEFAULT_BUFFER_SIZE);
309        // Should contain the last DEFAULT_BUFFER_SIZE samples.
310        for (i, &val) in snap.iter().enumerate() {
311            assert_eq!(val, (i + 500) as f32);
312        }
313        assert_eq!(buf.sample_rate(), 48000);
314    }
315
316    #[test]
317    fn channels_and_sample_rate() {
318        let buf = VizBuffer::new();
319        assert_eq!(buf.channels(), 2);
320        assert_eq!(buf.sample_rate(), 44100);
321
322        buf.push_samples(&[1.0, 2.0], 1, 96000);
323        assert_eq!(buf.channels(), 1);
324        assert_eq!(buf.sample_rate(), 96000);
325    }
326
327    #[test]
328    fn viz_snapshot_read_write() {
329        let snap = VizSnapshot::new();
330        let frame = snap.read();
331        assert_eq!(frame.spectrum.len(), NUM_BARS);
332        assert_eq!(frame.vu_levels, [0.0, 0.0]);
333
334        let mut new_spectrum = [0.0f32; NUM_BARS];
335        new_spectrum[5] = 0.9;
336        snap.write(VizFrame {
337            spectrum: new_spectrum,
338            vu_levels: [0.5, 0.5],
339            timestamp: std::time::Instant::now(),
340        });
341
342        let frame2 = snap.read();
343        assert!((frame2.spectrum[5] - 0.9).abs() < 0.001);
344        assert!((frame2.vu_levels[0] - 0.5).abs() < 0.001);
345    }
346}