SPL Meter: measure the room, not just watch it #10
+21
-13
@@ -210,7 +210,7 @@ function readoutView(root, moduleApi) {
|
|||||||
*/
|
*/
|
||||||
function fit() {
|
function fit() {
|
||||||
const w = root.clientWidth, h = root.clientHeight;
|
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;
|
if (key === fitKey || w < 2 || h < 2) return;
|
||||||
fitKey = key;
|
fitKey = key;
|
||||||
|
|
||||||
@@ -257,6 +257,7 @@ function readoutView(root, moduleApi) {
|
|||||||
const parts = [`${payload.weighting || '?'} · ${label}`, `ch ${ch.i}`];
|
const parts = [`${payload.weighting || '?'} · ${label}`, `ch ${ch.i}`];
|
||||||
if (ch.clip) parts.push('CLIP');
|
if (ch.clip) parts.push('CLIP');
|
||||||
if (!payload.calibrated) parts.push('uncalibrated');
|
if (!payload.calibrated) parts.push('uncalibrated');
|
||||||
|
sub.hidden = s.caption === false;
|
||||||
sub.textContent = parts.join(' · ');
|
sub.textContent = parts.join(' · ');
|
||||||
fit();
|
fit();
|
||||||
},
|
},
|
||||||
@@ -331,18 +332,22 @@ function spectrumView(root, moduleApi, scrolling) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scrolling) {
|
if (scrolling) {
|
||||||
// A title-bar toggle so the axis can be flipped while watching, without
|
// The axis lives here and nowhere else (rule 11): a tile does not offer
|
||||||
// opening the gear. It writes the same setting the gear does, so the two
|
// the same action twice. Icon only, tooltip carries the words (rule 9).
|
||||||
// never disagree — but saveInstanceSettings deliberately does not remount
|
// saveInstanceSettings deliberately does not remount us, so the new
|
||||||
// us, so the new value is held here too and read back by axis().
|
// 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({
|
axisBtn = moduleApi.header.addButton({
|
||||||
label: '⇆',
|
icon: swapIcon,
|
||||||
title: 'Swap axes',
|
title: describe(),
|
||||||
onClick() {
|
onClick() {
|
||||||
axisOverride = axis() === 'x' ? 'y' : 'x';
|
axisOverride = axis() === 'x' ? 'y' : 'x';
|
||||||
moduleApi.saveInstanceSettings({ axis: axisOverride });
|
moduleApi.saveInstanceSettings({ axis: axisOverride });
|
||||||
painted = false; // the picture means nothing in the other orientation
|
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;
|
const freqs = folded.freq;
|
||||||
let values = folded.db;
|
let values = folded.db;
|
||||||
const grid = s.gridlines === true;
|
const grid = s.gridlines === true;
|
||||||
const axisPad = 13;
|
const showLabels = s.labels !== false;
|
||||||
|
const axisPad = showLabels || grid ? 13 : 0;
|
||||||
|
|
||||||
if (!scrolling) {
|
if (!scrolling) {
|
||||||
// Damp the bars toward the measurement. Cosmetic: `values` is what
|
// 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.fillRect(i * bw, plotH - bh, Math.max(1, bw - (bw > 3 ? 1 : 0)), bh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showLabels) {
|
||||||
ctx.fillStyle = muted; ctx.font = '9px system-ui'; ctx.textAlign = 'center';
|
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);
|
for (const l of labels) ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── spectrogram ──
|
// ── spectrogram ──
|
||||||
const timeX = axis() === 'x';
|
const timeX = axis() === 'x';
|
||||||
const colW = 2;
|
const colW = 2;
|
||||||
const gutter = grid ? axisPad : 0;
|
const gutter = showLabels ? axisPad : 0;
|
||||||
// Frequency occupies whichever axis time does not.
|
// Frequency occupies whichever axis time does not.
|
||||||
const freqExtent = timeX ? h - gutter : w - gutter;
|
const freqExtent = timeX ? h - gutter : w - gutter;
|
||||||
const timeExtent = timeX ? w : h;
|
const timeExtent = timeX ? w : h;
|
||||||
@@ -489,7 +497,7 @@ function spectrumView(root, moduleApi, scrolling) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grid) {
|
if (grid || showLabels) {
|
||||||
const posOf = (f) => {
|
const posOf = (f) => {
|
||||||
const t = Math.log2(f / freqs[0]) / Math.log2(freqs[freqs.length - 1] / freqs[0]);
|
const t = Math.log2(f / freqs[0]) / Math.log2(freqs[freqs.length - 1] / freqs[0]);
|
||||||
return timeX ? freqExtent - t * freqExtent : gutter + t * freqExtent;
|
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);
|
if (timeX) ctx.fillRect(0, h - gutter, w, gutter);
|
||||||
else ctx.fillRect(0, 0, gutter, h);
|
else ctx.fillRect(0, 0, gutter, h);
|
||||||
ctx.fillStyle = muted; ctx.font = '9px system-ui';
|
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) {
|
for (const l of labels) {
|
||||||
if (timeX) {
|
if (timeX) {
|
||||||
const y = Math.round(l.pos) + 0.5;
|
const y = Math.round(l.pos) + 0.5;
|
||||||
@@ -511,7 +519,7 @@ function spectrumView(root, moduleApi, scrolling) {
|
|||||||
}
|
}
|
||||||
ctx.stroke(); ctx.globalAlpha = 1;
|
ctx.stroke(); ctx.globalAlpha = 1;
|
||||||
ctx.textAlign = timeX ? 'left' : 'center';
|
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));
|
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);
|
else ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "spl-meter",
|
"id": "spl-meter",
|
||||||
"name": "SPL Meter",
|
"name": "SPL Meter",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"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": [
|
||||||
|
|||||||
+11
-20
@@ -48,10 +48,10 @@ function averagingField() {
|
|||||||
return {
|
return {
|
||||||
type: 'select', label: 'Averaging', default: 'fast',
|
type: 'select', label: 'Averaging', default: 'fast',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'fast', label: 'Fast — 125 ms' },
|
{ value: 'fast', label: 'Fast · 125 ms' },
|
||||||
{ value: 'slow', label: 'Slow — 1 s' },
|
{ value: 'slow', label: 'Slow · 1 s' },
|
||||||
{ value: 'inf', label: 'Inf — since reset' },
|
{ value: 'inf', label: 'Inf · since reset' },
|
||||||
{ value: 'none', label: 'None — no averaging' },
|
{ value: 'none', label: 'None' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -63,9 +63,6 @@ function averagingField() {
|
|||||||
*/
|
*/
|
||||||
const BANDING_FIELD = {
|
const BANDING_FIELD = {
|
||||||
type: 'select', label: 'Banding', default: '3',
|
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: [
|
options: [
|
||||||
{ value: '3', label: '1/3 octave' },
|
{ value: '3', label: '1/3 octave' },
|
||||||
{ value: '6', label: '1/6 octave' },
|
{ value: '6', label: '1/6 octave' },
|
||||||
@@ -193,11 +190,12 @@ module.exports = {
|
|||||||
type: 'select', label: 'Show', default: 'level',
|
type: 'select', label: 'Show', default: 'level',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'level', label: 'Level' },
|
{ value: 'level', label: 'Level' },
|
||||||
{ value: 'leq1m', label: 'Leq, last minute' },
|
{ value: 'leq1m', label: 'Leq 1 min' },
|
||||||
{ value: 'leq10m', label: 'Leq, last 10 minutes' },
|
{ value: 'leq10m', label: 'Leq 10 min' },
|
||||||
{ value: 'peakC', label: 'Peak, C-weighted' },
|
{ value: 'peakC', label: 'Peak C' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
caption: { type: 'switch', label: 'Caption', default: true },
|
||||||
warnAt: { type: 'number', label: 'Amber above', group: 'Thresholds', default: 95 },
|
warnAt: { type: 'number', label: 'Amber above', group: 'Thresholds', default: 95 },
|
||||||
alarmAt: { type: 'number', label: 'Red above', group: 'Thresholds', default: 100 },
|
alarmAt: { type: 'number', label: 'Red above', group: 'Thresholds', default: 100 },
|
||||||
},
|
},
|
||||||
@@ -222,15 +220,14 @@ module.exports = {
|
|||||||
banding: BANDING_FIELD,
|
banding: BANDING_FIELD,
|
||||||
smoothing: {
|
smoothing: {
|
||||||
type: 'select', label: 'Smoothing', default: 'medium',
|
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: [
|
options: [
|
||||||
{ value: 'off', label: 'Off — every frame as measured' },
|
{ value: 'off', label: 'Off' },
|
||||||
{ value: 'light', label: 'Light' },
|
{ value: 'light', label: 'Light' },
|
||||||
{ value: 'medium', label: 'Medium' },
|
{ value: 'medium', label: 'Medium' },
|
||||||
{ value: 'heavy', label: 'Heavy' },
|
{ value: 'heavy', label: 'Heavy' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
labels: { type: 'switch', label: 'Labels', default: true },
|
||||||
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -240,13 +237,7 @@ module.exports = {
|
|||||||
instanceSchema: {
|
instanceSchema: {
|
||||||
channel: CHANNEL_FIELD,
|
channel: CHANNEL_FIELD,
|
||||||
banding: BANDING_FIELD,
|
banding: BANDING_FIELD,
|
||||||
axis: {
|
labels: { type: 'switch', label: 'Labels', default: true },
|
||||||
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 },
|
gridlines: { type: 'switch', label: 'Gridlines', default: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user