use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
pub struct AtomicF32(AtomicU32);
impl AtomicF32 {
pub fn new(v: f32) -> Self { Self(AtomicU32::new(v.to_bits())) }
pub fn load(&self) -> f32 { f32::from_bits(self.0.load(Ordering::Relaxed)) }
pub fn store(&self, v: f32) { self.0.store(v.to_bits(), Ordering::Relaxed); }
}
pub struct AtomicRingBuffer {
buf: Box<[f32]>,
capacity: usize,
head: AtomicU64, }
impl AtomicRingBuffer {
pub fn new(capacity: usize) -> Self {
let cap = capacity.next_power_of_two().max(256);
Self {
buf: vec![0.0f32; cap].into_boxed_slice(),
capacity: cap,
head: AtomicU64::new(0),
}
}
pub fn push_slice(&self, data: &[f32]) {
let head = self.head.load(Ordering::Relaxed) as usize;
let mask = self.capacity - 1;
for (i, &sample) in data.iter().enumerate() {
let idx = (head + i) & mask;
unsafe {
let ptr = self.buf.as_ptr() as *mut f32;
ptr.add(idx).write(sample);
}
}
self.head.store((head + data.len()) as u64, Ordering::Relaxed);
}
pub fn read_latest(&self, count: usize, out: &mut [f32]) -> usize {
let head = self.head.load(Ordering::Relaxed) as usize;
let n = count.min(self.capacity).min(out.len());
let mask = self.capacity - 1;
let start = if head >= n { head - n } else { 0 };
let actual = head.saturating_sub(start);
for i in 0..actual {
let idx = (start + i) & mask;
out[i] = self.buf[idx];
}
actual
}
pub fn capacity(&self) -> usize { self.capacity }
}
pub struct AudioVizBuffer {
pub samples: AtomicRingBuffer,
pub fft_bins: AtomicRingBuffer,
pub rms_left: AtomicF32,
pub rms_right: AtomicF32,
pub peak_left: AtomicF32,
pub peak_right: AtomicF32,
pub phase_correlation: AtomicF32,
pub audio_clock_us: AtomicU64,
pub game_tick_us: AtomicU64,
pub active_lane: AtomicU8,
pub underrun_count: AtomicU32,
}
impl AudioVizBuffer {
pub fn new(sample_capacity: usize, fft_capacity: usize) -> Self {
Self {
samples: AtomicRingBuffer::new(sample_capacity),
fft_bins: AtomicRingBuffer::new(fft_capacity),
rms_left: AtomicF32::new(0.0),
rms_right: AtomicF32::new(0.0),
peak_left: AtomicF32::new(0.0),
peak_right: AtomicF32::new(0.0),
phase_correlation: AtomicF32::new(0.0),
audio_clock_us: AtomicU64::new(0),
game_tick_us: AtomicU64::new(0),
active_lane: AtomicU8::new(0),
underrun_count: AtomicU32::new(0),
}
}
pub fn compute_and_store_rms(&self, window: usize) {
let n = window * 2; let mut buf = vec![0.0f32; n];
let read = self.samples.read_latest(n, &mut buf);
if read < 2 { return; }
let (mut sum_l, mut sum_r) = (0.0f64, 0.0f64);
let mut count = 0usize;
for chunk in buf[..read].chunks_exact(2) {
sum_l += (chunk[0] as f64) * (chunk[0] as f64);
sum_r += (chunk[1] as f64) * (chunk[1] as f64);
count += 1;
}
if count > 0 {
self.rms_left.store((sum_l / count as f64).sqrt() as f32);
self.rms_right.store((sum_r / count as f64).sqrt() as f32);
}
}
pub fn update_peak_hold(&self) {
const DECAY: f32 = 0.0003; let rms_l = self.rms_left.load();
let rms_r = self.rms_right.load();
let cur_l = self.peak_left.load();
let cur_r = self.peak_right.load();
self.peak_left.store(if rms_l > cur_l { rms_l } else { (cur_l - DECAY).max(0.0) });
self.peak_right.store(if rms_r > cur_r { rms_r } else { (cur_r - DECAY).max(0.0) });
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncStatus { Green, Yellow, Red }
pub fn sync_health(viz: &AudioVizBuffer) -> SyncStatus {
let audio = viz.audio_clock_us.load(Ordering::Relaxed);
let game = viz.game_tick_us.load(Ordering::Relaxed);
let drift = audio.abs_diff(game);
if drift < 10_000 { SyncStatus::Green }
else if drift < 20_000 { SyncStatus::Yellow }
else { SyncStatus::Red }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_buffer_push_read() {
let rb = AtomicRingBuffer::new(16);
rb.push_slice(&[1.0, 2.0, 3.0, 4.0]);
let mut out = [0.0f32; 4];
let n = rb.read_latest(4, &mut out);
assert_eq!(n, 4);
assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn ring_buffer_wraps() {
let rb = AtomicRingBuffer::new(4); rb.push_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); let mut out = [0.0f32; 4];
let n = rb.read_latest(4, &mut out);
assert_eq!(n, 4);
assert_eq!(out[2], 5.0);
assert_eq!(out[3], 6.0);
}
#[test]
fn atomic_f32_roundtrip() {
let a = AtomicF32::new(0.42);
assert!((a.load() - 0.42).abs() < 1e-6);
a.store(0.99);
assert!((a.load() - 0.99).abs() < 1e-6);
}
#[test]
fn viz_buffer_rms() {
let viz = AudioVizBuffer::new(1024, 256);
let silence = vec![0.0f32; 200];
viz.samples.push_slice(&silence);
viz.compute_and_store_rms(100);
assert_eq!(viz.rms_left.load(), 0.0);
assert_eq!(viz.rms_right.load(), 0.0);
}
#[test]
fn viz_buffer_peak_decay() {
let viz = AudioVizBuffer::new(1024, 256);
viz.peak_left.store(0.5);
viz.rms_left.store(0.1); viz.update_peak_hold();
assert!(viz.peak_left.load() < 0.5); }
#[test]
fn sync_health_green() {
let viz = AudioVizBuffer::new(256, 64);
viz.audio_clock_us.store(1_000_000, Ordering::Relaxed);
viz.game_tick_us.store(1_000_500, Ordering::Relaxed);
assert_eq!(sync_health(&viz), SyncStatus::Green);
}
#[test]
fn sync_health_red() {
let viz = AudioVizBuffer::new(256, 64);
viz.audio_clock_us.store(1_000_000, Ordering::Relaxed);
viz.game_tick_us.store(1_030_000, Ordering::Relaxed);
assert_eq!(sync_health(&viz), SyncStatus::Red);
}
}