viewers: smoother mic dBL chart + restore binary/TXT download links
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) <noreply@anthropic.com>
This commit is contained in:
+65
-5
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user