SPL Meter: measure the room, not just watch it #10

Merged
michael merged 7 commits from claude/spl-meter-module into main 2026-09-17 16:18:17 -04:00
2 changed files with 16 additions and 5 deletions
Showing only changes of commit 87929bfd0b - Show all commits
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "spl-meter", "id": "spl-meter",
"name": "SPL Meter", "name": "SPL Meter",
"version": "0.3.0", "version": "0.3.1",
"proddash": ">=1.5.0", "proddash": ">=1.5.0",
"description": "Calibrated sound level, Leq and RTA from an audio input on this machine", "description": "Calibrated sound level, Leq and RTA from an audio input on this machine",
"permissions": [ "permissions": [
+15 -4
View File
@@ -132,6 +132,7 @@ module.exports = {
moduleDir, moduleDir,
log, log,
onUpdate(update) { onUpdate(update) {
if (!state) return; // a frame in flight when the module unmounted
if (update.error) { if (update.error) {
if (update.error !== state.error) log(update.error); if (update.error !== state.error) log(update.error);
state.error = update.error; state.error = update.error;
@@ -146,9 +147,10 @@ module.exports = {
}, },
}); });
const mineStreams = state.streams;
state.pinger = setInterval(() => { state.pinger = setInterval(() => {
for (const res of state.streams) { for (const res of mineStreams) {
try { res.write(': ping\n\n'); } catch { state.streams.delete(res); } try { res.write(': ping\n\n'); } catch { mineStreams.delete(res); }
} }
}, PING_MS); }, PING_MS);
@@ -260,8 +262,14 @@ module.exports = {
try { try {
res.write(`event: state\ndata: ${JSON.stringify(snapshot())}\n\n`); res.write(`event: state\ndata: ${JSON.stringify(snapshot())}\n\n`);
} catch { return; } } catch { return; }
state.streams.add(res); // Hold this mount's set, not `state`. teardown() nulls `state` and
req.on('close', () => state.streams.delete(res)); // then ends every stream, which fires these close handlers — a
// handler that reached through `state` threw on null and took the
// whole server down with it, on any re-init with a browser
// connected. Deleting from a set nobody reads any more is harmless.
const streams = state.streams;
streams.add(res);
req.on('close', () => streams.delete(res));
}, },
/** Device list for the admin page. Runs the capture tool's --list, /** Device list for the admin page. Runs the capture tool's --list,
@@ -311,6 +319,7 @@ module.exports = {
const samples = []; const samples = [];
const started = Date.now(); const started = Date.now();
const tick = setInterval(() => { const tick = setInterval(() => {
if (!state) { clearInterval(tick); return; } // module unmounted mid-measure
const ch = state.frame && state.frame.ch.find((c) => c.i === wanted); const ch = state.frame && state.frame.ch.find((c) => c.i === wanted);
if (ch && Number.isFinite(ch.dbfs) && ch.dbfs > -200) samples.push(ch.dbfs); if (ch && Number.isFinite(ch.dbfs) && ch.dbfs > -200) samples.push(ch.dbfs);
if (Date.now() - started < MEASURE_MS) return; if (Date.now() - started < MEASURE_MS) return;
@@ -343,6 +352,7 @@ module.exports = {
/* ── helpers ────────────────────────────────────────────────────────── */ /* ── helpers ────────────────────────────────────────────────────────── */
function snapshot() { function snapshot() {
if (!state) return { error: 'not measuring', calibrated: false, channels: [], frame: null };
return { return {
error: state.error, error: state.error,
calibrated: state.calibrated, calibrated: state.calibrated,
@@ -356,6 +366,7 @@ function snapshot() {
} }
function broadcast() { function broadcast() {
if (!state) return; // a listener callback landing after teardown
const payload = snapshot(); const payload = snapshot();
const line = JSON.stringify(payload); const line = JSON.stringify(payload);
if (line === state.lastSent) return; // nothing moved since last frame if (line === state.lastSent) return; // nothing moved since last frame