SPL Meter 0.3.0: bring the tiles to the interface rules
Rebasing onto main landed rules 5 to 11, which this module predates and broke in three places. Rule 5 — no explanatory text in a tile's gear: the Banding and Smoothing fields each carried a paragraph. The Banding one said something true and worth knowing, that below roughly 200 Hz the window length limits real resolution however fine the setting, so it moves to the source where the band plan is built rather than being lost. Option labels shortened with it. Rule 11 — one control per action: the spectrogram offered its axis both as a gear field and as a title-bar button. The button stays, since that is what was asked for, and the field is gone. The setting still persists with the tile; saveInstanceSettings keeps keys that no schema declares. Rule 9 — title-bar controls are icons: the axis button was drawing a "⇆" as a label. It is an inline SVG now, with the tooltip carrying the words and naming the current direction rather than the action. Rule 8 also asks that a tile can switch off what it draws, so the readout gained a Caption toggle and both graphs gained Labels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
543795f9fd
commit
44fd0b10ba
+23
-15
@@ -210,7 +210,7 @@ function readoutView(root, moduleApi) {
|
||||
*/
|
||||
function fit() {
|
||||
const w = root.clientWidth, h = root.clientHeight;
|
||||
const key = `${w}x${h}:${num.textContent.length}:${unit.textContent}`;
|
||||
const key = `${w}x${h}:${num.textContent.length}:${unit.textContent}:${sub.hidden}`;
|
||||
if (key === fitKey || w < 2 || h < 2) return;
|
||||
fitKey = key;
|
||||
|
||||
@@ -257,6 +257,7 @@ function readoutView(root, moduleApi) {
|
||||
const parts = [`${payload.weighting || '?'} · ${label}`, `ch ${ch.i}`];
|
||||
if (ch.clip) parts.push('CLIP');
|
||||
if (!payload.calibrated) parts.push('uncalibrated');
|
||||
sub.hidden = s.caption === false;
|
||||
sub.textContent = parts.join(' · ');
|
||||
fit();
|
||||
},
|
||||
@@ -331,18 +332,22 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
}
|
||||
|
||||
if (scrolling) {
|
||||
// A title-bar toggle so the axis can be flipped while watching, without
|
||||
// opening the gear. It writes the same setting the gear does, so the two
|
||||
// never disagree — but saveInstanceSettings deliberately does not remount
|
||||
// us, so the new value is held here too and read back by axis().
|
||||
// The axis lives here and nowhere else (rule 11): a tile does not offer
|
||||
// the same action twice. Icon only, tooltip carries the words (rule 9).
|
||||
// saveInstanceSettings deliberately does not remount us, so the new
|
||||
// value is held here as well and read back by axis().
|
||||
const swapIcon = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" '
|
||||
+ 'stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">'
|
||||
+ '<path d="M2 5h9M8.5 2 11.5 5 8.5 8"/><path d="M14 11H5M7.5 8 4.5 11 7.5 14"/></svg>';
|
||||
const describe = () => (axis() === 'x' ? 'Time left to right' : 'Time top to bottom');
|
||||
axisBtn = moduleApi.header.addButton({
|
||||
label: '⇆',
|
||||
title: 'Swap axes',
|
||||
icon: swapIcon,
|
||||
title: describe(),
|
||||
onClick() {
|
||||
axisOverride = axis() === 'x' ? 'y' : 'x';
|
||||
moduleApi.saveInstanceSettings({ axis: axisOverride });
|
||||
painted = false; // the picture means nothing in the other orientation
|
||||
axisBtn.title = axis() === 'x' ? 'Time left to right' : 'Time top to bottom';
|
||||
axisBtn.title = describe();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -403,7 +408,8 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
const freqs = folded.freq;
|
||||
let values = folded.db;
|
||||
const grid = s.gridlines === true;
|
||||
const axisPad = 13;
|
||||
const showLabels = s.labels !== false;
|
||||
const axisPad = showLabels || grid ? 13 : 0;
|
||||
|
||||
if (!scrolling) {
|
||||
// Damp the bars toward the measurement. Cosmetic: `values` is what
|
||||
@@ -450,15 +456,17 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
ctx.fillRect(i * bw, plotH - bh, Math.max(1, bw - (bw > 3 ? 1 : 0)), bh);
|
||||
}
|
||||
|
||||
ctx.fillStyle = muted; ctx.font = '9px system-ui'; ctx.textAlign = 'center';
|
||||
for (const l of labels) ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
||||
if (showLabels) {
|
||||
ctx.fillStyle = muted; ctx.font = '9px system-ui'; ctx.textAlign = 'center';
|
||||
for (const l of labels) ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── spectrogram ──
|
||||
const timeX = axis() === 'x';
|
||||
const colW = 2;
|
||||
const gutter = grid ? axisPad : 0;
|
||||
const gutter = showLabels ? axisPad : 0;
|
||||
// Frequency occupies whichever axis time does not.
|
||||
const freqExtent = timeX ? h - gutter : w - gutter;
|
||||
const timeExtent = timeX ? w : h;
|
||||
@@ -489,7 +497,7 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
}
|
||||
}
|
||||
|
||||
if (grid) {
|
||||
if (grid || showLabels) {
|
||||
const posOf = (f) => {
|
||||
const t = Math.log2(f / freqs[0]) / Math.log2(freqs[freqs.length - 1] / freqs[0]);
|
||||
return timeX ? freqExtent - t * freqExtent : gutter + t * freqExtent;
|
||||
@@ -499,7 +507,7 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
if (timeX) ctx.fillRect(0, h - gutter, w, gutter);
|
||||
else ctx.fillRect(0, 0, gutter, h);
|
||||
ctx.fillStyle = muted; ctx.font = '9px system-ui';
|
||||
ctx.strokeStyle = border; ctx.globalAlpha = 0.35; ctx.beginPath();
|
||||
ctx.strokeStyle = border; ctx.globalAlpha = grid ? 0.35 : 0; ctx.beginPath();
|
||||
for (const l of labels) {
|
||||
if (timeX) {
|
||||
const y = Math.round(l.pos) + 0.5;
|
||||
@@ -511,7 +519,7 @@ function spectrumView(root, moduleApi, scrolling) {
|
||||
}
|
||||
ctx.stroke(); ctx.globalAlpha = 1;
|
||||
ctx.textAlign = timeX ? 'left' : 'center';
|
||||
for (const l of labels) {
|
||||
for (const l of showLabels ? labels : []) {
|
||||
if (timeX) ctx.fillText(hzLabel(l.f), 2, Math.min(h - gutter - 2, l.pos + 3));
|
||||
else ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "spl-meter",
|
||||
"name": "SPL Meter",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"proddash": ">=1.5.0",
|
||||
"description": "Calibrated sound level, Leq and RTA from an audio input on this machine",
|
||||
"permissions": [
|
||||
|
||||
+11
-20
@@ -48,10 +48,10 @@ 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' },
|
||||
{ value: 'fast', label: 'Fast · 125 ms' },
|
||||
{ value: 'slow', label: 'Slow · 1 s' },
|
||||
{ value: 'inf', label: 'Inf · since reset' },
|
||||
{ value: 'none', label: 'None' },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -63,9 +63,6 @@ function averagingField() {
|
||||
*/
|
||||
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' },
|
||||
@@ -193,11 +190,12 @@ module.exports = {
|
||||
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' },
|
||||
{ value: 'leq1m', label: 'Leq 1 min' },
|
||||
{ value: 'leq10m', label: 'Leq 10 min' },
|
||||
{ value: 'peakC', label: 'Peak C' },
|
||||
],
|
||||
},
|
||||
caption: { type: 'switch', label: 'Caption', default: true },
|
||||
warnAt: { type: 'number', label: 'Amber above', group: 'Thresholds', default: 95 },
|
||||
alarmAt: { type: 'number', label: 'Red above', group: 'Thresholds', default: 100 },
|
||||
},
|
||||
@@ -222,15 +220,14 @@ module.exports = {
|
||||
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: 'off', label: 'Off' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'heavy', label: 'Heavy' },
|
||||
],
|
||||
},
|
||||
labels: { type: 'switch', label: 'Labels', default: true },
|
||||
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||
},
|
||||
},
|
||||
@@ -240,13 +237,7 @@ module.exports = {
|
||||
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' },
|
||||
],
|
||||
},
|
||||
labels: { type: 'switch', label: 'Labels', default: true },
|
||||
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user