use std::cell::UnsafeCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use libpulse_binding::def::BufferAttr;
use libpulse_binding::sample::{Format, Spec};
use libpulse_binding::stream::Direction;
use libpulse_simple_binding::Simple;
use rustfft::FftPlanner;
use rustfft::num_complex::Complex;
const SAMPLE_RATE: u32 = 44100;
const FFT_SIZE: usize = 1024;
const READ_CHUNK: usize = 1024;
pub const SPECTRUM_SIZE: usize = FFT_SIZE / 2;
pub struct SpectrumBuffer {
bufs: [UnsafeCell<[f32; SPECTRUM_SIZE]>; 2],
active: AtomicUsize,
}
unsafe impl Sync for SpectrumBuffer {}
impl SpectrumBuffer {
fn new() -> Self {
Self {
bufs: [
UnsafeCell::new([0.0f32; SPECTRUM_SIZE]),
UnsafeCell::new([0.0f32; SPECTRUM_SIZE]),
],
active: AtomicUsize::new(0),
}
}
pub fn read(&self) -> &[f32; SPECTRUM_SIZE] {
let idx = self.active.load(Ordering::Acquire);
unsafe { &*self.bufs[idx].get() }
}
}
pub fn spawn_capture() -> Arc<SpectrumBuffer> {
let spectrum = Arc::new(SpectrumBuffer::new());
let spectrum_clone = Arc::clone(&spectrum);
thread::spawn(move || {
let spec = Spec {
format: Format::S16le,
channels: 1,
rate: SAMPLE_RATE,
};
let buf_attr = BufferAttr {
maxlength: u32::MAX,
tlength: u32::MAX,
prebuf: u32::MAX,
minreq: u32::MAX,
fragsize: (READ_CHUNK * 2) as u32,
};
let source = match Simple::new(
None,
"zik",
Direction::Record,
Some("@DEFAULT_MONITOR@"),
"visualizer",
&spec,
None,
Some(&buf_attr),
) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to open PulseAudio monitor: {e}");
return;
}
};
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(FFT_SIZE);
let window: Vec<f32> = (0..FFT_SIZE)
.map(|i| {
0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (FFT_SIZE - 1) as f32).cos())
})
.collect();
let fft_norm = 10.0 * (FFT_SIZE as f32).log10();
let mut ring = vec![0.0f32; FFT_SIZE];
let mut ring_pos: usize = 0;
let mut raw_buf = vec![0u8; READ_CHUNK * 2];
let mut fft_buf = vec![Complex::new(0.0f32, 0.0); FFT_SIZE];
let mut smoothed = [0.0f32; SPECTRUM_SIZE];
loop {
if source.read(&mut raw_buf).is_err() {
break;
}
for chunk in raw_buf.chunks_exact(2) {
ring[ring_pos] = i16::from_le_bytes([chunk[0], chunk[1]]) as f32 / 32768.0;
ring_pos = (ring_pos + 1) % FFT_SIZE;
}
for i in 0..FFT_SIZE {
let idx = (ring_pos + i) % FFT_SIZE;
fft_buf[i] = Complex::new(ring[idx] * window[i], 0.0);
}
fft.process(&mut fft_buf);
const DB_FLOOR: f32 = -60.0;
const DB_CEIL: f32 = -6.0;
let db_range = DB_CEIL - DB_FLOOR;
let active = spectrum_clone.active.load(Ordering::Relaxed);
let write_idx = 1 - active;
let write_buf = unsafe { &mut *spectrum_clone.bufs[write_idx].get() };
for i in 0..SPECTRUM_SIZE {
let power = fft_buf[i].norm_sqr();
let db = if power > 1e-12 {
10.0 * power.log10() - fft_norm
} else {
DB_FLOOR
};
let val = ((db - DB_FLOOR) / db_range).clamp(0.0, 1.0);
smoothed[i] = smoothed[i] * 0.3 + val * 0.7;
write_buf[i] = smoothed[i];
}
spectrum_clone.active.store(write_idx, Ordering::Release);
}
});
spectrum
}
pub struct BinMapper {
ranges: Vec<(usize, usize)>,
}
impl BinMapper {
pub fn new(num_bars: usize) -> Self {
let min_freq = 40.0f32;
let max_freq = 16000.0f32;
let freq_per_bin = SAMPLE_RATE as f32 / (SPECTRUM_SIZE * 2) as f32;
let half = SPECTRUM_SIZE;
let log_ratio = (max_freq / min_freq).ln();
let ranges = (0..num_bars)
.map(|bar_idx| {
let f_low = min_freq * (log_ratio * bar_idx as f32 / num_bars as f32).exp();
let f_high = min_freq * (log_ratio * (bar_idx + 1) as f32 / num_bars as f32).exp();
let lo = (f_low / freq_per_bin) as usize;
let hi = (f_high / freq_per_bin).ceil() as usize;
(lo.max(1).min(half - 1), hi.max(lo + 1).min(half))
})
.collect();
Self { ranges }
}
pub fn num_bars(&self) -> usize {
self.ranges.len()
}
pub fn bin_into(&self, spectrum: &[f32], out: &mut [f32]) {
for (bar_idx, &(lo, hi)) in self.ranges.iter().enumerate() {
let sum: f32 = spectrum[lo..hi].iter().sum();
out[bar_idx] = sum / (hi - lo) as f32;
}
}
}