SPL Meter 0.2.0: one measurement, many views
Every control asked for here is a view choice, so none of them may reach the capture child — two dashboards watching the same room must be able to disagree about how to draw it without disturbing the measurement or each other. That set the shape of the change: the child now sends everything anyone might ask for, once, and each tile folds it down. Averaging (was "Reading") is Fast, Slow, Inf and None, and the child computes and sends all four every frame. Fast and Slow are the IEC time constants, Inf is Leq since reset, None is the block with no time constant at all. The admin-wide time constant is gone — it was never a property of the room. Banding is 1/3 through 1/48 octave. The wire always carries 1/48, and because every banding offered divides 48 a coarser band is an exact sum of finer ones in the power domain. Nothing is resampled and no tile's choice is visible to another. The transform grew to 16384 points to have bins fine enough to mean it; below roughly 200 Hz a 1/48 band is still narrower than one bin, which the Banding help says out loud rather than pretending. Each tile kind now carries its own settings, which is the fix for the complaint that started this: the RTA was offering an alarm threshold and a metric picker that did nothing to it. An entry's instanceSchema replaces the module's, so a gear popover only holds controls that change that tile. Also: frequency labels in three tiers that thin out as a tile narrows instead of smearing, gridlines off by default on both graphs, RTA smoothing as a damping factor on the drawn value only, a title-bar toggle to swap the spectrogram's axes, and the threshold blurb removed. Settings saved by 0.1.0 are mapped forward — a layout on a wall should not start reading dashes because a field was renamed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e90ae51495
commit
d2d4207fe4
+121
-13
@@ -33,6 +33,48 @@ const PING_MS = 15000;
|
||||
/** Set by init(), read by routes() — both are rebuilt together on re-init. */
|
||||
let state = null;
|
||||
|
||||
/* ── shared tile settings ───────────────────────────────────────────── */
|
||||
|
||||
const CHANNEL_FIELD = {
|
||||
type: 'select', label: 'Channel', default: '', optionsRoute: '/channels',
|
||||
};
|
||||
|
||||
/**
|
||||
* Averaging is a view choice, not a measurement one: the capture child
|
||||
* computes all four every frame and sends all four, so one dashboard on Fast
|
||||
* and another on Inf are reading the same measurement two ways.
|
||||
*/
|
||||
function averagingField() {
|
||||
return {
|
||||
type: 'select', label: 'Averaging', default: 'fast',
|
||||
options: [
|
||||
{ value: 'fast', label: 'Fast — 125 ms' },
|
||||
{ value: 'slow', label: 'Slow — 1 s' },
|
||||
{ value: 'inf', label: 'Inf — since reset' },
|
||||
{ value: 'none', label: 'None — no averaging' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Banding folds the 1/48-octave bands on the wire down to something
|
||||
* readable. Coarser is an exact sum of finer, so this costs nothing and
|
||||
* disturbs no other tile.
|
||||
*/
|
||||
const BANDING_FIELD = {
|
||||
type: 'select', label: 'Banding', default: '3',
|
||||
help: 'Finer bandings need a wide tile to be worth drawing, and below '
|
||||
+ 'roughly 200 Hz the window length limits real resolution however fine '
|
||||
+ 'this is set.',
|
||||
options: [
|
||||
{ value: '3', label: '1/3 octave' },
|
||||
{ value: '6', label: '1/6 octave' },
|
||||
{ value: '12', label: '1/12 octave' },
|
||||
{ value: '24', label: '1/24 octave' },
|
||||
{ value: '48', label: '1/48 octave' },
|
||||
],
|
||||
};
|
||||
|
||||
function parseChannels(raw) {
|
||||
return String(raw || '1')
|
||||
.split(',')
|
||||
@@ -88,7 +130,6 @@ module.exports = {
|
||||
device: config.device,
|
||||
channels,
|
||||
weighting: config.weighting || 'A',
|
||||
filter: config.filter || 'fast',
|
||||
bands: config.bands !== false,
|
||||
calibration,
|
||||
moduleDir,
|
||||
@@ -129,20 +170,86 @@ module.exports = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Four tile kinds rather than one tile with a mode switch: an operator
|
||||
* picking "+ SPL" wants to choose what they are putting on the wall, not
|
||||
* place a box and then configure it into being useful.
|
||||
* Four tile kinds, each with only the settings that do something to it.
|
||||
*
|
||||
* An entry's own instanceSchema replaces the module's, which is the point:
|
||||
* an RTA has no use for an alarm threshold and a readout has no use for a
|
||||
* banding, and offering either is how a gear popover stops being read.
|
||||
*
|
||||
* Every setting here is a view choice. None of them reach the capture
|
||||
* child — it already sends every averaging and the finest banding, so two
|
||||
* dashboards can disagree about how to draw the same room.
|
||||
*/
|
||||
tiles() {
|
||||
return [
|
||||
{ id: 'readout', name: 'SPL', description: 'The level, big',
|
||||
defaultSize: { w: 2, h: 1 }, minSize: { w: 1, h: 1 } },
|
||||
{ id: 'bars', name: 'SPL Levels', description: 'One bar per channel, with peak hold',
|
||||
defaultSize: { w: 3, h: 2 }, minSize: { w: 2, h: 1 } },
|
||||
{ id: 'rta', name: 'RTA', description: 'Third-octave bars',
|
||||
defaultSize: { w: 5, h: 3 }, minSize: { w: 3, h: 2 } },
|
||||
{ id: 'spectrogram', name: 'Spectrogram', description: 'Third-octave bands over time',
|
||||
defaultSize: { w: 5, h: 3 }, minSize: { w: 3, h: 2 } },
|
||||
{
|
||||
id: 'readout', name: 'SPL', description: 'The level, big',
|
||||
defaultSize: { w: 2, h: 1 }, minSize: { w: 1, h: 1 },
|
||||
instanceGroups: { Thresholds: { columns: 2 } },
|
||||
instanceSchema: {
|
||||
channel: CHANNEL_FIELD,
|
||||
averaging: averagingField(),
|
||||
metric: {
|
||||
type: 'select', label: 'Show', default: 'level',
|
||||
options: [
|
||||
{ value: 'level', label: 'Level' },
|
||||
{ value: 'leq1m', label: 'Leq, last minute' },
|
||||
{ value: 'leq10m', label: 'Leq, last 10 minutes' },
|
||||
{ value: 'peakC', label: 'Peak, C-weighted' },
|
||||
],
|
||||
},
|
||||
warnAt: { type: 'number', label: 'Amber above', group: 'Thresholds', default: 95 },
|
||||
alarmAt: { type: 'number', label: 'Red above', group: 'Thresholds', default: 100 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'bars', name: 'SPL Levels', description: 'One bar per channel, with peak hold',
|
||||
defaultSize: { w: 3, h: 2 }, minSize: { w: 2, h: 1 },
|
||||
instanceGroups: { Thresholds: { columns: 2 } },
|
||||
instanceSchema: {
|
||||
averaging: averagingField(),
|
||||
peakHold: { type: 'switch', label: 'Peak hold', default: true },
|
||||
warnAt: { type: 'number', label: 'Amber above', group: 'Thresholds', default: 95 },
|
||||
alarmAt: { type: 'number', label: 'Red above', group: 'Thresholds', default: 100 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rta', name: 'RTA', description: 'Spectrum now, at your choice of banding',
|
||||
defaultSize: { w: 5, h: 3 }, minSize: { w: 3, h: 2 },
|
||||
instanceSchema: {
|
||||
channel: CHANNEL_FIELD,
|
||||
averaging: averagingField(),
|
||||
banding: BANDING_FIELD,
|
||||
smoothing: {
|
||||
type: 'select', label: 'Smoothing', default: 'medium',
|
||||
help: 'How hard the bars are damped between frames. Cosmetic only — '
|
||||
+ 'it changes the animation, never the measurement.',
|
||||
options: [
|
||||
{ value: 'off', label: 'Off — every frame as measured' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'heavy', label: 'Heavy' },
|
||||
],
|
||||
},
|
||||
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'spectrogram', name: 'Spectrogram', description: 'Spectrum over time',
|
||||
defaultSize: { w: 5, h: 3 }, minSize: { w: 3, h: 2 },
|
||||
instanceSchema: {
|
||||
channel: CHANNEL_FIELD,
|
||||
banding: BANDING_FIELD,
|
||||
axis: {
|
||||
type: 'select', label: 'Time runs', default: 'x',
|
||||
options: [
|
||||
{ value: 'x', label: 'Left to right' },
|
||||
{ value: 'y', label: 'Top to bottom' },
|
||||
],
|
||||
},
|
||||
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -250,8 +357,9 @@ function snapshot() {
|
||||
calibrated: state.calibrated,
|
||||
channels: state.channels,
|
||||
weighting: state.meta ? state.meta.weighting : '',
|
||||
filter: state.meta ? state.meta.filter : '',
|
||||
bandCentres: state.meta ? state.meta.bands : [],
|
||||
bandN0: state.meta ? state.meta.bandN0 : 0,
|
||||
bandsPerOctave: state.meta ? state.meta.bandsPerOctave : 48,
|
||||
frame: state.frame,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user