this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

VQT

alice 7a8f79de 450e7ff0

+690 -1
+3
docs/README.md
··· 45 45 - Time/Trace test cart: 46 46 - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/time_trace_test.lua` 47 47 - Shows elapsed ms and emits a trace once per second to the console. 48 + - VQT test cart: 49 + - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/vqt_test.lua --audio-device "<name-substr>"` 50 + - Visualizes 120 bins (12 octaves) with a 2px per-bin bar chart; auto-toggles between raw and whitened views every ~3 seconds.
+9 -1
docs/specs/implementation_status.md
··· 42 42 - Lua APIs implemented: `fft/ffts/fftr/fftrs` with C-identical clamping/sum semantics. 43 43 - Tests: headless unit tests cover raw-peak behavior and Lua bridging; additional clamp/sum range tests added. 44 44 45 + - VQT (8k): 46 + - Kernel generation for 120 semitone-spaced bins from 19.445 Hz; Hamming window; modulated and normalized over full 8192 buffer; sparse frequency-domain kernels via magnitude threshold. 47 + - Tick-thread R2C over latest 8192 samples; per-bin sparse complex dot; magnitude scaled by 2.0. 48 + - Unwhitened: smoothed (0.3), peak-normalized [0,1]. 49 + - Whitened: log-domain envelope (width 21), subtract, exp, mixed by alpha 0.95; smoothed and peak-normalized separately. 50 + - Lua APIs implemented: `vqt/vqts/vqtr/vqtrs` and `vqtw/vqtsw/vqtrw/vqtrsw` with C-identical OOB behavior. 51 + - Tests: kernel/peak sanity, Lua bridging, whitened arrays finite. 52 + 45 53 Behavioral Notes 46 54 - Triangles: top-left inclusion; CCW orientation enforced internally; half-open bounding box prevents shared-edge double draws. 47 55 - Ellipses vs circles: fill then border may overdraw endpoints; order-dependent at axis rows (parity with TIC-80). ··· 58 66 - Audio 59 67 - `sfx`, `music`; audio mixer/synth; capture ring for analysis. 60 68 - Analysis 61 - - `vqt/vqts/vqtr/vqtrs` and whitening variants; behavior per CLAUDE.md. 69 + - (none for VQT); whitening completed. 62 70 - Sprite flags 63 71 - `fget`, `fset`. 64 72
+6
docs/testing/test_carts.md
··· 28 28 - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/time_trace_test.lua` 29 29 - Expected: On-screen elapsed ms text; a small marker toggles color every second; console prints `sec=<n>` lines via `trace()`. 30 30 31 + - `tic80_rust/assets/vqt_test.lua` 32 + - Purpose: Visualize VQT across 12 octaves (120 bins), with auto-toggle between unwhitened and whitened views. 33 + - How to run: 34 + - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/vqt_test.lua --audio-device "<name-substr>"` 35 + - Expected: 120 bars (2px each) filling the 240px width; octave grid lines at every 12 bins; legend shows current mode (`raw` vs `whitened`) toggled every ~3 seconds; peak bin outlined. 36 + 31 37 Notes 32 38 - These carts are designed for fast feedback during local development. They complement headless tests and can reveal platform quirks (devices, timing). 33 39 - Keep carts small, single-purpose, and deterministic where possible.
+56
tic80_rust/assets/vqt_test.lua
··· 1 + -- VQT visualization cart: 120 bins (12 octaves), 2px per bin across 240px 2 + -- Auto-toggles between unwhitened (vqt/vqts) and whitened (vqtw/vqtsw) every few seconds. 3 + 4 + local sw, sh = 240, 136 5 + local bins = 120 6 + local bw = 2 -- 2px per bin -> 120 * 2 = 240 7 + local top = 12 -- top margin for labels 8 + 9 + function BOOT() 10 + cls(0) 11 + end 12 + 13 + local function octave_color(bin) 14 + -- Map octave (0..11) to palette indices with a pleasant ramp 15 + local oct = math.floor(bin / 12) 16 + local palette = {6, 7, 10, 12, 14, 9, 11, 15} 17 + return palette[(oct % #palette) + 1] 18 + end 19 + 20 + function TIC() 21 + cls(0) 22 + 23 + -- Toggle whitened view every 3 seconds 24 + local ms = time() 25 + local whiten = ((ms // 3000) % 2) == 1 26 + 27 + -- Draw bars 28 + local max_v = -1 29 + local max_bin = 0 30 + for i = 0, bins - 1 do 31 + local v = whiten and vqtsw(i) or vqts(i) -- smoothed normalized 32 + if v < 0 then v = 0 end 33 + if v > 1 then v = 1 end 34 + if v > max_v then max_v = v; max_bin = i end 35 + local h = math.floor(v * (sh - (top + 8))) 36 + local x = i * bw 37 + local y = sh - h 38 + local c = octave_color(i) 39 + rect(x, y, bw - 1, h, c) 40 + end 41 + 42 + -- Highlight peak bin 43 + rectb(max_bin * bw, sh - math.floor(max_v * (sh - (top + 8))), bw - 1, math.floor(max_v * (sh - (top + 8))), 2) 44 + 45 + -- Draw octave grid lines every 12 bins 46 + for o = 0, 11 do 47 + local x = o * 12 * bw 48 + rect(x, top, 1, sh - (top + 1), 5) 49 + end 50 + 51 + -- Labels 52 + local mode = whiten and "whitened" or "raw" 53 + print("VQT (" .. mode .. ")", 2, 2, 14) 54 + print("bins: 0..119 (12 octaves)", 2, 6, 13) 55 + end 56 +
+388
tic80_rust/src/audio/vqt.rs
··· 1 + use realfft::{num_complex::Complex, RealFftPlanner, RealToComplex}; 2 + 3 + pub struct VqtKernel { 4 + pub indices: Vec<usize>, 5 + pub real: Vec<f32>, 6 + pub imag: Vec<f32>, 7 + } 8 + 9 + pub struct VQTState { 10 + // Config 11 + n: usize, // 8192 12 + half: usize, // n/2 13 + sample_rate: u32, // Hz (usually 44100) 14 + bins: usize, // 120 15 + 16 + // Rolling mono buffer (last samples) 17 + buf: Vec<f32>, 18 + write_idx: usize, 19 + filled: bool, 20 + 21 + // FFT planning and work buffers 22 + r2c: std::sync::Arc<dyn RealToComplex<f32>>, 23 + scratch: Vec<Complex<f32>>, // realfft 3.x uses Complex scratch 24 + input: Vec<f32>, 25 + spectrum: Vec<Complex<f32>>, // length half+1 26 + 27 + // Kernels (sparse frequency-domain) 28 + kernels: Vec<VqtKernel>, 29 + 30 + // Output buffers 31 + pub vqt_raw: Vec<f32>, 32 + pub vqt_sm: Vec<f32>, 33 + pub vqt_norm: Vec<f32>, 34 + // Peak normalization for raw path 35 + vqt_peak: f32, 36 + 37 + // Whitened copies 38 + pub vqt_w_raw: Vec<f32>, 39 + pub vqt_w_sm: Vec<f32>, 40 + pub vqt_w_norm: Vec<f32>, 41 + // Peak normalization for whitened path 42 + vqt_w_peak: f32, 43 + } 44 + 45 + const VQT_BINS: usize = 120; 46 + const VQT_MIN_FREQ: f32 = 19.445; // D#0/Eb0 47 + const VQT_SMOOTHING_FACTOR: f32 = 0.3; 48 + const VQT_PEAK_SMOOTH: f32 = 0.99; 49 + const VQT_PEAK_MIN: f32 = 0.0001; 50 + 51 + // Whitening params 52 + const VQT_WHITEN_WIDTH: usize = 21; // odd 53 + const VQT_WHITEN_ALPHA: f32 = 0.95; 54 + const VQT_WHITEN_EPS: f32 = 1e-6; 55 + 56 + impl VQTState { 57 + pub fn new(sample_rate: u32, rolling_capacity: usize) -> Self { 58 + let n = 8192usize; 59 + let half = n / 2; 60 + let mut planner = RealFftPlanner::<f32>::new(); 61 + let r2c = planner.plan_fft_forward(n); 62 + let input = r2c.make_input_vec(); 63 + let spectrum = r2c.make_output_vec(); 64 + let scratch = r2c.make_scratch_vec(); 65 + 66 + let mut s = Self { 67 + n, 68 + half, 69 + sample_rate, 70 + bins: VQT_BINS, 71 + buf: vec![0.0; rolling_capacity.max(n)], 72 + write_idx: 0, 73 + filled: false, 74 + r2c, 75 + scratch, 76 + input, 77 + spectrum, 78 + kernels: Vec::with_capacity(VQT_BINS), 79 + vqt_raw: vec![0.0; VQT_BINS], 80 + vqt_sm: vec![0.0; VQT_BINS], 81 + vqt_norm: vec![0.0; VQT_BINS], 82 + vqt_peak: VQT_PEAK_MIN, 83 + vqt_w_raw: vec![0.0; VQT_BINS], 84 + vqt_w_sm: vec![0.0; VQT_BINS], 85 + vqt_w_norm: vec![0.0; VQT_BINS], 86 + vqt_w_peak: VQT_PEAK_MIN, 87 + }; 88 + s.generate_kernels(); 89 + s 90 + } 91 + 92 + fn center_frequencies(&self) -> Vec<f32> { 93 + // Semitone steps from base 94 + (0..self.bins) 95 + .map(|i| VQT_MIN_FREQ * (2.0f32).powf(i as f32 / 12.0)) 96 + .collect() 97 + } 98 + 99 + fn variable_q(center: f32) -> f32 { 100 + // Port of C schedule optimized for 8k 101 + if center < 25.0 { 102 + 7.4 103 + } else if center < 30.0 { 104 + 9.2 105 + } else if center < 40.0 { 106 + 11.5 107 + } else if center < 50.0 { 108 + 14.5 109 + } else if center < 65.0 { 110 + 16.0 111 + } else if center < 160.0 { 112 + 17.0 113 + } else if center < 320.0 { 114 + 15.0 115 + } else if center < 640.0 { 116 + 13.0 117 + } else { 118 + 11.0 119 + } 120 + } 121 + 122 + fn adaptive_threshold(center: f32) -> f32 { 123 + let q = Self::variable_q(center); 124 + if q > 30.0 { 125 + 0.005 126 + } else if q > 20.0 { 127 + 0.01 128 + } else { 129 + 0.02 130 + } 131 + } 132 + 133 + fn generate_kernels(&mut self) { 134 + let centers = self.center_frequencies(); 135 + self.kernels.clear(); 136 + for &f0 in &centers { 137 + let q = Self::variable_q(f0); 138 + let mut win_len = (q * (self.sample_rate as f32) / f0).round() as usize; 139 + if win_len > self.n { 140 + win_len = self.n; 141 + } 142 + if win_len < 32 { 143 + win_len = 32; 144 + } 145 + // Time-domain kernel placed centrally in full N-length buffer 146 + let mut time = vec![0.0f32; self.n]; 147 + let start = (self.n - win_len) / 2; 148 + // Hamming window 149 + for i in 0..win_len { 150 + let w = 0.54 151 + - 0.46 * (2.0 * std::f32::consts::PI * i as f32 / (win_len as f32 - 1.0)).cos(); 152 + // Modulate with cosine (real part of complex exponential); center around N/2 153 + let idx = start + i; 154 + let phase = 155 + 2.0 * std::f32::consts::PI * f0 * ((idx as f32) - (self.n as f32 / 2.0)) 156 + / (self.sample_rate as f32); 157 + time[idx] = w * phase.cos(); 158 + } 159 + // Normalize by window length 160 + for t in &mut time { 161 + *t /= win_len as f32; 162 + } 163 + // FFT to frequency domain 164 + let mut spec = self.r2c.make_output_vec(); 165 + // reuse input buffer 166 + self.input[..self.n].copy_from_slice(&time); 167 + let mut scratch = self.r2c.make_scratch_vec(); 168 + let _ = self 169 + .r2c 170 + .process_with_scratch(&mut self.input, &mut spec, &mut scratch); 171 + 172 + // Build sparse kernel by thresholding magnitude 173 + let thr = Self::adaptive_threshold(f0); 174 + let mut idxs = Vec::new(); 175 + let mut reals = Vec::new(); 176 + let mut imags = Vec::new(); 177 + for (k, c) in spec.iter().enumerate().take(self.half + 1) { 178 + let mag = (c.re * c.re + c.im * c.im).sqrt(); 179 + if mag > thr { 180 + idxs.push(k); 181 + reals.push(c.re); 182 + imags.push(c.im); 183 + } 184 + } 185 + self.kernels.push(VqtKernel { 186 + indices: idxs, 187 + real: reals, 188 + imag: imags, 189 + }); 190 + } 191 + } 192 + 193 + pub fn ingest(&mut self, sample: f32) { 194 + self.buf[self.write_idx] = sample; 195 + self.write_idx += 1; 196 + if self.write_idx >= self.buf.len() { 197 + self.write_idx = 0; 198 + self.filled = true; 199 + } 200 + } 201 + 202 + fn copy_latest_window(&mut self) { 203 + let n = self.n; 204 + let len = self.buf.len(); 205 + let end = self.write_idx; 206 + let start = (len + end).saturating_sub(n) % len; 207 + if start + n <= len { 208 + self.input[..n].copy_from_slice(&self.buf[start..start + n]); 209 + } else { 210 + let first = len - start; 211 + self.input[..first].copy_from_slice(&self.buf[start..]); 212 + self.input[first..n].copy_from_slice(&self.buf[..(n - first)]); 213 + } 214 + } 215 + 216 + pub fn update(&mut self) { 217 + if !self.filled && self.write_idx < self.n { 218 + return; 219 + } 220 + self.copy_latest_window(); 221 + let _ = 222 + self.r2c 223 + .process_with_scratch(&mut self.input, &mut self.spectrum, &mut self.scratch); 224 + 225 + // Apply kernels 226 + for (i, ker) in self.kernels.iter().enumerate() { 227 + let mut re = 0.0f32; 228 + let mut im = 0.0f32; 229 + for (j, &idx) in ker.indices.iter().enumerate() { 230 + if idx > self.half { 231 + continue; 232 + } 233 + let c = self.spectrum[idx]; 234 + let kr = ker.real[j]; 235 + let ki = ker.imag[j]; 236 + re += c.re * kr - c.im * ki; 237 + im += c.re * ki + c.im * kr; 238 + } 239 + let mag = (re * re + im * im).sqrt() * 2.0; 240 + self.vqt_raw[i] = if mag.is_finite() && mag >= 0.0 { 241 + mag 242 + } else { 243 + 0.0 244 + }; 245 + } 246 + 247 + // Smoothing and normalization (raw path) 248 + let a = VQT_SMOOTHING_FACTOR; 249 + let mut peak = 0.0f32; 250 + for i in 0..self.bins { 251 + self.vqt_sm[i] = self.vqt_sm[i] * a + self.vqt_raw[i] * (1.0 - a); 252 + if self.vqt_sm[i] > peak { 253 + peak = self.vqt_sm[i]; 254 + } 255 + } 256 + if self.vqt_peak <= 0.0 { 257 + self.vqt_peak = VQT_PEAK_MIN; 258 + } 259 + if peak > self.vqt_peak { 260 + self.vqt_peak = peak; 261 + } else { 262 + self.vqt_peak = self.vqt_peak * VQT_PEAK_SMOOTH + peak * (1.0 - VQT_PEAK_SMOOTH); 263 + } 264 + if self.vqt_peak < VQT_PEAK_MIN { 265 + self.vqt_peak = VQT_PEAK_MIN; 266 + } 267 + let norm = 1.0 / self.vqt_peak; 268 + for i in 0..self.bins { 269 + let mut v = self.vqt_sm[i] * norm; 270 + if v > 1.0 { 271 + v = 1.0; 272 + } 273 + if !v.is_finite() { 274 + v = 0.0; 275 + } 276 + self.vqt_norm[i] = v; 277 + } 278 + 279 + // Whitening path 280 + // log domain 281 + let mut logm = vec![0.0f32; self.bins]; 282 + for (i, mslot) in logm.iter_mut().enumerate().take(self.bins) { 283 + let m = if self.vqt_raw[i].is_finite() && self.vqt_raw[i] >= 0.0 { 284 + self.vqt_raw[i] 285 + } else { 286 + 0.0 287 + }; 288 + *mslot = (m + VQT_WHITEN_EPS).ln(); 289 + } 290 + // moving average envelope 291 + let halfw = VQT_WHITEN_WIDTH / 2; 292 + let mut env = vec![0.0f32; self.bins]; 293 + for i in 0..self.bins { 294 + let start = i.saturating_sub(halfw); 295 + let end = (i + halfw).min(self.bins - 1); 296 + let mut sum = 0.0f32; 297 + let mut count = 0; 298 + for val in logm.iter().take(end + 1).skip(start) { 299 + sum += *val; 300 + count += 1; 301 + } 302 + env[i] = if count > 0 { 303 + sum / count as f32 304 + } else { 305 + logm[i] 306 + }; 307 + } 308 + // whiten and mix 309 + for i in 0..self.bins { 310 + let wlog = logm[i] - env[i]; 311 + let mut wamp = wlog.exp() - 1.0; 312 + if !wamp.is_finite() || wamp < 0.0 { 313 + wamp = 0.0; 314 + } 315 + let raw = self.vqt_raw[i]; 316 + let mut mixed = (1.0 - VQT_WHITEN_ALPHA) * raw + VQT_WHITEN_ALPHA * wamp; 317 + if !mixed.is_finite() || mixed < 0.0 { 318 + mixed = 0.0; 319 + } 320 + self.vqt_w_raw[i] = mixed; 321 + } 322 + // Smooth and normalize whitened 323 + let mut wpeak = 0.0f32; 324 + for i in 0..self.bins { 325 + self.vqt_w_sm[i] = self.vqt_w_sm[i] * a + self.vqt_w_raw[i] * (1.0 - a); 326 + if self.vqt_w_sm[i] > wpeak { 327 + wpeak = self.vqt_w_sm[i]; 328 + } 329 + } 330 + if self.vqt_w_peak <= 0.0 { 331 + self.vqt_w_peak = VQT_PEAK_MIN; 332 + } 333 + if wpeak > self.vqt_w_peak { 334 + self.vqt_w_peak = wpeak; 335 + } else { 336 + self.vqt_w_peak = self.vqt_w_peak * VQT_PEAK_SMOOTH + wpeak * (1.0 - VQT_PEAK_SMOOTH); 337 + } 338 + if self.vqt_w_peak < VQT_PEAK_MIN { 339 + self.vqt_w_peak = VQT_PEAK_MIN; 340 + } 341 + let wnorm = 1.0 / self.vqt_w_peak; 342 + for i in 0..self.bins { 343 + let mut v = self.vqt_w_sm[i] * wnorm; 344 + if v > 1.0 { 345 + v = 1.0; 346 + } 347 + if !v.is_finite() { 348 + v = 0.0; 349 + } 350 + self.vqt_w_norm[i] = v; 351 + } 352 + } 353 + 354 + pub fn bins_count(&self) -> usize { 355 + self.bins 356 + } 357 + } 358 + 359 + use parking_lot::RwLock; 360 + use std::sync::{Arc, OnceLock}; 361 + static VQT_SHARED: OnceLock<Arc<RwLock<VQTState>>> = OnceLock::new(); 362 + 363 + pub fn set_global_vqt(state: Arc<RwLock<VQTState>>) { 364 + let _ = VQT_SHARED.set(state); 365 + } 366 + 367 + pub fn get_global_vqt() -> Option<&'static Arc<RwLock<VQTState>>> { 368 + VQT_SHARED.get() 369 + } 370 + 371 + pub fn query_vqt(state: &VQTState, bin: i32, smoothing: bool, whitened: bool) -> f64 { 372 + if bin < 0 || (bin as usize) >= state.bins_count() { 373 + return 0.0; 374 + } 375 + let i = bin as usize; 376 + if !whitened { 377 + if smoothing { 378 + state.vqt_norm[i] as f64 379 + } else { 380 + // instantaneous normalized (raw divided by peak) 381 + (state.vqt_raw[i] / state.vqt_peak) as f64 382 + } 383 + } else if smoothing { 384 + state.vqt_w_norm[i] as f64 385 + } else { 386 + (state.vqt_w_raw[i] / state.vqt_w_peak) as f64 387 + } 388 + }
+1
tic80_rust/src/lib.rs
··· 13 13 pub mod audio { 14 14 pub mod capture; 15 15 pub mod fft; 16 + pub mod vqt; 16 17 }
+11
tic80_rust/src/main.rs
··· 125 125 last_print: Instant, 126 126 peak_acc: f32, 127 127 fft: Arc<RwLock<FFTState>>, 128 + vqt: Arc<RwLock<tic80_rust::audio::vqt::VQTState>>, 128 129 debug_fft: bool, 129 130 last_fft_dbg: Instant, 130 131 } ··· 148 149 audio_cap::default_ring_capacity(), 149 150 ))); 150 151 set_global_fft(fft_arc.clone()); 152 + let vqt_arc: Arc<RwLock<tic80_rust::audio::vqt::VQTState>> = 153 + Arc::new(RwLock::new(tic80_rust::audio::vqt::VQTState::new( 154 + handle.info.sample_rate, 155 + audio_cap::default_ring_capacity(), 156 + ))); 157 + tic80_rust::audio::vqt::set_global_vqt(vqt_arc.clone()); 151 158 audio_state = Some(AudioState { 152 159 _handle: handle, 153 160 cons, ··· 155 162 last_print: Instant::now(), 156 163 peak_acc: 0.0, 157 164 fft: fft_arc, 165 + vqt: vqt_arc, 158 166 debug_fft, 159 167 last_fft_dbg: Instant::now(), 160 168 }); ··· 200 208 while let Ok(s) = a.cons.pop() { 201 209 a.peak_acc = a.peak_acc.max(s.abs()); 202 210 w.ingest(s); 211 + // Also feed VQT buffer 212 + a.vqt.write().ingest(s); 203 213 } 204 214 w.update(); 215 + a.vqt.write().update(); 205 216 } 206 217 if a.debug_fft && a.last_fft_dbg.elapsed() >= Duration::from_millis(500) { 207 218 // Print a small subset of normalized bins
+123
tic80_rust/src/script/lua_runner.rs
··· 6 6 use mlua::{Function, Lua, MultiValue, RegistryKey, Result as LuaResult, Value}; 7 7 8 8 use crate::audio::fft::{get_global_fft, query_fft}; 9 + use crate::audio::vqt::get_global_vqt; 9 10 use crate::core::memory::Memory; 10 11 use crate::gfx::framebuffer::Framebuffer; 11 12 ··· 225 226 Ok(val) 226 227 })?; 227 228 globals.set("fftrs", fftrs_fn)?; 229 + 230 + // VQT APIs: vqt/vqts/vqtr/vqtrs and whitened variants vqtw/vqtsw/vqtrw/vqtrsw 231 + let vqt_fn = lua.create_function(move |_, bin: i32| { 232 + let val = if let Some(arc) = get_global_vqt() { 233 + let guard = arc.read(); 234 + // Return normalized instantaneous; align with TIC: vqt returns normalized 235 + if bin >= 0 && (bin as usize) < guard.bins_count() { 236 + guard.vqt_norm[bin as usize] as f64 237 + } else { 238 + 0.0 239 + } 240 + } else { 241 + 0.0 242 + }; 243 + Ok(val) 244 + })?; 245 + globals.set("vqt", vqt_fn)?; 246 + 247 + let vqts_fn = lua.create_function(move |_, bin: i32| { 248 + let val = if let Some(arc) = get_global_vqt() { 249 + let guard = arc.read(); 250 + if bin >= 0 && (bin as usize) < guard.bins_count() { 251 + guard.vqt_norm[bin as usize] as f64 252 + } else { 253 + 0.0 254 + } 255 + } else { 256 + 0.0 257 + }; 258 + Ok(val) 259 + })?; 260 + globals.set("vqts", vqts_fn)?; 261 + 262 + let vqtr_fn = lua.create_function(move |_, bin: i32| { 263 + let val = if let Some(arc) = get_global_vqt() { 264 + let guard = arc.read(); 265 + if bin >= 0 && (bin as usize) < guard.bins_count() { 266 + guard.vqt_raw[bin as usize] as f64 267 + } else { 268 + 0.0 269 + } 270 + } else { 271 + 0.0 272 + }; 273 + Ok(val) 274 + })?; 275 + globals.set("vqtr", vqtr_fn)?; 276 + 277 + let vqtrs_fn = lua.create_function(move |_, bin: i32| { 278 + let val = if let Some(arc) = get_global_vqt() { 279 + let guard = arc.read(); 280 + if bin >= 0 && (bin as usize) < guard.bins_count() { 281 + guard.vqt_sm[bin as usize] as f64 282 + } else { 283 + 0.0 284 + } 285 + } else { 286 + 0.0 287 + }; 288 + Ok(val) 289 + })?; 290 + globals.set("vqtrs", vqtrs_fn)?; 291 + 292 + let vqtw_fn = lua.create_function(move |_, bin: i32| { 293 + let val = if let Some(arc) = get_global_vqt() { 294 + let guard = arc.read(); 295 + if bin >= 0 && (bin as usize) < guard.bins_count() { 296 + guard.vqt_w_norm[bin as usize] as f64 297 + } else { 298 + 0.0 299 + } 300 + } else { 301 + 0.0 302 + }; 303 + Ok(val) 304 + })?; 305 + globals.set("vqtw", vqtw_fn)?; 306 + 307 + let vqtsw_fn = lua.create_function(move |_, bin: i32| { 308 + let val = if let Some(arc) = get_global_vqt() { 309 + let guard = arc.read(); 310 + if bin >= 0 && (bin as usize) < guard.bins_count() { 311 + guard.vqt_w_norm[bin as usize] as f64 312 + } else { 313 + 0.0 314 + } 315 + } else { 316 + 0.0 317 + }; 318 + Ok(val) 319 + })?; 320 + globals.set("vqtsw", vqtsw_fn)?; 321 + 322 + let vqtrw_fn = lua.create_function(move |_, bin: i32| { 323 + let val = if let Some(arc) = get_global_vqt() { 324 + let guard = arc.read(); 325 + if bin >= 0 && (bin as usize) < guard.bins_count() { 326 + guard.vqt_w_raw[bin as usize] as f64 327 + } else { 328 + 0.0 329 + } 330 + } else { 331 + 0.0 332 + }; 333 + Ok(val) 334 + })?; 335 + globals.set("vqtrw", vqtrw_fn)?; 336 + 337 + let vqtrsw_fn = lua.create_function(move |_, bin: i32| { 338 + let val = if let Some(arc) = get_global_vqt() { 339 + let guard = arc.read(); 340 + if bin >= 0 && (bin as usize) < guard.bins_count() { 341 + guard.vqt_w_sm[bin as usize] as f64 342 + } else { 343 + 0.0 344 + } 345 + } else { 346 + 0.0 347 + }; 348 + Ok(val) 349 + })?; 350 + globals.set("vqtrsw", vqtrsw_fn)?; 228 351 229 352 // trace(message, color=15) 230 353 let trace_fn = lua.create_function(move |_, args: MultiValue| {
+93
tic80_rust/tests/vqt_tests.rs
··· 1 + use std::cell::RefCell; 2 + use std::rc::Rc; 3 + use std::sync::Arc; 4 + 5 + use parking_lot::RwLock; 6 + use tic80_rust::audio::vqt::{set_global_vqt, VQTState}; 7 + use tic80_rust::core::memory::Memory; 8 + use tic80_rust::gfx::framebuffer::Framebuffer; 9 + use tic80_rust::script::lua_runner::LuaRunner; 10 + 11 + fn vqt_center_freq(bin: usize) -> f32 { 12 + 19.445f32 * (2.0f32).powf(bin as f32 / 12.0) 13 + } 14 + 15 + fn gen_sine(freq: f32, sample_rate: u32, n: usize) -> Vec<f32> { 16 + let sr = sample_rate as f32; 17 + (0..n) 18 + .map(|i| (2.0 * std::f32::consts::PI * freq * (i as f32) / sr).sin()) 19 + .collect() 20 + } 21 + 22 + #[test] 23 + fn vqt_peak_at_expected_bin() { 24 + let cap = 8192; 25 + let sr = 44_100; 26 + let mut vqt = VQTState::new(sr, cap); 27 + let bin = 48usize; // frequency near 19.445 * 2^(48/12) ~ 311 Hz 28 + let f0 = vqt_center_freq(bin); 29 + let samples = gen_sine(f0, sr, 10_000); 30 + for s in samples { 31 + vqt.ingest(s); 32 + } 33 + vqt.update(); 34 + // Check normalized peak near bin and greater than neighbors 35 + let v = vqt.vqt_norm[bin]; 36 + let vl = if bin > 0 { vqt.vqt_norm[bin - 1] } else { 0.0 }; 37 + let vr = vqt.vqt_norm[bin + 1]; 38 + assert!(v > 0.5, "expected strong normalized response at bin"); 39 + assert!(v > vl && v > vr, "expected local max at target bin"); 40 + } 41 + 42 + #[test] 43 + fn lua_vqt_reads_bin() { 44 + let cap = 8192; 45 + let sr = 44_100; 46 + let vqt = Arc::new(RwLock::new(VQTState::new(sr, cap))); 47 + let bin = 36usize; 48 + let f0 = vqt_center_freq(bin); 49 + let samples = gen_sine(f0, sr, 10_000); 50 + { 51 + let mut w = vqt.write(); 52 + for s in samples { 53 + w.ingest(s); 54 + } 55 + w.update(); 56 + } 57 + set_global_vqt(vqt); 58 + let script = format!( 59 + "\ 60 + function BOOT() cls(0) end\n\ 61 + function TIC()\n\ 62 + local v = vqt({bin})\n\ 63 + if v > 0.5 then pix(0,0,9) end\n\ 64 + end\n", 65 + bin = bin 66 + ); 67 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 68 + let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 69 + let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init"); 70 + runner.tick(); 71 + assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(9)); 72 + } 73 + 74 + #[test] 75 + fn vqt_whitened_arrays_are_finite() { 76 + let cap = 8192; 77 + let sr = 44_100; 78 + let mut vqt = VQTState::new(sr, cap); 79 + // Multi-tone to simulate broadband-ish content 80 + for i in 0..20_000 { 81 + let t = i as f32 / (sr as f32); 82 + let s = (2.0 * std::f32::consts::PI * 220.0 * t).sin() 83 + + (2.0 * std::f32::consts::PI * 880.0 * t).sin() 84 + + (2.0 * std::f32::consts::PI * 1760.0 * t).sin(); 85 + vqt.ingest(s * 0.33); 86 + } 87 + vqt.update(); 88 + for i in 0..vqt.bins_count() { 89 + assert!(vqt.vqt_w_raw[i].is_finite()); 90 + assert!(vqt.vqt_w_sm[i].is_finite()); 91 + assert!(vqt.vqt_w_norm[i].is_finite()); 92 + } 93 + }