this repo has no description
0
fork

Configure Feed

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

audio pt1

alice e97eee9a 6fa17186

+779 -21
+20 -2
AGENTS.md
··· 2 2 3 3 - **Owner:** AI Coding Agent (Codex CLI) 4 4 - **Scope:** Help drive the Rust rewrite forward with tests-first changes, targeted features, and tight parity with TIC-80 behavior. 5 - - **Last Updated:** 2025-08-26 5 + - **Last Updated:** 2025-08-27 6 + 7 + **Documentation Discipline — Agent Reminder (PROMINENT)** 8 + - ALWAYS document code changes immediately after writing code: 9 + - Update this file’s Current Status and Worklog (with date + concise bullets). 10 + - Update relevant docs (specs/architecture/testing) and cross-link from here. 11 + - Update `docs/specs/implementation_status.md` to reflect new capabilities. 12 + - If the plan changed, update the plan doc(s) and link them (Roadmap/Spec). 13 + - ALWAYS document plans as first-class docs: 14 + - Add a plan/Implementation TODOs section under the relevant spec (e.g., audio FFT/VQT) or create a new spec. 15 + - Reference new/updated plans from AGENTS.md and the docs index. 16 + - Keep hygiene visible: mention clippy/test status with each change. 6 17 7 18 **Context** 8 19 - **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`. ··· 38 49 - `print` implemented with default font (variable/fixed width, scale, newline advance) and returns width. 39 50 - Memory ops (`peek/poke` 1/2/4/8‑bit, `memcpy`, `memset`) implemented; VRAM updates reflect on screen. 40 51 - CLI loads bundled default cart or a provided `.lua` path. 52 + - 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. 41 53 42 54 **Near-Term Backlog** 55 + - FFT implementation (cpal + realfft) per `docs/specs/audio_fft_vqt.md` (see Implementation TODOs section); add headless tests and Lua `fft/ffts/fftr/fftrs`. 43 56 - Print edge cases: tests for scale>1 baseline/advance and multi‑line width parity. 44 57 - Small font: decide semantics and implement `smallfont=true` in `print` with tests. 45 58 - Lua error paths: add type/arity mismatch tests for core APIs (`pix/line/rect/print`). ··· 51 64 - Banks/persistence: `vbank`, `sync`, `pmem`; palette map/border color. 52 65 - Input/system: `btn/btnp`, `key/keyp`, `mouse`, `time`, `tstamp`, `trace`, `exit`, `reset` (fixed‑step repeat timing). 53 66 - Audio: `sfx`, `music` synth/mixer; capture ring for analysis. 54 - - Analysis: `fft/ffts/fftr/fftrs`, `vqt` variants; conformance carts + numeric tolerances. 67 + - Analysis (VQT): implement kernels + unwhitened/whitened paths per `docs/specs/audio_fft_vqt.md` (see Implementation TODOs) and expose `vqt*`/`vqt*w` APIs; conformance carts + numeric tolerances. 55 68 - Platform: WASM build path; window scaling and UX polish. 56 69 57 70 **Open Questions** ··· 78 91 - Renamed API parity to `docs/specs/lua_api_parity.md` and added specs stubs. 79 92 - Added docs index at `docs/README.md`, architecture/testing pages, and ADRs. 80 93 - Added detailed testing docs (`docs/testing/strategy.md`) and a test catalog (`docs/testing/test_catalog.md`). 94 + - 2025-08-27: 95 + - Implemented audio capture with `cpal`: device selection/listing, mono downmix, ring buffer. 96 + - Added CLI: `--list-audio`, `--audio-device`, `--audio-vu`, `--audio-disable`. 97 + - Integrated a 1s VU peak readout for manual verification. 98 + - Kept clippy/tests green; documented the FFT/VQT plan and linked TODOs. 81 99 82 100 **Docs Index** 83 101 - Start here: `docs/README.md`
+1 -1
docs/README.md
··· 10 10 - `docs/specs/memory_map.md`: Canonical pointer to the root `MEMORY_MAP.md` and usage notes. 11 11 - `docs/specs/lua_api_parity.md`: API parity checklist for Lua (name, signature, side effects). 12 12 - `docs/specs/graphics.md`: Framebuffer, palette mapping, text/print semantics (stub to be expanded). 13 - - `docs/specs/audio_fft_vqt.md`: FFT/VQT behavior and parameters (points to `CLAUDE.md`). 13 + - `docs/specs/audio_fft_vqt.md`: FFT/VQT behavior and Rust implementation plan (cpal + realfft), with links to `CLAUDE.md`. 14 14 - `docs/specs/implementation_status.md`: What’s implemented vs pending, with notes on behavior. 15 15 16 16 ## Architecture
+3
docs/architecture/runtime.md
··· 12 12 - CPU-owned framebuffer as palette indices; `blit_to_rgba` maps to RGBA for window texture. 13 13 - Integer scaling to maintain crisp pixels. 14 14 15 + Audio Capture 16 + - `cpal` input stream runs in a platform callback and writes mono f32 samples into a lock‑free ring buffer sized to 8192. 17 + - The analysis path (FFT/VQT) runs on the tick thread, reading from the ring’s tail to ensure determinism and keep heavy work off the audio thread.
+1 -1
docs/roadmap/overview.md
··· 52 52 - Rendering backend: Start with software rasterizer in `tic-gfx` (deterministic, headless testable). Add a thin SDL2 or winit+pixels presentation layer later. Optional `wgpu` in a later phase. 53 53 - Audio IO: Use `cpal` for capture/output. Keep synth/tick deterministic in `tic-audio`. For capture, implement a lock-free ring buffer shared with `tic-fx`. 54 54 - Compression: Use `flate2`/`miniz_oxide` for zlib compatible packing/unpacking of carts. 55 - - FFT: Use `rustfft` for 2k (FFT) and 8k (VQT path) with exact binning and smoothing behavior; implement variable-Q kernel generation per CLAUDE.md. 55 + - FFT/VQT: Use `realfft` (R2C) for 2k/8k transforms with exact binning and smoothing behavior; implement variable‑Q kernel generation per CLAUDE.md. See `docs/specs/audio_fft_vqt.md` for the Rust plan. 56 56 - Testing: Frame-hash snapshots for VRAM, audio block-level comparisons, API-level golden tests. Conformance carts from `demos/`. 57 57 58 58 ## Phased Roadmap
+137 -2
docs/specs/audio_fft_vqt.md
··· 1 1 # Audio Analysis (FFT/VQT) 2 2 3 - Detailed behavior, parameters, and equations live in `CLAUDE.md` at the repo root. 3 + This page captures the runtime behavior and the implementation plan for FFT/VQT in the Rust rewrite. Canonical derivations and design notes still live in `CLAUDE.md` at the repo root; this page focuses on API semantics, platform choices, and test strategy for parity with the C implementation. 4 + 5 + ## Goals 6 + - API Parity: Implement `fft/ffts/fftr/fftrs`, `vqt/vqts/vqtr/vqtrs`, and `vqtw/vqtsw/vqtrw/vqtrsw` with identical signatures and observable behavior. 7 + - Determinism: Stable outputs given fixed inputs (headless tests feed synthetic signals). 8 + - Performance: Desktop targets (Windows/macOS/Linux) at real‑time rates (2k FFT ~21 fps; 8k VQT ~5.4 fps). 9 + 10 + ## Technology Choices (Rust) 11 + - Audio I/O: `cpal` for capture (pure Rust), f32 stereo → mono downmix in callback. 12 + - FFT: `realfft` (R2C) for N=2048 and N=8192 transforms; built on `rustfft`. 13 + - Buffering: `ringbuf` (lock‑free SPSC) between audio callback and analysis thread. 14 + - Windows: `window-functions` or `apodize` for Hamming/Gaussian in VQT kernel generation. 15 + 16 + ## Sample Rate and Buffers 17 + - Sample Rate: 44100 Hz throughout (resample to 44.1k if device provides a different rate; future work). 18 + - Shared Audio Buffer: `AUDIO_BUFFER_SIZE = max(2*FFT_SIZE, VQT_FFT_SIZE) = 8192` mono f32 samples (aligned with C). 19 + - Capture Format: accept i16/u16/f32; convert to f32, average L/R to mono; no allocations in the callback. 20 + 21 + ## FFT (2k) — Behavior and Plan 22 + - Window: last 2048 mono samples (from the shared buffer). 23 + - Transform: R2C length 2048; magnitude per bin: `2.0 * hypot(re, im)`. 24 + - Bins: expose 0..1023 (drop Nyquist) to match C. 25 + - Buffers (mirroring C): 26 + - Raw: `fftRawData[1024]`, `fftRawSmoothingData[1024]` (IIR smoothing factor 0.6). 27 + - Display: `fftData[1024]`, `fftSmoothingData[1024]`, `fftNormalizedData[1024]`. 28 + - Peak tracking: `fPeakMinValue=0.01`, `fPeakSmoothing=0.995`, `fPeakSmoothValue`, `fAmplification=1/peak`. 29 + - Update (per tick): 30 + 1) Read last 2048 samples; zero‑pad until warm. 31 + 2) R2C → magnitudes; update raw and smoothed raw. 32 + 3) Update peak smoothing, recompute amplification, write normalized and smoothed normalized. 33 + - APIs: 34 + - `fft(start,end=-1)`: normalized value at `start` or inclusive sum over `[start..end]` with C’s clamping rules. 35 + - `ffts(start,end)`: smoothed normalized. 36 + - `fftr/fftrs`: raw and raw‑smoothed (no normalization). 37 + 38 + ## VQT (8k) — Behavior and Plan 39 + - Window: last 8192 mono samples. 40 + - Kernels (once): 41 + - Centers: 120 bins starting at 19.445 Hz (D#0/Eb0), semitone spacing `2^(1/12)`. 42 + - Variable Q: replicate C’s schedule; window length `Q * fs / f`, clamped to 8192; minimum length guard. 43 + - Windowing: Hamming or Gaussian (match current default); normalize by window length. 44 + - Modulation: complex exponential across full FFT buffer, centered at N/2 (parity with C notes and indices). 45 + - R2C kernel FFT; sparsify to `(indices[], real[], imag[])` above adaptive magnitude thresholds. 46 + - Runtime VQT (per tick): 47 + 1) R2C of input frame (8192). 48 + 2) For each kernel: sparse complex dot over half‑spectrum (0..4096), magnitude then `* 2.0`. 49 + 3) Unwhitened path: 50 + - Smooth `vqtSmoothingData` with `VQT_SMOOTHING_FACTOR=0.3`. 51 + - Peak smoothing to `vqtPeakSmoothValue` (0.99 mix as in C pattern), normalize to `vqtNormalizedData` in [0,1]. 52 + 4) Whitened path (separate arrays): 53 + - Whitening enabled by default: log(m+eps) → moving average over width=21 → subtract → exp → mix by `alpha=0.95`. 54 + - Smooth to `vqtWhiteSmoothingData`; separate peak tracker `vqtWhitePeakSmoothValue`; normalize to `vqtWhiteNormalizedData`. 55 + - APIs (two parallel sets): 56 + - Unwhitened: `vqt` (normalized), `vqts` (smoothed+normalized), `vqtr` (raw), `vqtrs` (raw smoothed). 57 + - Whitened: `vqtw`, `vqtsw`, `vqtrw`, `vqtrsw` with identical semantics on whitened buffers. 58 + 59 + ## Parity Rules 60 + - Bins and scaling match C exactly: drop Nyquist for 2k FFT; use `2.0` magnitude factor. 61 + - Peak tracking and smoothing factors: FFT raw/display (0.6), peak (0.995); VQT smoothing (0.3), whitening width=21, alpha=0.95, eps=1e‑6. 62 + - Inclusive sums and clamping follow C’s logic for out‑of‑bounds and reversed ranges. 63 + - Whitening is strictly a separate path with its own buffers and peak tracker. 64 + 65 + ## Testing Strategy 66 + - Headless tests feed synthetic tones/noise into the analysis (bypass `cpal`). 67 + - FFT: 68 + - Single‑tone peak lands at expected bin; normalized steady state ≈ 1.0. 69 + - Raw vs smoothed differ per IIR; inclusive sum matches expected bin sums. 70 + - VQT: 71 + - Kernel spot checks: indices non‑empty; ranges align with center frequencies. 72 + - Single‑tone hits expected semitone bin; whitened vs unwhitened differ predictably on broadband inputs. 73 + - Normalization clamps to [0,1]; whitened/unwhitened use independent peak trackers. 74 + 75 + ## Integration Plan (Milestones) 76 + 1) FFT foundation: ring buffer, 2k R2C planner, raw/normalized/smoothed buffers, Lua `fft/ffts/fftr/fftrs`. 77 + 2) VQT kernels: generation + storage; 8k R2C planner; unwhitened path with `vqt/vqts/vqtr/vqtrs`. 78 + 3) Whitening path: `vqtw/vqtsw/vqtrw/vqtrsw` with separate smoothing/peaks; feature flag to toggle. 79 + 4) Cross‑platform validation: confirm capture paths (WASAPI/CoreAudio/ALSA/Pulse/JACK) and resampling if needed. 80 + 81 + ## References 82 + - Canonical details: `CLAUDE.md`. 83 + - C sources for parity: `src/ext/fft.c`, `src/fftdata.h/.c`, `src/ext/vqt.c`, `src/vqtdata.h/.c`, `src/ext/vqt_kernel.c`. 84 + 85 + 86 + ## Implementation TODOs 87 + 88 + Status 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). 91 + 92 + Phase 1: Audio (cpal) 93 + - Device listing: Add `--list-audio` to print capture devices and default. 94 + - Device selection: Add `--audio-device "<name-substr>"` to pick device by substring; default to system default input. 95 + - Sample rate: Request 44100 Hz; if device differs, accept nearest and record actual rate; add `--audio-rate 44100` override (future resampling). 96 + - Channel format: Accept f32/i16/u16; convert to f32; downmix stereo to mono by average. 97 + - Buffering: Add SPSC ring buffer (capacity 8192 f32 mono samples). No allocations in callback. 98 + - Threading: Start `cpal` input stream on app init; push samples into ring buffer; graceful start/stop. 99 + - UX check: `--audio-vu` to print a simple VU meter/peak every second for manual verification. 100 + - Errors: Clear messages on device open failures; fallback to default device if selection fails; allow `--audio-disable`. 101 + 102 + Phase 2: FFT (2k) 103 + - Planner: Initialize `realfft` R2C plan for N=2048; allocate scratch/output once. 104 + - State buffers: 105 + - Raw: `fftRaw[1024]`, `fftRawSm[1024]` (IIR smoothing factor 0.6). 106 + - Display: `fftData[1024]`, `fftSm[1024]`, `fftNorm[1024]`. 107 + - Peaks: `fPeakMin=0.01`, `fPeakSmooth=0.995`, `fPeak`, `fAmpl=1/fPeak`. 108 + - Tick update: 109 + - Read last 2048 mono samples (zero-fill until warm). 110 + - R2C; magnitudes for bins 0..1023 as `2.0 * hypot(re, im)`. 111 + - Update raw/smoothed raw; update `fPeak` and `fAmpl`; write normalized and smoothed normalized. 112 + - Lua APIs: `fft/ffts/fftr/fftrs` with C-identical clamping and inclusive-sum semantics. 113 + - Tests (headless): tone peak at expected bin; normalized steady-state ≈ 1; smoothed below raw; range sums; OOB clamping. 114 + - Telemetry: Optional `--debug-fft` dump. 4 115 5 - This page will summarize API-facing behavior (bins, windows, smoothing, normalization, whitening) and cross-link into the canonical derivations in `CLAUDE.md`. 116 + Phase 3: VQT (8k) 117 + - Planner: Initialize `realfft` R2C plan for N=8192; allocate scratch/output once. 118 + - Kernel generation (once): 119 + - Centers: 120 semitone bins from 19.445 Hz (`2^(1/12)` spacing). 120 + - Q schedule: Mirror C’s rules; window length `Q*fs/f`, clamp to 8192; min length guard. 121 + - Windowing: Hamming (default) and Gaussian; normalize by window length. 122 + - Modulation: Complex exponential over full 8192 buffer centered at N/2. 123 + - R2C of time kernel; sparsify to `(indices, real, imag)` with adaptive thresholds. 124 + - VQT state buffers (unwhitened): `vqt[120]`, `vqtSm[120]` (0.3), `vqtNorm[120]`, `vqtPeak`. 125 + - Tick update (unwhitened): R2C input; sparse complex dot per bin over 0..4096; magnitude `* 2.0`; smooth; peak-smooth; normalize to [0,1]. 126 + - Lua APIs: `vqt/vqts/vqtr/vqtrs`. 127 + - Tests: kernel sanity (non-empty indices, expected spans); 440 Hz tone peak; normalization bounds. 6 128 129 + Phase 4: Whitening (separate path) 130 + - Config: Feature flag `whitening=true` (default); `--no-whitening` toggle. 131 + - Whitening buffers: `vqtW[120]`, `vqtWSm[120]`, `vqtWNorm[120]`, `vqtWPeak`. 132 + - Whitening math: log(m+eps) → moving-average env (width=21) → subtract → exp → mix by alpha=0.95. 133 + - Tick update (whitened): smooth; peak-smooth; normalize to [0,1]; if disabled, mirror unwhitened into whitened buffers. 134 + - Lua APIs: `vqtw/vqtsw/vqtrw/vqtrsw`. 135 + - Tests: broadband input flatter in whitened vs unwhitened; toggle affects only whitened set. 136 + 137 + Cross‑cutting 138 + - Determinism: All analysis in tick thread; audio callback only pushes to ring. 139 + - Performance: Reuse FFT plans/buffers; no alloc in hot paths; optional `--debug-fx` timing logs. 140 + - Error handling: Safe fail if no audio device; tests bypass `cpal`. 141 + - Docs: Keep this page as source of truth; update AGENTS.md Near-/Mid‑Term backlog as milestones are delivered.
+1
docs/specs/implementation_status.md
··· 26 26 Implemented (Runner/CLI) 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 + - 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. 29 30 30 31 Behavioral Notes 31 32 - Triangles: top-left inclusion; CCW orientation enforced internally; half-open bounding box prevents shared-edge double draws.
+373 -5
tic80_rust/Cargo.lock
··· 46 46 ] 47 47 48 48 [[package]] 49 + name = "aho-corasick" 50 + version = "1.1.3" 51 + source = "registry+https://github.com/rust-lang/crates.io-index" 52 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 53 + dependencies = [ 54 + "memchr", 55 + ] 56 + 57 + [[package]] 49 58 name = "allocator-api2" 50 59 version = "0.2.21" 51 60 source = "registry+https://github.com/rust-lang/crates.io-index" 52 61 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 53 62 54 63 [[package]] 64 + name = "alsa" 65 + version = "0.9.2" 66 + source = "registry+https://github.com/rust-lang/crates.io-index" 67 + checksum = "bdc00893e7a970727e9304671b2c88577b4cfe53dc64019fdfdf9683573a09c4" 68 + dependencies = [ 69 + "alsa-sys", 70 + "bitflags 2.9.3", 71 + "cfg-if", 72 + "libc", 73 + ] 74 + 75 + [[package]] 76 + name = "alsa-sys" 77 + version = "0.3.1" 78 + source = "registry+https://github.com/rust-lang/crates.io-index" 79 + checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 80 + dependencies = [ 81 + "libc", 82 + "pkg-config", 83 + ] 84 + 85 + [[package]] 55 86 name = "android-activity" 56 87 version = "0.4.3" 57 88 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 63 94 "jni-sys", 64 95 "libc", 65 96 "log", 66 - "ndk", 97 + "ndk 0.7.0", 67 98 "ndk-context", 68 - "ndk-sys", 99 + "ndk-sys 0.4.1+23.1.7779620", 69 100 "num_enum 0.6.1", 70 101 ] 71 102 ··· 85 116 ] 86 117 87 118 [[package]] 119 + name = "anyhow" 120 + version = "1.0.99" 121 + source = "registry+https://github.com/rust-lang/crates.io-index" 122 + checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 123 + 124 + [[package]] 88 125 name = "arrayref" 89 126 version = "0.3.9" 90 127 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 127 164 ] 128 165 129 166 [[package]] 167 + name = "bindgen" 168 + version = "0.72.0" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" 171 + dependencies = [ 172 + "bitflags 2.9.3", 173 + "cexpr", 174 + "clang-sys", 175 + "itertools", 176 + "proc-macro2", 177 + "quote", 178 + "regex", 179 + "rustc-hash 2.1.1", 180 + "shlex", 181 + "syn 2.0.106", 182 + ] 183 + 184 + [[package]] 130 185 name = "bit-set" 131 186 version = "0.5.3" 132 187 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 201 256 checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" 202 257 203 258 [[package]] 259 + name = "bytes" 260 + version = "1.10.1" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 263 + 264 + [[package]] 204 265 name = "calloop" 205 266 version = "0.10.6" 206 267 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 226 287 ] 227 288 228 289 [[package]] 290 + name = "cesu8" 291 + version = "1.1.0" 292 + source = "registry+https://github.com/rust-lang/crates.io-index" 293 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 294 + 295 + [[package]] 296 + name = "cexpr" 297 + version = "0.6.0" 298 + source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 300 + dependencies = [ 301 + "nom", 302 + ] 303 + 304 + [[package]] 229 305 name = "cfg-if" 230 306 version = "1.0.3" 231 307 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 238 314 checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 239 315 240 316 [[package]] 317 + name = "clang-sys" 318 + version = "1.8.1" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 321 + dependencies = [ 322 + "glob", 323 + "libc", 324 + "libloading 0.8.8", 325 + ] 326 + 327 + [[package]] 241 328 name = "codespan-reporting" 242 329 version = "0.11.1" 243 330 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 254 341 checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" 255 342 256 343 [[package]] 344 + name = "combine" 345 + version = "4.6.7" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 348 + dependencies = [ 349 + "bytes", 350 + "memchr", 351 + ] 352 + 353 + [[package]] 257 354 name = "core-foundation" 258 355 version = "0.9.4" 259 356 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 294 391 ] 295 392 296 393 [[package]] 394 + name = "coreaudio-rs" 395 + version = "0.11.3" 396 + source = "registry+https://github.com/rust-lang/crates.io-index" 397 + checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 398 + dependencies = [ 399 + "bitflags 1.3.2", 400 + "core-foundation-sys", 401 + "coreaudio-sys", 402 + ] 403 + 404 + [[package]] 405 + name = "coreaudio-sys" 406 + version = "0.2.17" 407 + source = "registry+https://github.com/rust-lang/crates.io-index" 408 + checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" 409 + dependencies = [ 410 + "bindgen", 411 + ] 412 + 413 + [[package]] 414 + name = "cpal" 415 + version = "0.15.3" 416 + source = "registry+https://github.com/rust-lang/crates.io-index" 417 + checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" 418 + dependencies = [ 419 + "alsa", 420 + "core-foundation-sys", 421 + "coreaudio-rs", 422 + "dasp_sample", 423 + "jni", 424 + "js-sys", 425 + "libc", 426 + "mach2", 427 + "ndk 0.8.0", 428 + "ndk-context", 429 + "oboe", 430 + "wasm-bindgen", 431 + "wasm-bindgen-futures", 432 + "web-sys", 433 + "windows 0.54.0", 434 + ] 435 + 436 + [[package]] 297 437 name = "crc32fast" 298 438 version = "1.5.0" 299 439 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 312 452 "libloading 0.8.8", 313 453 "winapi", 314 454 ] 455 + 456 + [[package]] 457 + name = "dasp_sample" 458 + version = "0.11.0" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 315 461 316 462 [[package]] 317 463 name = "dispatch" ··· 442 588 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 443 589 444 590 [[package]] 591 + name = "glob" 592 + version = "0.3.3" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 595 + 596 + [[package]] 445 597 name = "glow" 446 598 version = "0.12.3" 447 599 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 482 634 "log", 483 635 "thiserror", 484 636 "winapi", 485 - "windows", 637 + "windows 0.44.0", 486 638 ] 487 639 488 640 [[package]] ··· 581 733 ] 582 734 583 735 [[package]] 736 + name = "itertools" 737 + version = "0.13.0" 738 + source = "registry+https://github.com/rust-lang/crates.io-index" 739 + checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 740 + dependencies = [ 741 + "either", 742 + ] 743 + 744 + [[package]] 745 + name = "jni" 746 + version = "0.21.1" 747 + source = "registry+https://github.com/rust-lang/crates.io-index" 748 + checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 749 + dependencies = [ 750 + "cesu8", 751 + "cfg-if", 752 + "combine", 753 + "jni-sys", 754 + "log", 755 + "thiserror", 756 + "walkdir", 757 + "windows-sys 0.45.0", 758 + ] 759 + 760 + [[package]] 584 761 name = "jni-sys" 585 762 version = "0.3.0" 586 763 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 702 879 ] 703 880 704 881 [[package]] 882 + name = "mach2" 883 + version = "0.4.3" 884 + source = "registry+https://github.com/rust-lang/crates.io-index" 885 + checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" 886 + dependencies = [ 887 + "libc", 888 + ] 889 + 890 + [[package]] 705 891 name = "malloc_buf" 706 892 version = "0.0.6" 707 893 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 748 934 "objc", 749 935 "paste", 750 936 ] 937 + 938 + [[package]] 939 + name = "minimal-lexical" 940 + version = "0.2.1" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 751 943 752 944 [[package]] 753 945 name = "miniz_oxide" ··· 825 1017 dependencies = [ 826 1018 "bitflags 1.3.2", 827 1019 "jni-sys", 828 - "ndk-sys", 1020 + "ndk-sys 0.4.1+23.1.7779620", 829 1021 "num_enum 0.5.11", 830 1022 "raw-window-handle", 831 1023 "thiserror", 832 1024 ] 833 1025 834 1026 [[package]] 1027 + name = "ndk" 1028 + version = "0.8.0" 1029 + source = "registry+https://github.com/rust-lang/crates.io-index" 1030 + checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" 1031 + dependencies = [ 1032 + "bitflags 2.9.3", 1033 + "jni-sys", 1034 + "log", 1035 + "ndk-sys 0.5.0+25.2.9519653", 1036 + "num_enum 0.7.4", 1037 + "thiserror", 1038 + ] 1039 + 1040 + [[package]] 835 1041 name = "ndk-context" 836 1042 version = "0.1.1" 837 1043 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 842 1048 version = "0.4.1+23.1.7779620" 843 1049 source = "registry+https://github.com/rust-lang/crates.io-index" 844 1050 checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" 1051 + dependencies = [ 1052 + "jni-sys", 1053 + ] 1054 + 1055 + [[package]] 1056 + name = "ndk-sys" 1057 + version = "0.5.0+25.2.9519653" 1058 + source = "registry+https://github.com/rust-lang/crates.io-index" 1059 + checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 845 1060 dependencies = [ 846 1061 "jni-sys", 847 1062 ] ··· 872 1087 ] 873 1088 874 1089 [[package]] 1090 + name = "nom" 1091 + version = "7.1.3" 1092 + source = "registry+https://github.com/rust-lang/crates.io-index" 1093 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1094 + dependencies = [ 1095 + "memchr", 1096 + "minimal-lexical", 1097 + ] 1098 + 1099 + [[package]] 1100 + name = "num-derive" 1101 + version = "0.4.2" 1102 + source = "registry+https://github.com/rust-lang/crates.io-index" 1103 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 1104 + dependencies = [ 1105 + "proc-macro2", 1106 + "quote", 1107 + "syn 2.0.106", 1108 + ] 1109 + 1110 + [[package]] 875 1111 name = "num-traits" 876 1112 version = "0.2.19" 877 1113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 899 1135 ] 900 1136 901 1137 [[package]] 1138 + name = "num_enum" 1139 + version = "0.7.4" 1140 + source = "registry+https://github.com/rust-lang/crates.io-index" 1141 + checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" 1142 + dependencies = [ 1143 + "num_enum_derive 0.7.4", 1144 + "rustversion", 1145 + ] 1146 + 1147 + [[package]] 902 1148 name = "num_enum_derive" 903 1149 version = "0.5.11" 904 1150 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 923 1169 ] 924 1170 925 1171 [[package]] 1172 + name = "num_enum_derive" 1173 + version = "0.7.4" 1174 + source = "registry+https://github.com/rust-lang/crates.io-index" 1175 + checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" 1176 + dependencies = [ 1177 + "proc-macro-crate", 1178 + "proc-macro2", 1179 + "quote", 1180 + "syn 2.0.106", 1181 + ] 1182 + 1183 + [[package]] 926 1184 name = "objc" 927 1185 version = "0.2.7" 928 1186 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 977 1235 ] 978 1236 979 1237 [[package]] 1238 + name = "oboe" 1239 + version = "0.6.1" 1240 + source = "registry+https://github.com/rust-lang/crates.io-index" 1241 + checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" 1242 + dependencies = [ 1243 + "jni", 1244 + "ndk 0.8.0", 1245 + "ndk-context", 1246 + "num-derive", 1247 + "num-traits", 1248 + "oboe-sys", 1249 + ] 1250 + 1251 + [[package]] 1252 + name = "oboe-sys" 1253 + version = "0.6.1" 1254 + source = "registry+https://github.com/rust-lang/crates.io-index" 1255 + checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" 1256 + dependencies = [ 1257 + "cc", 1258 + ] 1259 + 1260 + [[package]] 980 1261 name = "once_cell" 981 1262 version = "1.21.3" 982 1263 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1145 1426 ] 1146 1427 1147 1428 [[package]] 1429 + name = "regex" 1430 + version = "1.11.2" 1431 + source = "registry+https://github.com/rust-lang/crates.io-index" 1432 + checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" 1433 + dependencies = [ 1434 + "aho-corasick", 1435 + "memchr", 1436 + "regex-automata", 1437 + "regex-syntax", 1438 + ] 1439 + 1440 + [[package]] 1441 + name = "regex-automata" 1442 + version = "0.4.10" 1443 + source = "registry+https://github.com/rust-lang/crates.io-index" 1444 + checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" 1445 + dependencies = [ 1446 + "aho-corasick", 1447 + "memchr", 1448 + "regex-syntax", 1449 + ] 1450 + 1451 + [[package]] 1452 + name = "regex-syntax" 1453 + version = "0.8.6" 1454 + source = "registry+https://github.com/rust-lang/crates.io-index" 1455 + checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 1456 + 1457 + [[package]] 1148 1458 name = "renderdoc-sys" 1149 1459 version = "1.1.0" 1150 1460 source = "registry+https://github.com/rust-lang/crates.io-index" 1151 1461 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" 1462 + 1463 + [[package]] 1464 + name = "rtrb" 1465 + version = "0.3.2" 1466 + source = "registry+https://github.com/rust-lang/crates.io-index" 1467 + checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" 1152 1468 1153 1469 [[package]] 1154 1470 name = "rustc-demangle" ··· 1197 1513 ] 1198 1514 1199 1515 [[package]] 1516 + name = "same-file" 1517 + version = "1.0.6" 1518 + source = "registry+https://github.com/rust-lang/crates.io-index" 1519 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1520 + dependencies = [ 1521 + "winapi-util", 1522 + ] 1523 + 1524 + [[package]] 1200 1525 name = "scoped-tls" 1201 1526 version = "1.0.1" 1202 1527 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1364 1689 name = "tic80_rust" 1365 1690 version = "0.1.0" 1366 1691 dependencies = [ 1692 + "anyhow", 1693 + "cpal", 1367 1694 "mlua", 1695 + "parking_lot", 1368 1696 "pixels", 1697 + "rtrb", 1369 1698 "winit", 1370 1699 ] 1371 1700 ··· 1455 1784 version = "0.9.5" 1456 1785 source = "registry+https://github.com/rust-lang/crates.io-index" 1457 1786 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1787 + 1788 + [[package]] 1789 + name = "walkdir" 1790 + version = "2.5.0" 1791 + source = "registry+https://github.com/rust-lang/crates.io-index" 1792 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1793 + dependencies = [ 1794 + "same-file", 1795 + "winapi-util", 1796 + ] 1458 1797 1459 1798 [[package]] 1460 1799 name = "wasi" ··· 1792 2131 ] 1793 2132 1794 2133 [[package]] 2134 + name = "windows" 2135 + version = "0.54.0" 2136 + source = "registry+https://github.com/rust-lang/crates.io-index" 2137 + checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" 2138 + dependencies = [ 2139 + "windows-core", 2140 + "windows-targets 0.52.6", 2141 + ] 2142 + 2143 + [[package]] 2144 + name = "windows-core" 2145 + version = "0.54.0" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" 2148 + dependencies = [ 2149 + "windows-result", 2150 + "windows-targets 0.52.6", 2151 + ] 2152 + 2153 + [[package]] 1795 2154 name = "windows-link" 1796 2155 version = "0.1.3" 1797 2156 source = "registry+https://github.com/rust-lang/crates.io-index" 1798 2157 checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2158 + 2159 + [[package]] 2160 + name = "windows-result" 2161 + version = "0.1.2" 2162 + source = "registry+https://github.com/rust-lang/crates.io-index" 2163 + checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 2164 + dependencies = [ 2165 + "windows-targets 0.52.6", 2166 + ] 1799 2167 1800 2168 [[package]] 1801 2169 name = "windows-sys" ··· 2083 2451 "libc", 2084 2452 "log", 2085 2453 "mio", 2086 - "ndk", 2454 + "ndk 0.7.0", 2087 2455 "objc2", 2088 2456 "once_cell", 2089 2457 "orbclient",
+4
tic80_rust/Cargo.toml
··· 7 7 winit = "0.28" 8 8 pixels = "0.14" 9 9 mlua = { version = "0.9", features = ["lua53", "vendored"] } 10 + cpal = "0.15" 11 + rtrb = "0.3" 12 + parking_lot = "0.12" 13 + anyhow = "1"
+157
tic80_rust/src/audio/capture.rs
··· 1 + use std::sync::Arc; 2 + 3 + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 4 + use cpal::{Device, Sample, SampleFormat, Stream, StreamConfig}; 5 + use rtrb::{Consumer, Producer, RingBuffer}; 6 + 7 + #[derive(Clone, Debug)] 8 + pub struct AudioInfo { 9 + pub device_name: String, 10 + pub sample_rate: u32, 11 + pub channels: u16, 12 + } 13 + 14 + #[derive(Clone, Debug, Default)] 15 + pub struct AudioCaptureConfig { 16 + pub device_substr: Option<String>, 17 + pub sample_rate: Option<u32>, 18 + pub ring_capacity: usize, // in samples (mono) 19 + } 20 + 21 + pub struct AudioCaptureHandle { 22 + _stream: Stream, // keep alive 23 + pub info: AudioInfo, 24 + } 25 + 26 + pub fn list_input_devices() -> Vec<String> { 27 + let host = cpal::default_host(); 28 + let mut out = Vec::new(); 29 + if let Ok(devices) = host.input_devices() { 30 + for d in devices { 31 + if let Ok(name) = d.name() { 32 + out.push(name); 33 + } 34 + } 35 + } 36 + out 37 + } 38 + 39 + fn pick_device(substr: Option<&str>) -> Option<Device> { 40 + let host = cpal::default_host(); 41 + if let Some(s) = substr { 42 + if let Ok(devices) = host.input_devices() { 43 + for d in devices { 44 + if let Ok(name) = d.name() { 45 + if name.to_lowercase().contains(&s.to_lowercase()) { 46 + return Some(d); 47 + } 48 + } 49 + } 50 + } 51 + } 52 + host.default_input_device() 53 + } 54 + 55 + pub fn default_ring_capacity() -> usize { 56 + // Shared buffer sized to max(2*FFT_SIZE, VQT_FFT_SIZE) from C impl: 8192 samples. 57 + 8192 58 + } 59 + 60 + pub fn start_capture( 61 + cfg: AudioCaptureConfig, 62 + ) -> anyhow::Result<(AudioCaptureHandle, Consumer<f32>)> { 63 + let device = pick_device(cfg.device_substr.as_deref()) 64 + .ok_or_else(|| anyhow::anyhow!("No input device available"))?; 65 + let device_name = device.name().unwrap_or_else(|_| "<unknown>".to_string()); 66 + 67 + // Choose a supported config close to the requested one 68 + let supported = device 69 + .supported_input_configs() 70 + .map_err(|e| anyhow::anyhow!("Failed to get supported configs: {e}"))?; 71 + let mut chosen: Option<cpal::SupportedStreamConfig> = None; 72 + let req_rate = cfg.sample_rate.unwrap_or(44_100); 73 + for sc in supported { 74 + let sr = sc.min_sample_rate().0..=sc.max_sample_rate().0; 75 + if sr.contains(&req_rate) { 76 + chosen = Some(sc.with_sample_rate(cpal::SampleRate(req_rate))); 77 + break; 78 + } 79 + } 80 + // Fallback to default config 81 + let stream_cfg = if let Some(ch) = chosen { 82 + ch 83 + } else { 84 + device 85 + .default_input_config() 86 + .map_err(|e| anyhow::anyhow!("No default input config: {e}"))? 87 + }; 88 + 89 + let sample_format = stream_cfg.sample_format(); 90 + let cfg_fixed: StreamConfig = stream_cfg.clone().into(); 91 + let channels = cfg_fixed.channels; 92 + let sample_rate = cfg_fixed.sample_rate.0; 93 + 94 + // Ring buffer (mono samples) 95 + let cap = if cfg.ring_capacity == 0 { 96 + default_ring_capacity() 97 + } else { 98 + cfg.ring_capacity 99 + }; 100 + let (prod, cons) = RingBuffer::<f32>::new(cap); 101 + let prod = Arc::new(parking_lot::Mutex::new(prod)); 102 + 103 + let stream = match sample_format { 104 + SampleFormat::F32 => build_stream::<f32>(&device, &cfg_fixed, channels, prod.clone())?, 105 + SampleFormat::I16 => build_stream::<i16>(&device, &cfg_fixed, channels, prod.clone())?, 106 + SampleFormat::U16 => build_stream::<u16>(&device, &cfg_fixed, channels, prod.clone())?, 107 + other => return Err(anyhow::anyhow!("Unsupported sample format: {other:?}")), 108 + }; 109 + 110 + stream.play()?; 111 + 112 + let info = AudioInfo { 113 + device_name, 114 + sample_rate, 115 + channels, 116 + }; 117 + 118 + Ok((AudioCaptureHandle { _stream: stream, info }, cons)) 119 + } 120 + 121 + fn build_stream<T>( 122 + device: &Device, 123 + cfg: &StreamConfig, 124 + channels: u16, 125 + prod: Arc<parking_lot::Mutex<Producer<f32>>>, 126 + ) -> anyhow::Result<Stream> 127 + where 128 + T: Sample + cpal::SizedSample, 129 + f32: cpal::FromSample<T>, 130 + { 131 + let err_fn = |e| eprintln!("Audio input error: {e}"); 132 + let ch = channels as usize; 133 + let stream = device.build_input_stream( 134 + cfg, 135 + move |data: &[T], _| { 136 + // Downmix to mono and push into ring buffer. 137 + // Note: Producer is used only from this audio callback thread. 138 + // The consumer lives on the main/tick thread. We wrap the producer 139 + // in a Mutex solely to satisfy Send/Sync constraints for sharing 140 + // into the callback; there is no contention in practice. 141 + let mut p = prod.lock(); 142 + for frame in data.chunks_exact(ch) { 143 + let mut mono = 0.0f32; 144 + for s in frame.iter() { 145 + let v: f32 = (*s).to_sample(); 146 + mono += v; 147 + } 148 + mono /= ch as f32; 149 + // Drop if full; consumer will catch up. 150 + let _ = p.push(mono); 151 + } 152 + }, 153 + err_fn, 154 + None, 155 + )?; 156 + Ok(stream) 157 + }
+4
tic80_rust/src/lib.rs
··· 9 9 pub mod script { 10 10 pub mod lua_runner; 11 11 } 12 + 13 + pub mod audio { 14 + pub mod capture; 15 + }
+78 -10
tic80_rust/src/main.rs
··· 13 13 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 14 14 use tic80_rust::script::lua_runner::LuaRunner; 15 15 use tic80_rust::core::memory::Memory; 16 + use tic80_rust::audio::capture as audio_cap; 16 17 17 18 // Simple fixed-step ticker at ~60 FPS 18 19 struct Ticker { ··· 59 60 let fb = Rc::new(RefCell::new(Framebuffer::new())); 60 61 let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 61 62 let mut ticker = Ticker::new(); 62 - // Program selection: first CLI arg as .lua script, else embedded default 63 - let args: Vec<String> = std::env::args().skip(1).collect(); 64 - let script = if let Some(first) = args.first() { 65 - if first.ends_with(".lua") && Path::new(first).is_file() { 66 - match fs::read_to_string(first) { 67 - Ok(s) => s, 68 - Err(e) => { 69 - eprintln!("Failed to read {}: {}. Falling back to default cart.", first, e); 70 - DEFAULT_LUA.to_string() 63 + // CLI parsing (minimal): flags + optional .lua path 64 + let mut args_iter = std::env::args().skip(1); 65 + let mut script_path: Option<String> = None; 66 + let mut list_audio = false; 67 + let mut audio_disable = false; 68 + let mut audio_device: Option<String> = None; 69 + let mut audio_vu = false; 70 + while let Some(arg) = args_iter.next() { 71 + match arg.as_str() { 72 + "--list-audio" => list_audio = true, 73 + "--audio-disable" => audio_disable = true, 74 + "--audio-vu" => audio_vu = true, 75 + "--audio-device" => { 76 + if let Some(val) = args_iter.next() { audio_device = Some(val); } 77 + } 78 + other => { 79 + if other.ends_with(".lua") && Path::new(other).is_file() { 80 + script_path = Some(other.to_string()); 71 81 } 72 82 } 83 + } 84 + } 85 + if list_audio { 86 + let list = audio_cap::list_input_devices(); 87 + if list.is_empty() { 88 + println!("No input devices found."); 73 89 } else { 74 - DEFAULT_LUA.to_string() 90 + println!("Input devices:"); 91 + for (i, name) in list.iter().enumerate() { 92 + println!(" {}: {}", i, name); 93 + } 94 + } 95 + return Ok(()); 96 + } 97 + let script = if let Some(path) = script_path.as_ref() { 98 + match fs::read_to_string(path) { 99 + Ok(s) => s, 100 + Err(e) => { 101 + eprintln!("Failed to read {}: {}. Falling back to default cart.", path, e); 102 + DEFAULT_LUA.to_string() 103 + } 75 104 } 76 105 } else { 77 106 DEFAULT_LUA.to_string() 78 107 }; 79 108 let lua_runner = LuaRunner::new(fb.clone(), mem.clone(), &script).ok(); 80 109 110 + // Optional audio capture 111 + struct AudioState { 112 + _handle: audio_cap::AudioCaptureHandle, 113 + cons: rtrb::Consumer<f32>, 114 + vu_enabled: bool, 115 + last_print: Instant, 116 + peak_acc: f32, 117 + } 118 + let mut audio_state: Option<AudioState> = None; 119 + if !audio_disable { 120 + let cap_cfg = audio_cap::AudioCaptureConfig { 121 + device_substr: audio_device.clone(), 122 + sample_rate: Some(44_100), 123 + ring_capacity: audio_cap::default_ring_capacity(), 124 + }; 125 + match audio_cap::start_capture(cap_cfg) { 126 + 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 }); 130 + } 131 + Err(e) => { 132 + eprintln!("Audio capture disabled ({}). Use --audio-disable to silence this.", e); 133 + } 134 + } 135 + } 136 + 81 137 event_loop.run(move |event, _, control_flow| { 82 138 *control_flow = ControlFlow::Poll; 83 139 match event { ··· 101 157 if ticker.should_tick() { 102 158 if let Some(r) = &lua_runner { 103 159 r.tick(); 160 + } 161 + // Simple VU meter from audio ring 162 + 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()); } 165 + if a.vu_enabled && a.last_print.elapsed() >= Duration::from_millis(1000) { 166 + let peak = a.peak_acc.max(1e-9); 167 + let db = 20.0 * peak.log10(); 168 + println!("VU: peak {:.3} ({:.1} dBFS)", peak, db); 169 + a.peak_acc = 0.0; 170 + a.last_print = Instant::now(); 171 + } 104 172 } 105 173 window.request_redraw(); 106 174 }