forge_audio/
viz_buffer.rs1use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
9
10pub struct AtomicF32(AtomicU32);
12
13impl AtomicF32 {
14 pub fn new(v: f32) -> Self { Self(AtomicU32::new(v.to_bits())) }
15 pub fn load(&self) -> f32 { f32::from_bits(self.0.load(Ordering::Relaxed)) }
16 pub fn store(&self, v: f32) { self.0.store(v.to_bits(), Ordering::Relaxed); }
17}
18
19pub struct AtomicRingBuffer {
22 buf: Box<[f32]>,
23 capacity: usize,
24 head: AtomicU64, }
26
27impl AtomicRingBuffer {
28 pub fn new(capacity: usize) -> Self {
29 let cap = capacity.next_power_of_two().max(256);
30 Self {
31 buf: vec![0.0f32; cap].into_boxed_slice(),
32 capacity: cap,
33 head: AtomicU64::new(0),
34 }
35 }
36
37 pub fn push_slice(&self, data: &[f32]) {
39 let head = self.head.load(Ordering::Relaxed) as usize;
40 let mask = self.capacity - 1;
41 for (i, &sample) in data.iter().enumerate() {
42 let idx = (head + i) & mask;
43 unsafe {
45 let ptr = self.buf.as_ptr() as *mut f32;
46 ptr.add(idx).write(sample);
47 }
48 }
49 self.head.store((head + data.len()) as u64, Ordering::Relaxed);
50 }
51
52 pub fn read_latest(&self, count: usize, out: &mut [f32]) -> usize {
54 let head = self.head.load(Ordering::Relaxed) as usize;
55 let n = count.min(self.capacity).min(out.len());
56 let mask = self.capacity - 1;
57 let start = if head >= n { head - n } else { 0 };
58 let actual = head.saturating_sub(start);
59 for i in 0..actual {
60 let idx = (start + i) & mask;
61 out[i] = self.buf[idx];
62 }
63 actual
64 }
65
66 pub fn capacity(&self) -> usize { self.capacity }
67}
68
69pub struct AudioVizBuffer {
71 pub samples: AtomicRingBuffer,
73 pub fft_bins: AtomicRingBuffer,
75 pub rms_left: AtomicF32,
77 pub rms_right: AtomicF32,
78 pub peak_left: AtomicF32,
80 pub peak_right: AtomicF32,
81 pub phase_correlation: AtomicF32,
83 pub audio_clock_us: AtomicU64,
85 pub game_tick_us: AtomicU64,
87 pub active_lane: AtomicU8,
89 pub underrun_count: AtomicU32,
91}
92
93impl AudioVizBuffer {
94 pub fn new(sample_capacity: usize, fft_capacity: usize) -> Self {
95 Self {
96 samples: AtomicRingBuffer::new(sample_capacity),
97 fft_bins: AtomicRingBuffer::new(fft_capacity),
98 rms_left: AtomicF32::new(0.0),
99 rms_right: AtomicF32::new(0.0),
100 peak_left: AtomicF32::new(0.0),
101 peak_right: AtomicF32::new(0.0),
102 phase_correlation: AtomicF32::new(0.0),
103 audio_clock_us: AtomicU64::new(0),
104 game_tick_us: AtomicU64::new(0),
105 active_lane: AtomicU8::new(0),
106 underrun_count: AtomicU32::new(0),
107 }
108 }
109
110 pub fn compute_and_store_rms(&self, window: usize) {
113 let n = window * 2; let mut buf = vec![0.0f32; n];
115 let read = self.samples.read_latest(n, &mut buf);
116 if read < 2 { return; }
117 let (mut sum_l, mut sum_r) = (0.0f64, 0.0f64);
118 let mut count = 0usize;
119 for chunk in buf[..read].chunks_exact(2) {
120 sum_l += (chunk[0] as f64) * (chunk[0] as f64);
121 sum_r += (chunk[1] as f64) * (chunk[1] as f64);
122 count += 1;
123 }
124 if count > 0 {
125 self.rms_left.store((sum_l / count as f64).sqrt() as f32);
126 self.rms_right.store((sum_r / count as f64).sqrt() as f32);
127 }
128 }
129
130 pub fn update_peak_hold(&self) {
132 const DECAY: f32 = 0.0003; let rms_l = self.rms_left.load();
134 let rms_r = self.rms_right.load();
135 let cur_l = self.peak_left.load();
136 let cur_r = self.peak_right.load();
137 self.peak_left.store(if rms_l > cur_l { rms_l } else { (cur_l - DECAY).max(0.0) });
138 self.peak_right.store(if rms_r > cur_r { rms_r } else { (cur_r - DECAY).max(0.0) });
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum SyncStatus { Green, Yellow, Red }
145
146pub fn sync_health(viz: &AudioVizBuffer) -> SyncStatus {
148 let audio = viz.audio_clock_us.load(Ordering::Relaxed);
149 let game = viz.game_tick_us.load(Ordering::Relaxed);
150 let drift = audio.abs_diff(game);
151 if drift < 10_000 { SyncStatus::Green }
152 else if drift < 20_000 { SyncStatus::Yellow }
153 else { SyncStatus::Red }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn ring_buffer_push_read() {
162 let rb = AtomicRingBuffer::new(16);
163 rb.push_slice(&[1.0, 2.0, 3.0, 4.0]);
164 let mut out = [0.0f32; 4];
165 let n = rb.read_latest(4, &mut out);
166 assert_eq!(n, 4);
167 assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
168 }
169
170 #[test]
171 fn ring_buffer_wraps() {
172 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];
175 let n = rb.read_latest(4, &mut out);
176 assert_eq!(n, 4);
177 assert_eq!(out[2], 5.0);
179 assert_eq!(out[3], 6.0);
180 }
181
182 #[test]
183 fn atomic_f32_roundtrip() {
184 let a = AtomicF32::new(0.42);
185 assert!((a.load() - 0.42).abs() < 1e-6);
186 a.store(0.99);
187 assert!((a.load() - 0.99).abs() < 1e-6);
188 }
189
190 #[test]
191 fn viz_buffer_rms() {
192 let viz = AudioVizBuffer::new(1024, 256);
193 let silence = vec![0.0f32; 200];
195 viz.samples.push_slice(&silence);
196 viz.compute_and_store_rms(100);
197 assert_eq!(viz.rms_left.load(), 0.0);
198 assert_eq!(viz.rms_right.load(), 0.0);
199 }
200
201 #[test]
202 fn viz_buffer_peak_decay() {
203 let viz = AudioVizBuffer::new(1024, 256);
204 viz.peak_left.store(0.5);
205 viz.rms_left.store(0.1); viz.update_peak_hold();
207 assert!(viz.peak_left.load() < 0.5); }
209
210 #[test]
211 fn sync_health_green() {
212 let viz = AudioVizBuffer::new(256, 64);
213 viz.audio_clock_us.store(1_000_000, Ordering::Relaxed);
214 viz.game_tick_us.store(1_000_500, Ordering::Relaxed);
215 assert_eq!(sync_health(&viz), SyncStatus::Green);
216 }
217
218 #[test]
219 fn sync_health_red() {
220 let viz = AudioVizBuffer::new(256, 64);
221 viz.audio_clock_us.store(1_000_000, Ordering::Relaxed);
222 viz.game_tick_us.store(1_030_000, Ordering::Relaxed);
223 assert_eq!(sync_health(&viz), SyncStatus::Red);
224 }
225}