this repo has no description
0
fork

Configure Feed

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

FFT

alice a4618d45 e97eee9a

+840 -122
+3
AGENTS.md
··· 14 14 - Add a plan/Implementation TODOs section under the relevant spec (e.g., audio FFT/VQT) or create a new spec. 15 15 - Reference new/updated plans from AGENTS.md and the docs index. 16 16 - Keep hygiene visible: mention clippy/test status with each change. 17 + - ALWAYS format code with `cargo fmt` after changes, in addition to fixing all compiler/clippy warnings and errors. 17 18 18 19 **Context** 19 20 - **Rewrite code location:** All Rust rewrite code lives under `tic80_rust/` (crate root). Tests live in `tic80_rust/tests/`. The windowed demo binary is `tic80_rust/src/main.rs`. ··· 50 51 - Memory ops (`peek/poke` 1/2/4/8‑bit, `memcpy`, `memset`) implemented; VRAM updates reflect on screen. 51 52 - CLI loads bundled default cart or a provided `.lua` path. 52 53 - Audio capture foundation: `cpal` input stream with mono downmix into a lock‑free ring buffer (8192 samples); CLI flags `--list-audio`, `--audio-device`, `--audio-vu`, `--audio-disable`; simple VU feedback prints peak dBFS once per second. 54 + - FFT foundation: 2k R2C (`realfft`) updates on the tick thread; maintains raw/smoothed/normalized buffers with peak tracking; `--debug-fft` prints first bins; Lua APIs pending. 53 55 54 56 **Near-Term Backlog** 55 57 - FFT implementation (cpal + realfft) per `docs/specs/audio_fft_vqt.md` (see Implementation TODOs section); add headless tests and Lua `fft/ffts/fftr/fftrs`. ··· 95 97 - Implemented audio capture with `cpal`: device selection/listing, mono downmix, ring buffer. 96 98 - Added CLI: `--list-audio`, `--audio-device`, `--audio-vu`, `--audio-disable`. 97 99 - Integrated a 1s VU peak readout for manual verification. 100 + - Implemented 2k FFT analysis (realfft) with normalized/smoothed buffers and debug print flag. 98 101 - Kept clippy/tests green; documented the FFT/VQT plan and linked TODOs. 99 102 100 103 **Docs Index**
+3
docs/README.md
··· 38 38 - Load a `.lua` file: 39 39 - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/alt.lua` 40 40 - In crate dir: `cargo run -- assets/alt.lua` 41 + - Audio FFT test cart: 42 + - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/fft_test.lua --audio-device "<name-substr>"` 43 + - Add `--audio-vu` to print a 1s peak; add `--debug-fft` to print the first few bins.
+1 -1
docs/specs/audio_fft_vqt.md
··· 87 87 88 88 Status 89 89 - Phase 1 implemented: `cpal` capture + mono downmix + ring buffer + CLI flags + VU feedback. 90 - - Next: Phase 2 (FFT 2k, realfft R2C, Lua APIs, tests). 90 + - Phase 2 implemented: FFT 2k using `realfft` (tick‑thread), raw/smoothed/normalized buffers and peak tracking; optional `--debug-fft`. Lua wiring and tests next. 91 91 92 92 Phase 1: Audio (cpal) 93 93 - Device listing: Add `--list-audio` to print capture devices and default.
+9
docs/specs/implementation_status.md
··· 27 27 - `.lua` loader: First CLI arg as a `.lua` path runs external script; fallback to bundled `assets/default.lua`. 28 28 - Window title: “rustic”. 29 29 - Audio capture scaffolding: `cpal` input stream (44.1 kHz if supported), stereo→mono downmix, lock‑free ring buffer (8192 samples); CLI flags to list/select devices and optional VU meter output. 30 + - VU behavior: Peak meter observed ~-180 dBFS at silence (BlackHole 2ch on macOS), responsive under Multi‑Output device routing. 31 + 32 + Implemented (Analysis) 33 + - FFT (2k): 34 + - Real‑to‑complex transform using `realfft` over the latest 2048 samples on the tick thread. 35 + - Bins 0..1023 maintained (Nyquist dropped), magnitudes scaled by 2.0 to match C behavior. 36 + - Buffers: raw, raw‑smoothed (0.6), normalized, normalized‑smoothed, with peak tracking (`fPeakMin=0.01`, `fPeakSmooth=0.995`). 37 + - Optional `--debug-fft` prints the first 16 smoothed normalized bins periodically. 38 + - Lua APIs pending wiring (`fft/ffts/fftr/fftrs`). 30 39 31 40 Behavioral Notes 32 41 - Triangles: top-left inclusion; CCW orientation enforced internally; half-open bounding box prevents shared-edge double draws.
+67
tic80_rust/Cargo.lock
··· 1097 1097 ] 1098 1098 1099 1099 [[package]] 1100 + name = "num-complex" 1101 + version = "0.4.6" 1102 + source = "registry+https://github.com/rust-lang/crates.io-index" 1103 + checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 1104 + dependencies = [ 1105 + "num-traits", 1106 + ] 1107 + 1108 + [[package]] 1100 1109 name = "num-derive" 1101 1110 version = "0.4.2" 1102 1111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1105 1114 "proc-macro2", 1106 1115 "quote", 1107 1116 "syn 2.0.106", 1117 + ] 1118 + 1119 + [[package]] 1120 + name = "num-integer" 1121 + version = "0.1.46" 1122 + source = "registry+https://github.com/rust-lang/crates.io-index" 1123 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1124 + dependencies = [ 1125 + "num-traits", 1108 1126 ] 1109 1127 1110 1128 [[package]] ··· 1356 1374 checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" 1357 1375 1358 1376 [[package]] 1377 + name = "primal-check" 1378 + version = "0.3.4" 1379 + source = "registry+https://github.com/rust-lang/crates.io-index" 1380 + checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" 1381 + dependencies = [ 1382 + "num-integer", 1383 + ] 1384 + 1385 + [[package]] 1359 1386 name = "proc-macro-crate" 1360 1387 version = "1.3.1" 1361 1388 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1408 1435 checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 1409 1436 1410 1437 [[package]] 1438 + name = "realfft" 1439 + version = "3.5.0" 1440 + source = "registry+https://github.com/rust-lang/crates.io-index" 1441 + checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" 1442 + dependencies = [ 1443 + "rustfft", 1444 + ] 1445 + 1446 + [[package]] 1411 1447 name = "redox_syscall" 1412 1448 version = "0.3.5" 1413 1449 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1483 1519 version = "2.1.1" 1484 1520 source = "registry+https://github.com/rust-lang/crates.io-index" 1485 1521 checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1522 + 1523 + [[package]] 1524 + name = "rustfft" 1525 + version = "6.4.0" 1526 + source = "registry+https://github.com/rust-lang/crates.io-index" 1527 + checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4" 1528 + dependencies = [ 1529 + "num-complex", 1530 + "num-integer", 1531 + "num-traits", 1532 + "primal-check", 1533 + "strength_reduce", 1534 + "transpose", 1535 + ] 1486 1536 1487 1537 [[package]] 1488 1538 name = "rustix" ··· 1629 1679 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1630 1680 1631 1681 [[package]] 1682 + name = "strength_reduce" 1683 + version = "0.2.4" 1684 + source = "registry+https://github.com/rust-lang/crates.io-index" 1685 + checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" 1686 + 1687 + [[package]] 1632 1688 name = "strict-num" 1633 1689 version = "0.1.1" 1634 1690 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1694 1750 "mlua", 1695 1751 "parking_lot", 1696 1752 "pixels", 1753 + "realfft", 1697 1754 "rtrb", 1698 1755 "winit", 1699 1756 ] ··· 1738 1795 "indexmap 2.11.0", 1739 1796 "toml_datetime", 1740 1797 "winnow", 1798 + ] 1799 + 1800 + [[package]] 1801 + name = "transpose" 1802 + version = "0.2.3" 1803 + source = "registry+https://github.com/rust-lang/crates.io-index" 1804 + checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" 1805 + dependencies = [ 1806 + "num-integer", 1807 + "strength_reduce", 1741 1808 ] 1742 1809 1743 1810 [[package]]
+1
tic80_rust/Cargo.toml
··· 11 11 rtrb = "0.3" 12 12 parking_lot = "0.12" 13 13 anyhow = "1" 14 + realfft = "3"
+25
tic80_rust/assets/fft_test.lua
··· 1 + -- Simple FFT visualization test cart 2 + -- Usage: 3 + -- cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/fft_test.lua --audio-device "..." 4 + 5 + function BOOT() 6 + cls(0) 7 + end 8 + 9 + function TIC() 10 + cls(0) 11 + local bars = 32 12 + local sw, sh = 240, 136 13 + local bw = math.floor(sw / bars) 14 + for i=0,bars-1 do 15 + local v = fft(i, -1) -- normalized bin value 16 + if v < 0 then v = 0 end 17 + if v > 1 then v = 1 end 18 + local h = math.floor(v * (sh-10)) 19 + local x = i * bw 20 + local y = sh - h 21 + rect(x, y, bw-1, h, 6) 22 + end 23 + print("fft test (normalized)", 2, 2, 14, false, 1, false) 24 + end 25 +
+7 -1
tic80_rust/src/audio/capture.rs
··· 115 115 channels, 116 116 }; 117 117 118 - Ok((AudioCaptureHandle { _stream: stream, info }, cons)) 118 + Ok(( 119 + AudioCaptureHandle { 120 + _stream: stream, 121 + info, 122 + }, 123 + cons, 124 + )) 119 125 } 120 126 121 127 fn build_stream<T>(
+214
tic80_rust/src/audio/fft.rs
··· 1 + use parking_lot::RwLock; 2 + use realfft::{num_complex::Complex, RealFftPlanner, RealToComplex}; 3 + use std::sync::{Arc, OnceLock}; 4 + 5 + pub struct FFTState { 6 + // Analysis window 7 + n: usize, // 2048 8 + half: usize, // 1024 9 + // Rolling mono buffer (last samples), sized to at least n (we use 8192 for future VQT) 10 + buf: Vec<f32>, 11 + write_idx: usize, 12 + filled: bool, 13 + 14 + // FFT planning and work buffers 15 + r2c: std::sync::Arc<dyn RealToComplex<f32>>, 16 + scratch: Vec<Complex<f32>>, 17 + input: Vec<f32>, 18 + spectrum: Vec<Complex<f32>>, // length half+1 19 + 20 + // Output buffers (length half) 21 + pub fft_raw: Vec<f32>, // magnitudes (2.0 * |X[k]|) 22 + pub fft_raw_sm: Vec<f32>, // smoothed raw (factor 0.6) 23 + pub fft_data: Vec<f32>, // normalized magnitudes 24 + pub fft_sm: Vec<f32>, // smoothed normalized 25 + 26 + // Peak tracking for normalization 27 + f_peak_min: f32, // 0.01 28 + f_peak_smoothing: f32, // 0.995 29 + f_peak_value: f32, 30 + f_amplification: f32, 31 + 32 + // Smoothing factor for displayed series 33 + f_smooth_factor: f32, // 0.6 34 + } 35 + 36 + impl FFTState { 37 + pub fn new(rolling_capacity: usize) -> Self { 38 + let n = 2048usize; 39 + let half = n / 2; 40 + let mut planner = RealFftPlanner::<f32>::new(); 41 + let r2c = planner.plan_fft_forward(n); 42 + let input = r2c.make_input_vec(); 43 + let spectrum = r2c.make_output_vec(); 44 + let scratch = r2c.make_scratch_vec(); 45 + Self { 46 + n, 47 + half, 48 + buf: vec![0.0; rolling_capacity.max(n)], 49 + write_idx: 0, 50 + filled: false, 51 + r2c, 52 + scratch, 53 + input, 54 + spectrum, 55 + fft_raw: vec![0.0; half], 56 + fft_raw_sm: vec![0.0; half], 57 + fft_data: vec![0.0; half], 58 + fft_sm: vec![0.0; half], 59 + f_peak_min: 0.01, 60 + f_peak_smoothing: 0.995, 61 + f_peak_value: 0.01, 62 + f_amplification: 1.0, 63 + f_smooth_factor: 0.6, 64 + } 65 + } 66 + 67 + pub fn ingest(&mut self, sample: f32) { 68 + self.buf[self.write_idx] = sample; 69 + self.write_idx += 1; 70 + if self.write_idx >= self.buf.len() { 71 + self.write_idx = 0; 72 + self.filled = true; 73 + } 74 + } 75 + 76 + fn copy_latest_window(&mut self) { 77 + let n = self.n; 78 + let len = self.buf.len(); 79 + // Copy last n samples ending at write_idx (exclusive) 80 + let end = self.write_idx; 81 + let start = (len + end).saturating_sub(n) % len; 82 + if start + n <= len { 83 + self.input[..n].copy_from_slice(&self.buf[start..start + n]); 84 + } else { 85 + let first = len - start; 86 + self.input[..first].copy_from_slice(&self.buf[start..]); 87 + self.input[first..n].copy_from_slice(&self.buf[..(n - first)]); 88 + } 89 + } 90 + 91 + pub fn update(&mut self) { 92 + // Ensure we have at least one full window written once 93 + if !self.filled && self.write_idx < self.n { 94 + // Not enough data yet; keep buffers near zero 95 + return; 96 + } 97 + self.copy_latest_window(); 98 + // Forward R2C 99 + self.r2c 100 + .process_with_scratch(&mut self.input, &mut self.spectrum, &mut self.scratch) 101 + .ok(); 102 + 103 + // Magnitudes for 0..half-1 (drop Nyquist at index half) 104 + let mut peak_raw = self.f_peak_min; 105 + for k in 0..self.half { 106 + let c = self.spectrum[k]; 107 + let mag = (c.re * c.re + c.im * c.im).sqrt() * 2.0; 108 + self.fft_raw[k] = mag; 109 + if mag > peak_raw { 110 + peak_raw = mag; 111 + } 112 + } 113 + 114 + // Update peak smoothing and amplification 115 + if peak_raw > self.f_peak_value { 116 + self.f_peak_value = peak_raw; 117 + } else { 118 + self.f_peak_value = self.f_peak_value * self.f_peak_smoothing 119 + + peak_raw * (1.0 - self.f_peak_smoothing); 120 + } 121 + if self.f_peak_value < self.f_peak_min { 122 + self.f_peak_value = self.f_peak_min; 123 + } 124 + self.f_amplification = 1.0 / self.f_peak_value; 125 + 126 + // Smoothed raw and normalized series 127 + let a = self.f_smooth_factor; // 0.6 128 + for k in 0..self.half { 129 + let raw = self.fft_raw[k]; 130 + let raw_sm = self.fft_raw_sm[k] * a + raw * (1.0 - a); 131 + self.fft_raw_sm[k] = raw_sm; 132 + 133 + let norm = raw * self.f_amplification; 134 + let norm_sm = self.fft_sm[k] * a + norm * (1.0 - a); 135 + self.fft_data[k] = norm; 136 + self.fft_sm[k] = norm_sm; 137 + } 138 + } 139 + 140 + pub fn bins(&self) -> usize { 141 + self.half 142 + } 143 + } 144 + 145 + // Global shared FFT state for Lua API access. 146 + static FFT_SHARED: OnceLock<Arc<RwLock<FFTState>>> = OnceLock::new(); 147 + 148 + pub fn set_global_fft(state: Arc<RwLock<FFTState>>) { 149 + let _ = FFT_SHARED.set(state); 150 + } 151 + 152 + pub fn get_global_fft() -> Option<&'static Arc<RwLock<FFTState>>> { 153 + FFT_SHARED.get() 154 + } 155 + 156 + // Helper function to query FFT arrays with C-like clamping semantics. 157 + // smoothing=false => normalized (fft_data) or raw (fft_raw); 158 + // smoothing=true => normalized-smoothed (fft_sm) or raw-smoothed (fft_raw_sm). 159 + pub fn query_fft(state: &FFTState, start: i32, end: i32, smoothing: bool, raw: bool) -> f64 { 160 + let size = state.half as i32; // 1024 161 + if end == -1 { 162 + if start < 0 || start >= size { 163 + return 0.0; 164 + } 165 + let idx = start as usize; 166 + let v = if raw { 167 + if smoothing { 168 + state.fft_raw_sm[idx] 169 + } else { 170 + state.fft_raw[idx] 171 + } 172 + } else if smoothing { 173 + state.fft_sm[idx] 174 + } else { 175 + state.fft_data[idx] 176 + }; 177 + return v as f64; 178 + } 179 + // both out-of-bounds on same side => 0 180 + if (start < 0 && end < 0) || (start >= size && end >= size) { 181 + return 0.0; 182 + } 183 + let mut s = start; 184 + let mut e = end; 185 + if s < 0 { 186 + s = 0; 187 + } 188 + if s >= size { 189 + s = 0; 190 + } 191 + if e >= size { 192 + e = size - 1; 193 + } 194 + if s > e { 195 + e = s; 196 + } 197 + let mut sum = 0.0f64; 198 + for i in s..=e { 199 + let u = i as usize; 200 + let v = if raw { 201 + if smoothing { 202 + state.fft_raw_sm[u] 203 + } else { 204 + state.fft_raw[u] 205 + } 206 + } else if smoothing { 207 + state.fft_sm[u] 208 + } else { 209 + state.fft_data[u] 210 + } as f64; 211 + sum += v; 212 + } 213 + sum 214 + }
+20 -6
tic80_rust/src/core/memory.rs
··· 31 31 let mut get_px = |pi: usize| -> u8 { 32 32 if pi < w * h { 33 33 fb.pix((pi % w) as i32, (pi / w) as i32, None).unwrap_or(0) & 0x0F 34 - } else { 0 } 34 + } else { 35 + 0 36 + } 35 37 }; 36 38 let lo = get_px(p); 37 39 let hi = get_px(p + 1); ··· 69 71 } 70 72 } 71 73 72 - pub fn peek(&self, addr: usize) -> u8 { self.get_byte(addr) } 73 - pub fn poke(&mut self, addr: usize, val: u8) { self.set_byte(addr, val); } 74 + pub fn peek(&self, addr: usize) -> u8 { 75 + self.get_byte(addr) 76 + } 77 + pub fn poke(&mut self, addr: usize, val: u8) { 78 + self.set_byte(addr, val); 79 + } 74 80 75 81 // bit-packed peeks/pokes across entire 96KB (VRAM included) 76 82 pub fn peek_bits(&self, addr: usize, bits: u8) -> u8 { ··· 78 84 8 => self.peek(addr), 79 85 4 => { 80 86 let byte = self.peek(addr >> 1); 81 - if (addr & 1) == 0 { byte & 0x0F } else { (byte >> 4) & 0x0F } 87 + if (addr & 1) == 0 { 88 + byte & 0x0F 89 + } else { 90 + (byte >> 4) & 0x0F 91 + } 82 92 } 83 93 2 => { 84 94 let byte = self.peek(addr >> 2); ··· 125 135 } 126 136 127 137 pub fn memcpy(&mut self, dst: usize, src: usize, size: usize) { 128 - if size == 0 { return; } 138 + if size == 0 { 139 + return; 140 + } 129 141 // Handle overlap with correct direction 130 142 if src < dst && src + size > dst { 131 143 for i in (0..size).rev() { ··· 141 153 } 142 154 143 155 pub fn memset(&mut self, dst: usize, value: u8, size: usize) { 144 - for i in 0..size { self.set_byte(dst + i, value); } 156 + for i in 0..size { 157 + self.set_byte(dst + i, value); 158 + } 145 159 } 146 160 }
+67 -25
tic80_rust/src/gfx/framebuffer.rs
··· 143 143 144 144 // Blit to RGBA buffer for pixels 145 145 pub fn blit_to_rgba(&self, rgba: &mut [u8]) { 146 - for (px, idx) in rgba 147 - .chunks_exact_mut(4) 148 - .zip(self.idx.iter().copied()) 149 - { 146 + for (px, idx) in rgba.chunks_exact_mut(4).zip(self.idx.iter().copied()) { 150 147 let pal = &PALETTE[(idx & 0x0F) as usize]; 151 148 px.copy_from_slice(pal); 152 149 } ··· 225 222 226 223 // Circle border using 8-way symmetry (integer midpoint algorithm) 227 224 pub fn circb(&mut self, cx: i32, cy: i32, r: i32, color: u8) { 228 - if r < 0 { return; } 225 + if r < 0 { 226 + return; 227 + } 229 228 let c = color & 0x0F; 230 229 if r == 0 { 231 230 let _ = self.set_pixel(cx, cy, c); ··· 257 256 258 257 // Filled circle via horizontal spans using symmetry 259 258 pub fn circ(&mut self, cx: i32, cy: i32, r: i32, color: u8) { 260 - if r < 0 { return; } 259 + if r < 0 { 260 + return; 261 + } 261 262 let c = color & 0x0F; 262 263 if r == 0 { 263 264 let _ = self.set_pixel(cx, cy, c); ··· 284 285 } 285 286 286 287 fn hspan(&mut self, x0: i32, x1: i32, y: i32, color: u8) { 287 - if y < 0 || y as u32 >= HEIGHT { return; } 288 + if y < 0 || y as u32 >= HEIGHT { 289 + return; 290 + } 288 291 let start = x0.min(x1); 289 292 let end = x0.max(x1); 290 293 for x in start..=end { ··· 294 297 295 298 // Ellipse border using midpoint algorithm 296 299 pub fn ellib(&mut self, cx: i32, cy: i32, a: i32, b: i32, color: u8) { 297 - if a < 0 || b < 0 { return; } 300 + if a < 0 || b < 0 { 301 + return; 302 + } 298 303 let c = color & 0x0F; 299 - if a == 0 && b == 0 { let _ = self.set_pixel(cx, cy, c); return; } 304 + if a == 0 && b == 0 { 305 + let _ = self.set_pixel(cx, cy, c); 306 + return; 307 + } 300 308 301 309 let a2 = (a as i64) * (a as i64); 302 310 let b2 = (b as i64) * (b as i64); ··· 305 313 let mut y: i64 = b as i64; 306 314 let mut d = b2 - a2 * (b as i64) + a2 / 4; 307 315 while b2 * x <= a2 * y { 308 - let xx = x as i32; let yy = y as i32; 316 + let xx = x as i32; 317 + let yy = y as i32; 309 318 let _ = self.set_pixel(cx + xx, cy + yy, c); 310 319 let _ = self.set_pixel(cx - xx, cy + yy, c); 311 320 let _ = self.set_pixel(cx + xx, cy - yy, c); ··· 319 328 x += 1; 320 329 } 321 330 322 - x = a as i64; y = 0; d = a2 - b2 * (a as i64) + b2 / 4; 331 + x = a as i64; 332 + y = 0; 333 + d = a2 - b2 * (a as i64) + b2 / 4; 323 334 while a2 * y <= b2 * x { 324 - let xx = x as i32; let yy = y as i32; 335 + let xx = x as i32; 336 + let yy = y as i32; 325 337 let _ = self.set_pixel(cx + xx, cy + yy, c); 326 338 let _ = self.set_pixel(cx - xx, cy + yy, c); 327 339 let _ = self.set_pixel(cx + xx, cy - yy, c); ··· 338 350 339 351 // Filled ellipse using horizontal spans 340 352 pub fn elli(&mut self, cx: i32, cy: i32, a: i32, b: i32, color: u8) { 341 - if a < 0 || b < 0 { return; } 353 + if a < 0 || b < 0 { 354 + return; 355 + } 342 356 let c = color & 0x0F; 343 - if a == 0 && b == 0 { let _ = self.set_pixel(cx, cy, c); return; } 344 - if a == 0 { for yy in (cy - b)..=(cy + b) { let _ = self.set_pixel(cx, yy, c); } return; } 345 - if b == 0 { for xx in (cx - a)..=(cx + a) { let _ = self.set_pixel(xx, cy, c); } return; } 346 - let af = a as f32; let bf = b as f32; let bf2 = bf * bf; 357 + if a == 0 && b == 0 { 358 + let _ = self.set_pixel(cx, cy, c); 359 + return; 360 + } 361 + if a == 0 { 362 + for yy in (cy - b)..=(cy + b) { 363 + let _ = self.set_pixel(cx, yy, c); 364 + } 365 + return; 366 + } 367 + if b == 0 { 368 + for xx in (cx - a)..=(cx + a) { 369 + let _ = self.set_pixel(xx, cy, c); 370 + } 371 + return; 372 + } 373 + let af = a as f32; 374 + let bf = b as f32; 375 + let bf2 = bf * bf; 347 376 for dy in -b..=b { 348 377 let yf = dy as f32; 349 378 let t = 1.0 - (yf * yf) / bf2; 350 - if t < 0.0 { continue; } 379 + if t < 0.0 { 380 + continue; 381 + } 351 382 let xf = af * t.sqrt(); 352 383 let x = xf.floor() as i32; 353 384 self.hspan(cx - x, cx + x, cy + dy, c); ··· 387 418 let (cx2, cy2) = ((v2x as i64) * 2, (v2y as i64) * 2); 388 419 389 420 // Edge deltas 390 - let e0_dx = bx2 - ax2; let e0_dy = by2 - ay2; // v0->v1 391 - let e1_dx = cx2 - bx2; let e1_dy = cy2 - by2; // v1->v2 392 - let e2_dx = ax2 - cx2; let e2_dy = ay2 - cy2; // v2->v0 421 + let e0_dx = bx2 - ax2; 422 + let e0_dy = by2 - ay2; // v0->v1 423 + let e1_dx = cx2 - bx2; 424 + let e1_dy = cy2 - by2; // v1->v2 425 + let e2_dx = ax2 - cx2; 426 + let e2_dy = ay2 - cy2; // v2->v0 393 427 394 428 // Top-left classification 395 429 let e0_top_left = e0_dy > 0 || (e0_dy == 0 && e0_dx < 0); ··· 468 502 if mask != 0 { 469 503 // find first 1 from the left (LSB) 470 504 let mut l = 0; 471 - while l < GLYPH_W && ((mask >> l) & 1) == 0 { l += 1; } 505 + while l < GLYPH_W && ((mask >> l) & 1) == 0 { 506 + l += 1; 507 + } 472 508 // find last 1 from the left (rightmost set bit + 1) 473 509 let mut r = GLYPH_W; 474 - while r > 0 && (((mask >> (r - 1)) & 1) == 0) { r -= 1; } 475 - if l < left { left = l; } 476 - if r > right { right = r; } 510 + while r > 0 && (((mask >> (r - 1)) & 1) == 0) { 511 + r -= 1; 512 + } 513 + if l < left { 514 + left = l; 515 + } 516 + if r > right { 517 + right = r; 518 + } 477 519 } 478 520 } 479 521 let width = right.saturating_sub(left);
+1
tic80_rust/src/lib.rs
··· 12 12 13 13 pub mod audio { 14 14 pub mod capture; 15 + pub mod fft; 15 16 }
+65 -10
tic80_rust/src/main.rs
··· 10 10 use winit::event_loop::{ControlFlow, EventLoop}; 11 11 use winit::window::WindowBuilder; 12 12 13 + use parking_lot::RwLock; 14 + use std::sync::Arc; 15 + use tic80_rust::audio::capture as audio_cap; 16 + use tic80_rust::audio::fft::{set_global_fft, FFTState}; 17 + use tic80_rust::core::memory::Memory; 13 18 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 14 19 use tic80_rust::script::lua_runner::LuaRunner; 15 - use tic80_rust::core::memory::Memory; 16 - use tic80_rust::audio::capture as audio_cap; 17 20 18 21 // Simple fixed-step ticker at ~60 FPS 19 22 struct Ticker { ··· 73 76 "--audio-disable" => audio_disable = true, 74 77 "--audio-vu" => audio_vu = true, 75 78 "--audio-device" => { 76 - if let Some(val) = args_iter.next() { audio_device = Some(val); } 79 + if let Some(val) = args_iter.next() { 80 + audio_device = Some(val); 81 + } 77 82 } 78 83 other => { 79 84 if other.ends_with(".lua") && Path::new(other).is_file() { ··· 98 103 match fs::read_to_string(path) { 99 104 Ok(s) => s, 100 105 Err(e) => { 101 - eprintln!("Failed to read {}: {}. Falling back to default cart.", path, e); 106 + eprintln!( 107 + "Failed to read {}: {}. Falling back to default cart.", 108 + path, e 109 + ); 102 110 DEFAULT_LUA.to_string() 103 111 } 104 112 } ··· 114 122 vu_enabled: bool, 115 123 last_print: Instant, 116 124 peak_acc: f32, 125 + fft: Arc<RwLock<FFTState>>, 126 + debug_fft: bool, 117 127 } 118 128 let mut audio_state: Option<AudioState> = None; 129 + // Optional debug flag for FFT bins 130 + let debug_fft = std::env::args().any(|a| a == "--debug-fft"); 131 + 119 132 if !audio_disable { 120 133 let cap_cfg = audio_cap::AudioCaptureConfig { 121 134 device_substr: audio_device.clone(), ··· 124 137 }; 125 138 match audio_cap::start_capture(cap_cfg) { 126 139 Ok((handle, cons)) => { 127 - println!("Audio capture: '{}' @ {} Hz, {} ch", handle.info.device_name, handle.info.sample_rate, handle.info.channels); 128 - if audio_vu { println!("Audio VU: enabled (prints every ~1s)"); } 129 - audio_state = Some(AudioState { _handle: handle, cons, vu_enabled: audio_vu, last_print: Instant::now(), peak_acc: 0.0 }); 140 + println!( 141 + "Audio capture: '{}' @ {} Hz, {} ch", 142 + handle.info.device_name, handle.info.sample_rate, handle.info.channels 143 + ); 144 + if audio_vu { 145 + println!("Audio VU: enabled (prints every ~1s)"); 146 + } 147 + let fft_arc: Arc<RwLock<FFTState>> = Arc::new(RwLock::new(FFTState::new( 148 + audio_cap::default_ring_capacity(), 149 + ))); 150 + set_global_fft(fft_arc.clone()); 151 + audio_state = Some(AudioState { 152 + _handle: handle, 153 + cons, 154 + vu_enabled: audio_vu, 155 + last_print: Instant::now(), 156 + peak_acc: 0.0, 157 + fft: fft_arc, 158 + debug_fft, 159 + }); 130 160 } 131 161 Err(e) => { 132 - eprintln!("Audio capture disabled ({}). Use --audio-disable to silence this.", e); 162 + eprintln!( 163 + "Audio capture disabled ({}). Use --audio-disable to silence this.", 164 + e 165 + ); 133 166 } 134 167 } 135 168 } ··· 160 193 } 161 194 // Simple VU meter from audio ring 162 195 if let Some(a) = audio_state.as_mut() { 163 - // Drain available samples and track peak 164 - while let Ok(s) = a.cons.pop() { a.peak_acc = a.peak_acc.max(s.abs()); } 196 + // Drain available samples, feed analyzer, track peak 197 + while let Ok(s) = a.cons.pop() { 198 + a.peak_acc = a.peak_acc.max(s.abs()); 199 + if let Some(mut w) = a.fft.try_write() { 200 + w.ingest(s); 201 + } 202 + } 203 + if let Some(mut w) = a.fft.try_write() { 204 + w.update(); 205 + } 206 + if a.debug_fft { 207 + // Print a small subset of normalized bins 208 + let bins = { 209 + let r = a.fft.read(); 210 + r.bins().min(16) 211 + }; 212 + let mut line = String::from("FFT[0..16]: "); 213 + if let Some(r) = a.fft.try_read() { 214 + for i in 0..bins { 215 + line.push_str(&format!("{:.2} ", r.fft_sm[i])); 216 + } 217 + } 218 + println!("{}", line); 219 + } 165 220 if a.vu_enabled && a.last_print.elapsed() >= Duration::from_millis(1000) { 166 221 let peak = a.peak_acc.max(1e-9); 167 222 let db = 20.0 * peak.log10();
+199 -50
tic80_rust/src/script/lua_runner.rs
··· 3 3 4 4 use mlua::{Function, Lua, MultiValue, RegistryKey, Result as LuaResult, Value}; 5 5 6 + use crate::audio::fft::{get_global_fft, query_fft}; 7 + use crate::core::memory::Memory; 6 8 use crate::gfx::framebuffer::Framebuffer; 7 - use crate::core::memory::Memory; 8 9 9 10 pub struct LuaRunner { 10 11 lua: Lua, ··· 12 13 } 13 14 14 15 impl LuaRunner { 15 - pub fn new(fb: Rc<RefCell<Framebuffer>>, mem: Rc<RefCell<Memory>>, script_src: &str) -> LuaResult<Self> { 16 + pub fn new( 17 + fb: Rc<RefCell<Framebuffer>>, 18 + mem: Rc<RefCell<Memory>>, 19 + script_src: &str, 20 + ) -> LuaResult<Self> { 16 21 let lua = Lua::new(); 17 22 let tic_key = { 18 23 let globals = lua.globals(); ··· 56 61 57 62 // rectb(x,y,w,h,color) 58 63 let fb_rectb = fb.clone(); 59 - let rectb_fn = lua.create_function( 60 - move |_, (x, y, w, h, color): (i32, i32, i32, i32, u8)| { 64 + let rectb_fn = 65 + lua.create_function(move |_, (x, y, w, h, color): (i32, i32, i32, i32, u8)| { 61 66 fb_rectb.borrow_mut().rectb(x, y, w, h, color); 62 67 Ok(()) 63 - }, 64 - )?; 68 + })?; 65 69 globals.set("rectb", rectb_fn)?; 66 70 67 71 // circ(x, y, r, color) 68 72 let fb_circ = fb.clone(); 69 - let circ_fn = lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| { 70 - fb_circ.borrow_mut().circ(x, y, r, color); 71 - Ok(()) 72 - })?; 73 + let circ_fn = 74 + lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| { 75 + fb_circ.borrow_mut().circ(x, y, r, color); 76 + Ok(()) 77 + })?; 73 78 globals.set("circ", circ_fn)?; 74 79 75 80 // circb(x, y, r, color) 76 81 let fb_circb = fb.clone(); 77 - let circb_fn = lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| { 78 - fb_circb.borrow_mut().circb(x, y, r, color); 79 - Ok(()) 80 - })?; 82 + let circb_fn = 83 + lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| { 84 + fb_circb.borrow_mut().circb(x, y, r, color); 85 + Ok(()) 86 + })?; 81 87 globals.set("circb", circb_fn)?; 82 88 83 89 // clip(x,y,w,h) or clip() to reset ··· 86 92 if args.is_empty() { 87 93 fb_clip.borrow_mut().clip_reset(); 88 94 } else { 89 - let x = match args.get(0) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 90 - let y = match args.get(1) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 91 - let w = match args.get(2) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 92 - let h = match args.get(3) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 95 + let x = match args.get(0) { 96 + Some(Value::Integer(n)) => *n as i32, 97 + _ => 0, 98 + }; 99 + let y = match args.get(1) { 100 + Some(Value::Integer(n)) => *n as i32, 101 + _ => 0, 102 + }; 103 + let w = match args.get(2) { 104 + Some(Value::Integer(n)) => *n as i32, 105 + _ => 0, 106 + }; 107 + let h = match args.get(3) { 108 + Some(Value::Integer(n)) => *n as i32, 109 + _ => 0, 110 + }; 93 111 fb_clip.borrow_mut().clip(x, y, w, h); 94 112 } 95 113 Ok(()) ··· 141 159 })?; 142 160 globals.set("print", print_fn)?; 143 161 162 + // FFT APIs: fft/ffts/fftr/fftrs 163 + fn parse_fft_args(args: &MultiValue) -> (i32, i32) { 164 + let start = match args.get(0) { 165 + Some(Value::Integer(n)) => *n as i32, 166 + _ => -1, 167 + }; 168 + let end = match args.get(1) { 169 + Some(Value::Integer(n)) => *n as i32, 170 + _ => -1, 171 + }; 172 + (start, end) 173 + } 174 + 175 + let fft_fn = lua.create_function(move |_, args: MultiValue| { 176 + let (start, end) = parse_fft_args(&args); 177 + let val = if let Some(arc) = get_global_fft() { 178 + let guard = arc.read(); 179 + query_fft(&guard, start, end, false, false) 180 + } else { 181 + 0.0 182 + }; 183 + Ok(val) 184 + })?; 185 + globals.set("fft", fft_fn)?; 186 + 187 + let ffts_fn = lua.create_function(move |_, args: MultiValue| { 188 + let (start, end) = parse_fft_args(&args); 189 + let val = if let Some(arc) = get_global_fft() { 190 + let guard = arc.read(); 191 + query_fft(&guard, start, end, true, false) 192 + } else { 193 + 0.0 194 + }; 195 + Ok(val) 196 + })?; 197 + globals.set("ffts", ffts_fn)?; 198 + 199 + let fftr_fn = lua.create_function(move |_, args: MultiValue| { 200 + let (start, end) = parse_fft_args(&args); 201 + let val = if let Some(arc) = get_global_fft() { 202 + let guard = arc.read(); 203 + query_fft(&guard, start, end, false, true) 204 + } else { 205 + 0.0 206 + }; 207 + Ok(val) 208 + })?; 209 + globals.set("fftr", fftr_fn)?; 210 + 211 + let fftrs_fn = lua.create_function(move |_, args: MultiValue| { 212 + let (start, end) = parse_fft_args(&args); 213 + let val = if let Some(arc) = get_global_fft() { 214 + let guard = arc.read(); 215 + query_fft(&guard, start, end, true, true) 216 + } else { 217 + 0.0 218 + }; 219 + Ok(val) 220 + })?; 221 + globals.set("fftrs", fftrs_fn)?; 222 + 144 223 // memory: peek/poke + bit variants + memcpy/memset 145 224 let mem_peek = mem.clone(); 146 225 let peek_fn = lua.create_function(move |_, (addr, bits): (u32, Option<u8>)| { 147 226 let a = addr as usize; 148 227 let b = bits.unwrap_or(8); 149 - let v = if b == 8 { mem_peek.borrow().peek(a) } else { mem_peek.borrow().peek_bits(a, b) }; 228 + let v = if b == 8 { 229 + mem_peek.borrow().peek(a) 230 + } else { 231 + mem_peek.borrow().peek_bits(a, b) 232 + }; 150 233 Ok(v as u32) 151 234 })?; 152 235 globals.set("peek", peek_fn)?; 153 236 154 237 let mem_poke = mem.clone(); 155 - let poke_fn = lua.create_function(move |_, (addr, val, bits): (u32, u32, Option<u8>)| { 156 - let a = addr as usize; 157 - let v = val as u8; 158 - let b = bits.unwrap_or(8); 159 - if b == 8 { mem_poke.borrow_mut().poke(a, v); } else { mem_poke.borrow_mut().poke_bits(a, b, v); } 160 - Ok(()) 161 - })?; 238 + let poke_fn = 239 + lua.create_function(move |_, (addr, val, bits): (u32, u32, Option<u8>)| { 240 + let a = addr as usize; 241 + let v = val as u8; 242 + let b = bits.unwrap_or(8); 243 + if b == 8 { 244 + mem_poke.borrow_mut().poke(a, v); 245 + } else { 246 + mem_poke.borrow_mut().poke_bits(a, b, v); 247 + } 248 + Ok(()) 249 + })?; 162 250 globals.set("poke", poke_fn)?; 163 251 164 252 let mem_peek1 = mem.clone(); 165 - globals.set("peek1", lua.create_function(move |_, addr: u32| Ok(mem_peek1.borrow().peek_bits(addr as usize, 1) as u32))?)?; 253 + globals.set( 254 + "peek1", 255 + lua.create_function(move |_, addr: u32| { 256 + Ok(mem_peek1.borrow().peek_bits(addr as usize, 1) as u32) 257 + })?, 258 + )?; 166 259 let mem_peek2 = mem.clone(); 167 - globals.set("peek2", lua.create_function(move |_, addr: u32| Ok(mem_peek2.borrow().peek_bits(addr as usize, 2) as u32))?)?; 260 + globals.set( 261 + "peek2", 262 + lua.create_function(move |_, addr: u32| { 263 + Ok(mem_peek2.borrow().peek_bits(addr as usize, 2) as u32) 264 + })?, 265 + )?; 168 266 let mem_peek4 = mem.clone(); 169 - globals.set("peek4", lua.create_function(move |_, addr: u32| Ok(mem_peek4.borrow().peek_bits(addr as usize, 4) as u32))?)?; 267 + globals.set( 268 + "peek4", 269 + lua.create_function(move |_, addr: u32| { 270 + Ok(mem_peek4.borrow().peek_bits(addr as usize, 4) as u32) 271 + })?, 272 + )?; 170 273 171 274 let mem_poke1 = mem.clone(); 172 - globals.set("poke1", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke1.borrow_mut().poke_bits(addr as usize, 1, val as u8); Ok(()) })?)?; 275 + globals.set( 276 + "poke1", 277 + lua.create_function(move |_, (addr, val): (u32, u32)| { 278 + mem_poke1 279 + .borrow_mut() 280 + .poke_bits(addr as usize, 1, val as u8); 281 + Ok(()) 282 + })?, 283 + )?; 173 284 let mem_poke2 = mem.clone(); 174 - globals.set("poke2", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke2.borrow_mut().poke_bits(addr as usize, 2, val as u8); Ok(()) })?)?; 285 + globals.set( 286 + "poke2", 287 + lua.create_function(move |_, (addr, val): (u32, u32)| { 288 + mem_poke2 289 + .borrow_mut() 290 + .poke_bits(addr as usize, 2, val as u8); 291 + Ok(()) 292 + })?, 293 + )?; 175 294 let mem_poke4 = mem.clone(); 176 - globals.set("poke4", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke4.borrow_mut().poke_bits(addr as usize, 4, val as u8); Ok(()) })?)?; 295 + globals.set( 296 + "poke4", 297 + lua.create_function(move |_, (addr, val): (u32, u32)| { 298 + mem_poke4 299 + .borrow_mut() 300 + .poke_bits(addr as usize, 4, val as u8); 301 + Ok(()) 302 + })?, 303 + )?; 177 304 178 305 let mem_memcpy = mem.clone(); 179 - globals.set("memcpy", lua.create_function(move |_, (dst, src, size): (u32, u32, u32)| { mem_memcpy.borrow_mut().memcpy(dst as usize, src as usize, size as usize); Ok(()) })?)?; 306 + globals.set( 307 + "memcpy", 308 + lua.create_function(move |_, (dst, src, size): (u32, u32, u32)| { 309 + mem_memcpy 310 + .borrow_mut() 311 + .memcpy(dst as usize, src as usize, size as usize); 312 + Ok(()) 313 + })?, 314 + )?; 180 315 let mem_memset = mem.clone(); 181 - globals.set("memset", lua.create_function(move |_, (dst, val, size): (u32, u32, u32)| { mem_memset.borrow_mut().memset(dst as usize, val as u8, size as usize); Ok(()) })?)?; 316 + globals.set( 317 + "memset", 318 + lua.create_function(move |_, (dst, val, size): (u32, u32, u32)| { 319 + mem_memset 320 + .borrow_mut() 321 + .memset(dst as usize, val as u8, size as usize); 322 + Ok(()) 323 + })?, 324 + )?; 182 325 183 326 // elli(x, y, a, b, color) 184 327 let fb_elli = fb.clone(); 185 - let elli_fn = lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| { 186 - fb_elli.borrow_mut().elli(x, y, a, b, color); 187 - Ok(()) 188 - })?; 328 + let elli_fn = 329 + lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| { 330 + fb_elli.borrow_mut().elli(x, y, a, b, color); 331 + Ok(()) 332 + })?; 189 333 globals.set("elli", elli_fn)?; 190 334 191 335 // ellib(x, y, a, b, color) 192 336 let fb_ellib = fb.clone(); 193 - let ellib_fn = lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| { 194 - fb_ellib.borrow_mut().ellib(x, y, a, b, color); 195 - Ok(()) 196 - })?; 337 + let ellib_fn = 338 + lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| { 339 + fb_ellib.borrow_mut().ellib(x, y, a, b, color); 340 + Ok(()) 341 + })?; 197 342 globals.set("ellib", ellib_fn)?; 198 343 199 344 // tri(x1,y1,x2,y2,x3,y3,color) 200 345 let fb_tri = fb.clone(); 201 - let tri_fn = lua.create_function(move |_, (x1,y1,x2,y2,x3,y3,color): (i32,i32,i32,i32,i32,i32,u8)| { 202 - fb_tri.borrow_mut().tri(x1,y1,x2,y2,x3,y3,color); 203 - Ok(()) 204 - })?; 346 + let tri_fn = lua.create_function( 347 + move |_, (x1, y1, x2, y2, x3, y3, color): (i32, i32, i32, i32, i32, i32, u8)| { 348 + fb_tri.borrow_mut().tri(x1, y1, x2, y2, x3, y3, color); 349 + Ok(()) 350 + }, 351 + )?; 205 352 globals.set("tri", tri_fn)?; 206 353 207 354 // trib(x1,y1,x2,y2,x3,y3,color) 208 355 let fb_trib = fb.clone(); 209 - let trib_fn = lua.create_function(move |_, (x1,y1,x2,y2,x3,y3,color): (i32,i32,i32,i32,i32,i32,u8)| { 210 - fb_trib.borrow_mut().trib(x1,y1,x2,y2,x3,y3,color); 211 - Ok(()) 212 - })?; 356 + let trib_fn = lua.create_function( 357 + move |_, (x1, y1, x2, y2, x3, y3, color): (i32, i32, i32, i32, i32, i32, u8)| { 358 + fb_trib.borrow_mut().trib(x1, y1, x2, y2, x3, y3, color); 359 + Ok(()) 360 + }, 361 + )?; 213 362 globals.set("trib", trib_fn)?; 214 363 215 364 // Load script
+76
tic80_rust/tests/fft_tests.rs
··· 1 + use std::sync::Arc; 2 + 3 + use parking_lot::RwLock; 4 + use std::cell::RefCell; 5 + use std::rc::Rc; 6 + use tic80_rust::audio::fft::{query_fft, set_global_fft, FFTState}; 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 gen_bin_exact_samples(k: usize, n_samples: usize) -> Vec<f32> { 12 + // Generate y[n] = sin(2π k n / N) with N=2048 to align with FFT bins 13 + let nfft = 2048.0f32; 14 + (0..n_samples) 15 + .map(|n| { 16 + let angle = 2.0 * std::f32::consts::PI * (k as f32) * (n as f32) / nfft; 17 + angle.sin() 18 + }) 19 + .collect() 20 + } 21 + 22 + #[test] 23 + fn fft_query_peak_at_bin() { 24 + let cap = 8192; 25 + let mut fft = FFTState::new(cap); 26 + let k = 64usize; 27 + let samples = gen_bin_exact_samples(k, 4096); 28 + for s in samples { 29 + fft.ingest(s); 30 + } 31 + fft.update(); 32 + 33 + // raw magnitude at k should exceed neighbors 34 + let v_k = query_fft(&fft, k as i32, -1, false, true); 35 + let v_l = query_fft(&fft, (k as i32) - 1, -1, false, true); 36 + let v_r = query_fft(&fft, (k as i32) + 1, -1, false, true); 37 + assert!( 38 + v_k > v_l * 2.0 && v_k > v_r * 2.0, 39 + "expected distinct peak at bin k" 40 + ); 41 + } 42 + 43 + #[test] 44 + fn lua_fft_returns_normalized_bin() { 45 + // Prepare shared FFT state and feed a strong tone at bin k 46 + let cap = 8192; 47 + let fft = Arc::new(RwLock::new(FFTState::new(cap))); 48 + let k = 32usize; 49 + let samples = gen_bin_exact_samples(k, 4096); 50 + { 51 + let mut w = fft.write(); 52 + for s in samples { 53 + w.ingest(s); 54 + } 55 + w.update(); 56 + } 57 + set_global_fft(fft); 58 + 59 + // Lua script: mark pixel if normalized fft(k) > 0.5 60 + let script = format!( 61 + "\ 62 + function BOOT() cls(0) end\n\ 63 + function TIC()\n\ 64 + local v = fft({k}, -1)\n\ 65 + if v > 0.5 then pix(0,0,7) end\n\ 66 + end\n", 67 + k = k 68 + ); 69 + 70 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 71 + let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 72 + let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init"); 73 + runner.tick(); 74 + 75 + assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(7)); 76 + }
+36 -13
tic80_rust/tests/gfx_framebuffer_tests.rs
··· 50 50 let (w, h) = dimensions(); 51 51 let x0 = 0i32; 52 52 let y0 = 0i32; 53 - let x1 = ( -5 + 10).min(w as i32); 54 - let y1 = ( -3 + 8).min(h as i32); 53 + let x1 = (-5 + 10).min(w as i32); 54 + let y1 = (-3 + 8).min(h as i32); 55 55 let expected = (x1 - x0).max(0) as usize * (y1 - y0).max(0) as usize; 56 56 let mut count = 0usize; 57 57 for y in 0..(h as i32) { ··· 141 141 let mut fb = Framebuffer::new(); 142 142 fb.cls(0); 143 143 // set three sample pixels to known colors 144 - fb.set_pixel(0, 0, 0); // black 145 - fb.set_pixel(1, 0, 9); // orange 146 - fb.set_pixel(2, 0, 15); // peach 144 + fb.set_pixel(0, 0, 0); // black 145 + fb.set_pixel(1, 0, 9); // orange 146 + fb.set_pixel(2, 0, 15); // peach 147 147 148 148 let (w, h) = dimensions(); 149 149 let mut rgba = vec![0u8; (w * h * 4) as usize]; ··· 208 208 // Something on row 0 209 209 let mut any_row0 = false; 210 210 for x in 0..8 { 211 - if fb2.pix(x, 0, None) == Some(15) { any_row0 = true; break; } 211 + if fb2.pix(x, 0, None) == Some(15) { 212 + any_row0 = true; 213 + break; 214 + } 212 215 } 213 216 assert!(any_row0); 214 217 // And something on row 6 215 218 let mut any_row6 = false; 216 219 for x in 0..8 { 217 - if fb2.pix(x, 6, None) == Some(15) { any_row6 = true; break; } 220 + if fb2.pix(x, 6, None) == Some(15) { 221 + any_row6 = true; 222 + break; 223 + } 218 224 } 219 225 assert!(any_row6); 220 226 } ··· 224 230 let mut fb = Framebuffer::new(); 225 231 fb.cls(2); 226 232 fb.clip(1, 1, 1, 1); // only (1,1) 227 - // Write outside clip 233 + // Write outside clip 228 234 let _ = fb.pix(0, 0, Some(7)); 229 235 // Write inside clip 230 236 let _ = fb.pix(1, 1, Some(7)); ··· 352 358 fb.tri(10, 10, 20, 10, 15, 15, 6); 353 359 // Top scanline: interior x in (10,20) filled; endpoints excluded by top-left rule 354 360 assert_eq!(fb.pix(10, 10, None), Some(0)); 355 - for x in 11..20 { assert_eq!(fb.pix(x, 10, None), Some(6)); } 361 + for x in 11..20 { 362 + assert_eq!(fb.pix(x, 10, None), Some(6)); 363 + } 356 364 assert_eq!(fb.pix(20, 10, None), Some(0)); 357 365 // Bottom row excluded 358 - for x in 10..=20 { assert_eq!(fb.pix(x, 15, None), Some(0)); } 366 + for x in 10..=20 { 367 + assert_eq!(fb.pix(x, 15, None), Some(0)); 368 + } 359 369 } 360 370 361 371 #[test] ··· 364 374 fb.cls(0); 365 375 // Flat-bottom triangle: bottom edge y=20 excluded 366 376 fb.tri(10, 10, 5, 20, 15, 20, 7); 367 - for x in 5..=15 { assert_eq!(fb.pix(x, 20, None), Some(0)); } 377 + for x in 5..=15 { 378 + assert_eq!(fb.pix(x, 20, None), Some(0)); 379 + } 368 380 // No assumption on apex inclusion; key check is base exclusion 369 381 } 370 382 ··· 376 388 fb.tri(0, 0, 10, 0, 0, 10, 3); 377 389 fb.tri(10, 10, 10, 0, 0, 10, 3); 378 390 let mut count = 0usize; 379 - for y in 0..10 { for x in 0..10 { if fb.pix(x, y, None) == Some(3) { count += 1; } } } 391 + for y in 0..10 { 392 + for x in 0..10 { 393 + if fb.pix(x, y, None) == Some(3) { 394 + count += 1; 395 + } 396 + } 397 + } 380 398 assert_eq!(count, 100, "expected full 10x10 coverage without gaps"); 381 399 } 382 400 ··· 388 406 fb.tri(10, 10, 10, 15, 10, 20, 9); 389 407 // No pixels should be colored with 9 390 408 let mut any = false; 391 - for y in 10..=20 { if fb.pix(10, y, None) == Some(9) { any = true; break; } } 409 + for y in 10..=20 { 410 + if fb.pix(10, y, None) == Some(9) { 411 + any = true; 412 + break; 413 + } 414 + } 392 415 assert!(!any, "degenerate triangle should not draw"); 393 416 }
+36 -12
tic80_rust/tests/lua_api_tests.rs
··· 1 1 use std::cell::RefCell; 2 - use std::rc::Rc; 3 2 use std::fs; 4 3 use std::path::PathBuf; 4 + use std::rc::Rc; 5 5 6 + use tic80_rust::core::memory::Memory; 6 7 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 7 8 use tic80_rust::script::lua_runner::LuaRunner; 8 - use tic80_rust::core::memory::Memory; 9 9 10 10 fn run_lua(script: &str, ticks: usize) -> Rc<RefCell<Framebuffer>> { 11 11 let fb = Rc::new(RefCell::new(Framebuffer::new())); 12 12 let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 13 13 let runner = LuaRunner::new(fb.clone(), mem, script).expect("lua init"); 14 - for _ in 0..ticks { runner.tick(); } 14 + for _ in 0..ticks { 15 + runner.tick(); 16 + } 15 17 fb 16 18 } 17 19 ··· 75 77 break; 76 78 } 77 79 } 78 - if any { break; } 80 + if any { 81 + break; 82 + } 79 83 } 80 84 assert!(any, "expected some glyph pixels drawn near (10,10)"); 81 85 ··· 83 87 let (w, _) = dimensions(); 84 88 let mut found_marker = false; 85 89 for x in 10..(w as i32) { 86 - if fbm.pix(x, 10, None) == Some(14) { found_marker = true; break; } 90 + if fbm.pix(x, 10, None) == Some(14) { 91 + found_marker = true; 92 + break; 93 + } 87 94 } 88 - assert!(found_marker, "expected marker pixel with color 14 on row 10"); 95 + assert!( 96 + found_marker, 97 + "expected marker pixel with color 14 on row 10" 98 + ); 89 99 } 90 100 91 101 #[test] ··· 114 124 let mut any = false; 115 125 for yy in 0..8 { 116 126 for xx in 0..8 { 117 - if fbm.pix(xx, yy, None) == Some(15) { any = true; break; } 127 + if fbm.pix(xx, yy, None) == Some(15) { 128 + any = true; 129 + break; 130 + } 118 131 } 119 - if any { break; } 132 + if any { 133 + break; 134 + } 120 135 } 121 136 assert!(any, "expected some glyph pixels drawn near origin"); 122 137 // verify the script marked (w,0) with 7 (we don't need to know w here) ··· 209 224 let fb = Rc::new(RefCell::new(Framebuffer::new())); 210 225 let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 211 226 let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init"); 212 - for _ in 0..ticks { runner.tick(); } 227 + for _ in 0..ticks { 228 + runner.tick(); 229 + } 213 230 let mut borrowed = fb.borrow_mut(); 214 231 fb_hash(&mut borrowed) 215 232 }; ··· 223 240 assert_eq!(h2, h2_again, "hash should be deterministic for 2 ticks"); 224 241 225 242 // Different frame counts should usually yield different hashes for this cart 226 - assert_ne!(h1, h2, "different ticks should yield different frame hashes"); 243 + assert_ne!( 244 + h1, h2, 245 + "different ticks should yield different frame hashes" 246 + ); 227 247 } 228 248 229 249 #[test] ··· 238 258 let fb = run_lua(script, 1); 239 259 let mut fbm = fb.borrow_mut(); 240 260 // Filled circle: center row span for r=4 241 - for x in 16..=24 { assert_eq!(fbm.pix(x, 20, None), Some(6)); } 261 + for x in 16..=24 { 262 + assert_eq!(fbm.pix(x, 20, None), Some(6)); 263 + } 242 264 // Border circle: cardinal points for r=3 243 265 assert_eq!(fbm.pix(33, 20, None), Some(9)); 244 266 assert_eq!(fbm.pix(27, 20, None), Some(9)); ··· 265 287 assert_eq!(fbm.pix(60, 23, None), Some(12)); 266 288 assert_eq!(fbm.pix(60, 17, None), Some(12)); 267 289 // Filled ellipse center row (interior only; endpoints are border color) 268 - for x in 56..=64 { assert_eq!(fbm.pix(x, 20, None), Some(4)); } 290 + for x in 56..=64 { 291 + assert_eq!(fbm.pix(x, 20, None), Some(4)); 292 + } 269 293 // Triangle interior 270 294 assert_eq!(fbm.pix(80, 18, None), Some(6)); 271 295 // Border triangle vertices
+10 -4
tic80_rust/tests/memory_tests.rs
··· 33 33 let mut mem = Memory::new(fb.clone()); 34 34 // memset first 10 bytes of VRAM screen -> sets first 20 pixels (pairs) to (0x5,0x5) 35 35 mem.memset(0, 0x55, 10); 36 - for x in 0..20 { assert_eq!(fb.borrow_mut().pix(x, 0, None), Some(0x5)); } 36 + for x in 0..20 { 37 + assert_eq!(fb.borrow_mut().pix(x, 0, None), Some(0x5)); 38 + } 37 39 38 40 // prepare source bytes with pattern 0xAB -> (0xB,0xA) on pixels 39 41 let src = 40000usize; // within RAM region beyond VRAM 40 - for i in 0..4 { mem.poke(src + i, 0xAB); } 42 + for i in 0..4 { 43 + mem.poke(src + i, 0xAB); 44 + } 41 45 mem.memcpy(0, src, 4); // copy into beginning of VRAM 42 - // First 8 pixels now map from 0xAB pairs 46 + // First 8 pixels now map from 0xAB pairs 43 47 let mut px = vec![]; 44 - for x in 0..8 { px.push(fb.borrow_mut().pix(x, 0, None).unwrap()); } 48 + for x in 0..8 { 49 + px.push(fb.borrow_mut().pix(x, 0, None).unwrap()); 50 + } 45 51 assert_eq!(&px, &[0xB, 0xA, 0xB, 0xA, 0xB, 0xA, 0xB, 0xA]); 46 52 } 47 53