SPL Meter 0.4.1: name a dead input, and stop the spectrogram smearing its labels #16
+85
-37
@@ -350,7 +350,18 @@ function spectrumView(root, moduleApi, scrolling) {
|
|||||||
root.innerHTML = '<canvas class="spl-canvas"></canvas>';
|
root.innerHTML = '<canvas class="spl-canvas"></canvas>';
|
||||||
const canvas = root.querySelector('.spl-canvas');
|
const canvas = root.querySelector('.spl-canvas');
|
||||||
const ctx = canvas.getContext('2d', { alpha: false });
|
const ctx = canvas.getContext('2d', { alpha: false });
|
||||||
let painted = false; // spectrogram: has the background been laid down
|
/**
|
||||||
|
* The spectrogram's scrolling history, on its own surface.
|
||||||
|
*
|
||||||
|
* It used to scroll by copying the visible canvas onto itself, which also
|
||||||
|
* copied whatever had been drawn on top of it — so every frame dragged the
|
||||||
|
* frequency labels and gridlines one step further along and left a smear.
|
||||||
|
* Only measured pixels belong on a surface that scrolls; annotations are
|
||||||
|
* drawn fresh over the top of it, every frame, and never into it.
|
||||||
|
*/
|
||||||
|
let history = null;
|
||||||
|
let historyCtx = null;
|
||||||
|
let painted = false; // spectrogram: has the history been cleared yet
|
||||||
let smoothed = null; // RTA: displayed values, damped toward the real ones
|
let smoothed = null; // RTA: displayed values, damped toward the real ones
|
||||||
let smoothedFor = '';
|
let smoothedFor = '';
|
||||||
let axisBtn = null;
|
let axisBtn = null;
|
||||||
@@ -497,62 +508,99 @@ function spectrumView(root, moduleApi, scrolling) {
|
|||||||
// ── spectrogram ──
|
// ── spectrogram ──
|
||||||
const timeX = axis() === 'x';
|
const timeX = axis() === 'x';
|
||||||
const colW = 2;
|
const colW = 2;
|
||||||
const gutter = showLabels ? axisPad : 0;
|
// A label along the bottom needs only its line height; one down the
|
||||||
// Frequency occupies whichever axis time does not.
|
// side has to fit "12.5k" beside the plot rather than on top of it.
|
||||||
const freqExtent = timeX ? h - gutter : w - gutter;
|
const gutter = (showLabels || grid) ? (timeX ? 30 : axisPad) : 0;
|
||||||
const timeExtent = timeX ? w : h;
|
// Labels belong beside the frequency axis: down the left when frequency
|
||||||
|
// runs vertically, along the bottom when it runs horizontally.
|
||||||
|
const plotX = timeX ? gutter : 0;
|
||||||
|
const plotW = timeX ? w - gutter : w;
|
||||||
|
const plotH = timeX ? h : h - gutter;
|
||||||
|
const freqExtent = timeX ? plotH : plotW;
|
||||||
|
const timeExtent = timeX ? plotW : plotH;
|
||||||
|
if (plotW < 4 || plotH < 4) return;
|
||||||
|
|
||||||
if (!painted) { ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h); painted = true; }
|
// The history surface is sized to the plot, in device pixels, and is
|
||||||
|
// thrown away whenever that geometry changes — a scroll buffer means
|
||||||
// Source rect in device pixels, destination in CSS pixels: the context
|
// nothing once it is being stretched into a different shape.
|
||||||
// carries a devicePixelRatio transform but the canvas as an image
|
const dpr = window.devicePixelRatio || 1;
|
||||||
// source does not, and letting drawImage infer them scales the picture
|
const needW = Math.max(1, Math.round(plotW * dpr));
|
||||||
// up and pushes every column off the canvas.
|
const needH = Math.max(1, Math.round(plotH * dpr));
|
||||||
if (timeX) {
|
if (!history || history.width !== needW || history.height !== needH) {
|
||||||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, -colW, 0, w, h);
|
history = document.createElement('canvas');
|
||||||
} else {
|
history.width = needW;
|
||||||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, -colW, w, h);
|
history.height = needH;
|
||||||
|
historyCtx = history.getContext('2d', { alpha: false });
|
||||||
|
historyCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
painted = false;
|
||||||
}
|
}
|
||||||
|
if (!painted) {
|
||||||
|
historyCtx.fillStyle = bg;
|
||||||
|
historyCtx.fillRect(0, 0, plotW, plotH);
|
||||||
|
painted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll the history, then add the newest column. The source rectangle
|
||||||
|
// is in device pixels and the destination in CSS pixels, because the
|
||||||
|
// context carries the dpr transform but the canvas as an image source
|
||||||
|
// does not.
|
||||||
|
historyCtx.drawImage(history, 0, 0, history.width, history.height,
|
||||||
|
timeX ? -colW : 0, timeX ? 0 : -colW, plotW, plotH);
|
||||||
|
|
||||||
const bandExtent = freqExtent / values.length;
|
const bandExtent = freqExtent / values.length;
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
ctx.fillStyle = heat(norm(values[i], payload.calibrated));
|
historyCtx.fillStyle = heat(norm(values[i], payload.calibrated));
|
||||||
if (timeX) {
|
if (timeX) {
|
||||||
// Low frequencies at the bottom, as every spectrogram has them.
|
// Low frequencies at the bottom, as every spectrogram has them.
|
||||||
ctx.fillRect(timeExtent - colW, freqExtent - (i + 1) * bandExtent,
|
historyCtx.fillRect(timeExtent - colW, freqExtent - (i + 1) * bandExtent,
|
||||||
colW, Math.ceil(bandExtent) + 1);
|
colW, Math.ceil(bandExtent) + 1);
|
||||||
} else {
|
} else {
|
||||||
// Low frequencies at the left.
|
// Low frequencies at the left.
|
||||||
ctx.fillRect(gutter + i * bandExtent, timeExtent - colW,
|
historyCtx.fillRect(i * bandExtent, timeExtent - colW,
|
||||||
Math.ceil(bandExtent) + 1, colW);
|
Math.ceil(bandExtent) + 1, colW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grid || showLabels) {
|
// Repaint the visible canvas from scratch every frame: history first,
|
||||||
const posOf = (f) => {
|
// annotations over it. Nothing drawn here can ever reach the scroll.
|
||||||
const t = Math.log2(f / freqs[0]) / Math.log2(freqs[freqs.length - 1] / freqs[0]);
|
ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
|
||||||
return timeX ? freqExtent - t * freqExtent : gutter + t * freqExtent;
|
ctx.drawImage(history, 0, 0, history.width, history.height,
|
||||||
};
|
plotX, 0, plotW, plotH);
|
||||||
const labels = chooseLabels(freqs, posOf, timeX ? freqExtent : w, timeX ? 16 : 34);
|
|
||||||
ctx.fillStyle = bg;
|
if (!grid && !showLabels) return;
|
||||||
if (timeX) ctx.fillRect(0, h - gutter, w, gutter);
|
|
||||||
else ctx.fillRect(0, 0, gutter, h);
|
const span = Math.log2(freqs[freqs.length - 1] / freqs[0]);
|
||||||
ctx.fillStyle = muted; ctx.font = '9px system-ui';
|
const posOf = (f) => {
|
||||||
ctx.strokeStyle = border; ctx.globalAlpha = grid ? 0.35 : 0; ctx.beginPath();
|
const t = Math.log2(f / freqs[0]) / span;
|
||||||
|
return timeX ? plotH - t * plotH : plotX + t * plotW;
|
||||||
|
};
|
||||||
|
const labels = chooseLabels(freqs, posOf, timeX ? plotH : w, timeX ? 14 : 34);
|
||||||
|
|
||||||
|
if (grid) {
|
||||||
|
ctx.strokeStyle = border; ctx.lineWidth = 1; ctx.globalAlpha = 0.35;
|
||||||
|
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;
|
||||||
ctx.moveTo(0, y); ctx.lineTo(w, y);
|
ctx.moveTo(plotX, y); ctx.lineTo(w, y);
|
||||||
} else {
|
} else {
|
||||||
const x = Math.round(l.pos) + 0.5;
|
const x = Math.round(l.pos) + 0.5;
|
||||||
ctx.moveTo(x, 0); ctx.lineTo(x, h);
|
ctx.moveTo(x, 0); ctx.lineTo(x, plotH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.stroke(); ctx.globalAlpha = 1;
|
ctx.stroke(); ctx.globalAlpha = 1;
|
||||||
ctx.textAlign = timeX ? 'left' : 'center';
|
}
|
||||||
for (const l of showLabels ? labels : []) {
|
|
||||||
if (timeX) ctx.fillText(hzLabel(l.f), 2, Math.min(h - gutter - 2, l.pos + 3));
|
if (showLabels) {
|
||||||
else ctx.fillText(hzLabel(l.f), l.pos, h - 3);
|
ctx.fillStyle = muted; ctx.font = '9px system-ui';
|
||||||
|
ctx.textAlign = timeX ? 'right' : 'center';
|
||||||
|
for (const l of labels) {
|
||||||
|
if (timeX) {
|
||||||
|
ctx.fillText(hzLabel(l.f), gutter - 2,
|
||||||
|
Math.max(7, Math.min(h - 2, l.pos + 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.4.0",
|
"version": "0.4.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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user