From 87aec3f4d1755fe57527b36a2fe75a5d6a6fa031 Mon Sep 17 00:00:00 2001 From: serversdown Date: Wed, 27 May 2026 23:08:21 +0000 Subject: [PATCH] viewers: smoother mic dBL chart + restore binary/TXT download links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues spotted in the modal: 1. Mic dBL chart looked spikey/discontinuous — isolated bars at 80-95 with gaps in between. Cause: _psiToDbl() returns null for zero or negative samples, and most mic samples on a quiet event sit at the digitization noise floor where they're effectively zero. Result: the chart only renders the moments when instantaneous SPL exceeded the Y-axis bottom — looks like a sound trigger gate. Fix: new _psiToDblForChart() rectifies the AC waveform (abs), then converts to dBL, then floors at MIC_DBL_FLOOR=60 dBL. Chart now has a continuous 60 dBL baseline with peaks above it — matches how acoustic engineers expect SPL-vs-time. Y-axis bottom pinned to MIC_DBL_FLOOR, top to peak + 5 dB headroom. Peak label still uses the unrectified _psiToDbl so the displayed peak value is exact. 2. Filename in Source/Files block was unlinked. Endpoint exists (/db/events/{id}/blastware_file) — just wasn't wired to the modal. Made it a clickable download link. Same treatment for the preserved .TXT — added "(download .TXT)" link next to source kind when source.txt_filename is populated (events ingested after the .TXT preservation feature landed; older events show no link). Applied to both the inline modal in sfm_webapp.html and the standalone /events page in event_browser.html. Co-Authored-By: Claude Opus 4.7 (1M context) --- sfm/event_browser.html | 24 ++++++++++++++- sfm/sfm_webapp.html | 70 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/sfm/event_browser.html b/sfm/event_browser.html index 30542af..9be5431 100644 --- a/sfm/event_browser.html +++ b/sfm/event_browser.html @@ -356,6 +356,18 @@ function _psiToDbl(psi) { return 20 * Math.log10(psi / DBL_REF); } +// Per-sample mic chart conversion — rectify the AC waveform, dBL, +// floor below the noise-floor minimum. Gives a continuous baseline +// instead of the spikey/discontinuous look you get from raw _psiToDbl. +const MIC_DBL_FLOOR = 60; +function _psiToDblForChart(psi) { + if (psi == null) return MIC_DBL_FLOOR; + const a = Math.abs(psi); + if (a === 0) return MIC_DBL_FLOOR; + const dbl = 20 * Math.log10(a / DBL_REF); + return dbl > MIC_DBL_FLOOR ? dbl : MIC_DBL_FLOOR; +} + // Format an ISO timestamp in the browser's local timezone — UTC values // (with 'Z' suffix) convert; naive values are interpreted as local clock. // Returns '—' for null/empty/unparseable. @@ -638,7 +650,10 @@ function renderWaveform(data) { let peak = chData.peak; const peakT = chData.peak_t_ms; if (ch === 'MicL' && unit === 'psi' && micUnit === 'dBL') { - values = values.map(_psiToDbl); + // Per-sample chart uses rectified-and-floored conversion so the + // baseline is continuous; the peak label uses the unrectified + // converter to preserve the true measurement. + values = values.map(_psiToDblForChart); peak = _psiToDbl(peak); unit = 'dB(L)'; } @@ -711,6 +726,13 @@ function renderWaveform(data) { } const padded = (absMax || 1) * 1.10; yBounds = { min: -padded, max: padded }; + } else if (ch === 'MicL' && micUnit === 'dBL') { + // Baseline at noise-floor minimum (matches what we floored + // null/quiet samples to), top at peak + 5 dB headroom. + const peakDbl = (typeof peak === 'number' && isFinite(peak)) + ? peak + 5 + : 100; + yBounds = { min: MIC_DBL_FLOOR, max: Math.max(peakDbl, MIC_DBL_FLOOR + 20) }; } const chart = new Chart(canvas, { diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html index 856c8f5..19a3e05 100644 --- a/sfm/sfm_webapp.html +++ b/sfm/sfm_webapp.html @@ -2589,6 +2589,23 @@ function _psiToDbl(psi) { return 20 * Math.log10(psi / DBL_REF); } +// Per-sample mic display floor. Sound pressure AC samples spend most +// of their time at the digitization noise floor (1-2 ADC counts ≈ ~20-40 +// dBL). Rendering each one as null/-inf produces a spikey discontinuous +// chart of "moments when sound briefly exceeded 80 dBL" — confusing. +// Instead we rectify (abs the AC waveform), convert to dBL, and floor +// anything below MIC_DBL_FLOOR so the chart has a continuous baseline +// with peaks rising above it. Matches how acoustic engineers expect to +// see SPL-vs-time. +const MIC_DBL_FLOOR = 60; +function _psiToDblForChart(psi) { + if (psi == null) return MIC_DBL_FLOOR; + const a = Math.abs(psi); + if (a === 0) return MIC_DBL_FLOOR; + const dbl = 20 * Math.log10(a / DBL_REF); + return dbl > MIC_DBL_FLOOR ? dbl : MIC_DBL_FLOOR; +} + // Adaptive decimal formatter — scientific notation is reserved for truly // extreme values (10000+ or sub-0.0001). Normal-range values (most peaks // fall here) render as decimals with sensible precision. Replaces the @@ -2649,10 +2666,14 @@ function _renderScWaveform(data) { let chPeak = chData.peak; // Mic channel: convert from raw psi to dB(L) when user prefers dBL - // (default). Mic samples that are zero/negative become null (Chart.js - // renders them as gaps in line mode, zero-height bars in histogram mode). + // (default). Per-sample values use _psiToDblForChart which rectifies + // (abs) the AC waveform and floors at MIC_DBL_FLOOR so the chart is + // continuous with a baseline + peaks above it, instead of a sparse + // pattern of isolated spikes for "moments when sound briefly exceeded + // the Y-axis bottom". The peak label uses _psiToDbl with the + // unrectified peak (preserves the true measurement). if (ch === 'MicL' && chUnit === 'psi' && micUnit === 'dBL') { - values = values.map(_psiToDbl); + values = values.map(_psiToDblForChart); chPeak = _psiToDbl(chPeak); chUnit = 'dB(L)'; } @@ -2736,6 +2757,13 @@ function _renderScWaveform(data) { } const padded = (absMax || 1) * 1.10; yBounds = { min: -padded, max: padded }; + } else if (ch === 'MicL' && micUnit === 'dBL') { + // Pin baseline at the chart floor (which matches what we flooded + // null/quiet samples to), top at the actual peak + a few dB headroom. + const peakDbl = (typeof chPeak === 'number' && isFinite(chPeak)) + ? chPeak + 5 + : 100; + yBounds = { min: MIC_DBL_FLOOR, max: Math.max(peakDbl, MIC_DBL_FLOOR + 20) }; } _scCharts[ch] = new Chart(canvas, { @@ -2882,10 +2910,42 @@ function _renderSidecar(data) { document.getElementById('sc-f-operator').textContent = pi.operator || '—'; document.getElementById('sc-f-loc').textContent = pi.sensor_location || '—'; - document.getElementById('sc-f-bw').textContent = bw.filename || '—'; + // Filename rendered as a clickable download link for the original BW + // binary. Same endpoint the live-device viewer uses for stored events + // (/db/events/{id}/blastware_file). + const bwCell = document.getElementById('sc-f-bw'); + bwCell.innerHTML = ''; + if (bw.filename && _scCurrentEventId) { + const a = document.createElement('a'); + a.href = `${api()}/db/events/${_scCurrentEventId}/blastware_file`; + a.textContent = bw.filename; + a.download = bw.filename; + a.title = 'Download original BW event binary'; + a.style.color = 'var(--accent, #58a6ff)'; + a.style.textDecoration = 'underline'; + bwCell.appendChild(a); + } else { + bwCell.textContent = '—'; + } document.getElementById('sc-f-bwsize').textContent = bw.filesize != null ? `${bw.filesize} bytes` : '—'; document.getElementById('sc-f-sha').textContent = bw.sha256 || '—'; - document.getElementById('sc-f-src').textContent = src.kind || '—'; + // Source kind + a download link for the preserved BW ASCII report + // (.TXT), when available. Only events ingested after 2026-05-27 + // have the .TXT preserved; older events show "—". + const srcCell = document.getElementById('sc-f-src'); + srcCell.innerHTML = ''; + srcCell.appendChild(document.createTextNode(src.kind || '—')); + if (src.txt_filename && _scCurrentEventId) { + const a = document.createElement('a'); + a.href = `${api()}/db/events/${_scCurrentEventId}/ascii_report.txt`; + a.textContent = ' (download .TXT)'; + a.download = src.txt_filename; + a.title = 'Download preserved BW ASCII report'; + a.style.color = 'var(--accent, #58a6ff)'; + a.style.marginLeft = '8px'; + a.style.fontSize = '11px'; + srcCell.appendChild(a); + } // captured_at has a "Z" suffix (UTC); _fmtTs converts to browser local // — matches the BW-reported recorded-at, no more "21:59:57 vs it's 6 PM" // confusion from operators reading the raw UTC value.