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