feat: shared spectrum scope with audible anomalies, from real sources only
Replaces two ad-hoc canvases (a 256x110 fixed-size waterfall in the radio panel, a bar-graph in the EVP panel) with one source-agnostic SpectrumScope that renders any binned dB spectrum, plus sonification so anomalies can be heard rather than watched. Sources are real measured data only. The RTL-SDR path is genuine RF; the EVP path is a genuine AnalyserNode FFT of the device microphone. The ESP32 deliberately does NOT feed this: its firmware reports four scalars (temperature, pressure, evp level, presence) and has no spectrum at all, and device_anomaly.frequency_for_sensor_type() invents a per-sensor-type frequency for the fiction — neither is a real spectrum, so neither is plotted as one. SpectrumScope draws three layers because each answers a different question: the live trace (what is happening now), a decaying peak-hold (what was strongest recently, so a transient survives a glance away), and a waterfall (what the last minute looked like, where a steady carrier separates from a one-off burst). Anomaly markers flare at their frequency and fade over ~2.6s, so a spike already gone from the trace still says where to look. Frames reach the scope through a ref, not a prop. Routing 60Hz frames through React state re-renders the panel and the scope on every frame on top of the rAF loop that actually draws — measurably the wrong call on a phone. The EVP producer double-buffers into two fixed Float64Arrays so a frame costs zero allocation. spectrumSonify maps band position to pitch exponentially, so equal fractions of the band are equal musical intervals (a linear Hz map crams the bottom half into one indistinguishable octave), and magnitude to gain via sqrt so faint hits stay audible. Pings are throttled to 90ms because a busy band otherwise smears into a buzz that conveys nothing. Every entry point no-ops rather than throwing when audio is unavailable — a dead speaker must never take down the scope drawing the data. Audio requires an explicit gesture (ListenToggle), because iOS keeps any context created outside a touch handler permanently suspended. Also exposes EvpListener.sampleRate/nyquistHz and labels the EVP axis from the real hardware rate. The old code assumed 48kHz; Bluetooth headsets and some Android inputs hand back 44.1k or 16k, which mislabelled the spectrum by nearly an octave. Mobile: DPR-aware canvases, ResizeObserver, 44px touch targets, touch-action so dragging the scope pans the page, reduced-motion honoured, crowded axis ticks dropped under 560px. 329 frontend tests pass (18 new); i18n en/es parity gate passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,8 @@
|
||||
"deniedTitle": "the microphone stays shut",
|
||||
"denied": "This vessel refused the microphone. Grant audio permission in your browser to practice EVP — or let the Wire Ghost listen instead; it needs nothing but the network you already fear.",
|
||||
"floor": "room floor",
|
||||
"band": "voice band 300–3400 Hz"
|
||||
"band": "voice band 300–3400 Hz",
|
||||
"scopeLabel": "live audio spectrum from the microphone"
|
||||
},
|
||||
"radio": {
|
||||
"start": "sweep the band",
|
||||
@@ -149,7 +150,8 @@
|
||||
"claimFailed": "the dongle refused to be claimed — is it in use elsewhere?",
|
||||
"claimFix": "linux: the kernel driver dvb_usb_rtl28xxu is holding the dongle — `sudo modprobe -r dvb_usb_rtl28xxu` frees it until reboot (a udev rule banishes it for good). windows: swap the driver for WinUSB with Zadig.",
|
||||
"tryAgain": "try again",
|
||||
"errorTitle": "the static pushed back"
|
||||
"errorTitle": "the static pushed back",
|
||||
"scopeLabel": "live radio spectrum from the receiver"
|
||||
},
|
||||
"ouija": {
|
||||
"spelling": "the planchette spells",
|
||||
|
||||
@@ -134,7 +134,8 @@
|
||||
"deniedTitle": "el micrófono sigue cerrado",
|
||||
"denied": "Este recipiente rechazó el micrófono. Concede el permiso de audio en tu navegador para practicar EVP — o deja que el Fantasma del Cable escuche en tu lugar; no necesita más que la red que ya temes.",
|
||||
"floor": "suelo de la sala",
|
||||
"band": "banda de voz 300–3400 Hz"
|
||||
"band": "banda de voz 300–3400 Hz",
|
||||
"scopeLabel": "espectro de audio en vivo desde el micrófono"
|
||||
},
|
||||
"radio": {
|
||||
"start": "barrer la banda",
|
||||
@@ -149,7 +150,8 @@
|
||||
"claimFailed": "el dongle se negó a ser reclamado — ¿está en uso en otro lugar?",
|
||||
"claimFix": "linux: el driver del kernel dvb_usb_rtl28xxu retiene el dongle — `sudo modprobe -r dvb_usb_rtl28xxu` lo libera hasta el reinicio (una regla udev lo destierra para siempre). windows: cambia el driver a WinUSB con Zadig.",
|
||||
"tryAgain": "intenta de nuevo",
|
||||
"errorTitle": "la estática devolvió el golpe"
|
||||
"errorTitle": "la estática devolvió el golpe",
|
||||
"scopeLabel": "espectro de radio en vivo desde el receptor"
|
||||
},
|
||||
"ouija": {
|
||||
"spelling": "la planchette deletrea",
|
||||
|
||||
Reference in New Issue
Block a user