refactor: extract typed spectrum math
This commit is contained in:
@@ -461,6 +461,69 @@
|
||||
return Math.max(stepBw, Math.round(clamped / stepBw) * stepBw);
|
||||
}
|
||||
|
||||
// src/features/spectrum/math.ts
|
||||
function isNumericBins(value) {
|
||||
return Array.isArray(value) ? value.every((item) => typeof item === "number") : ArrayBuffer.isView(value) && !(value instanceof DataView);
|
||||
}
|
||||
var base64Lookup = new Uint8Array(128).fill(255);
|
||||
var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (let index = 0; index < base64Alphabet.length; index += 1) {
|
||||
base64Lookup[base64Alphabet.charCodeAt(index)] = index;
|
||||
}
|
||||
var spectrumBinBuffer = new Int8Array(0);
|
||||
function decodeBase64Int8(value) {
|
||||
let end = value.length;
|
||||
while (end > 0 && value.charCodeAt(end - 1) === 61) end -= 1;
|
||||
const outputLength = end * 3 >>> 2;
|
||||
if (spectrumBinBuffer.length !== outputLength) spectrumBinBuffer = new Int8Array(outputLength);
|
||||
let outputIndex = 0;
|
||||
for (let index = 0; index < end; ) {
|
||||
const sextets = [0, 0, 0, 0];
|
||||
for (let offset = 0; offset < 4 && index < end; offset += 1, index += 1) {
|
||||
const code = value.charCodeAt(index);
|
||||
const decoded = code < base64Lookup.length ? base64Lookup[code] : void 0;
|
||||
if (decoded === void 0 || decoded === 255) throw new TypeError("Invalid base64 spectrum frame");
|
||||
sextets[offset] = decoded;
|
||||
}
|
||||
const packed = (sextets[0] ?? 0) << 18 | (sextets[1] ?? 0) << 12 | (sextets[2] ?? 0) << 6 | (sextets[3] ?? 0);
|
||||
if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed >> 16 & 255;
|
||||
if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed >> 8 & 255;
|
||||
if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed & 255;
|
||||
}
|
||||
return spectrumBinBuffer;
|
||||
}
|
||||
var nthScratch = new Float64Array(0);
|
||||
function nthElement(values, target) {
|
||||
if (values.length === 0 || target < 0 || target >= values.length) return null;
|
||||
if (nthScratch.length < values.length) nthScratch = new Float64Array(values.length);
|
||||
for (let index = 0; index < values.length; index += 1) nthScratch[index] = values[index] ?? 0;
|
||||
let low = 0;
|
||||
let high = values.length - 1;
|
||||
while (low < high) {
|
||||
const pivot = nthScratch[low + (high - low >> 1)] ?? 0;
|
||||
let left = low;
|
||||
let right = high;
|
||||
while (left <= right) {
|
||||
while ((nthScratch[left] ?? Infinity) < pivot) left += 1;
|
||||
while ((nthScratch[right] ?? -Infinity) > pivot) right -= 1;
|
||||
if (left <= right) {
|
||||
const temporary = nthScratch[left] ?? 0;
|
||||
nthScratch[left] = nthScratch[right] ?? 0;
|
||||
nthScratch[right] = temporary;
|
||||
left += 1;
|
||||
right -= 1;
|
||||
}
|
||||
}
|
||||
if (right < target) low = left;
|
||||
if (target < left) high = right;
|
||||
}
|
||||
return nthScratch[target] ?? null;
|
||||
}
|
||||
function estimateNoiseFloorDb(bins) {
|
||||
if (!isNumericBins(bins) || bins.length === 0) return null;
|
||||
return nthElement(bins, Math.floor(bins.length * 0.15));
|
||||
}
|
||||
|
||||
// src/app.js
|
||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||
var authRole = null;
|
||||
@@ -1814,7 +1877,7 @@
|
||||
return { startIdx, endIdx };
|
||||
}
|
||||
function pushOverviewWaterfallFrame(data) {
|
||||
if (!overviewCanvas || !data || !isBinsArray(data.bins) || data.bins.length === 0) return;
|
||||
if (!overviewCanvas || !data || !isNumericBins(data.bins) || data.bins.length === 0) return;
|
||||
overviewWaterfallRows.push(data.bins.slice());
|
||||
overviewWaterfallPushCount++;
|
||||
trimOverviewWaterfallRows();
|
||||
@@ -1869,7 +1932,7 @@
|
||||
overviewWfTexHeight = iH;
|
||||
ensureWaterfallLut(pal, minDb, maxDb);
|
||||
function renderRow(dstY, srcBins) {
|
||||
if (!isBinsArray(srcBins) || srcBins.length === 0) return;
|
||||
if (!isNumericBins(srcBins) || srcBins.length === 0) return;
|
||||
const { startIdx, endIdx } = overviewVisibleBinWindow(lastSpectrumData, srcBins.length);
|
||||
const spanBins = Math.max(1, endIdx - startIdx);
|
||||
const rowBase = dstY * rowStride;
|
||||
@@ -2440,7 +2503,7 @@
|
||||
});
|
||||
}
|
||||
function spectrumBinIndexForHz(data, hz) {
|
||||
if (!data || !isBinsArray(data.bins) || data.bins.length < 2 || !Number.isFinite(hz)) {
|
||||
if (!data || !isNumericBins(data.bins) || data.bins.length < 2 || !Number.isFinite(hz)) {
|
||||
return null;
|
||||
}
|
||||
const maxIdx = data.bins.length - 1;
|
||||
@@ -2454,7 +2517,7 @@
|
||||
return 10 ** (clamped / 10);
|
||||
}
|
||||
function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) {
|
||||
if (!data || !isBinsArray(data.bins) || data.bins.length < 16) {
|
||||
if (!data || !isNumericBins(data.bins) || data.bins.length < 16) {
|
||||
return null;
|
||||
}
|
||||
if (!Number.isFinite(freqHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) {
|
||||
@@ -4590,7 +4653,7 @@ ${unsupportedBandSummary()}`;
|
||||
currentStyle,
|
||||
cssColorToRgba,
|
||||
rgbaWithAlpha,
|
||||
isBinsArray,
|
||||
isBinsArray: isNumericBins,
|
||||
estimateNoiseFloorDb,
|
||||
spectrumVisibleRange,
|
||||
drawSpectrum,
|
||||
@@ -4938,7 +5001,7 @@ ${unsupportedBandSummary()}`;
|
||||
if (!sdrSquelchSupported) return;
|
||||
let pct = 0;
|
||||
const data = lastSpectrumData || window.lastSpectrumData;
|
||||
if (data && isBinsArray(data.bins) && data.bins.length > 0) {
|
||||
if (data && isNumericBins(data.bins) && data.bins.length > 0) {
|
||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
||||
if (noiseDb != null && Number.isFinite(noiseDb)) {
|
||||
const thresholdDb = noiseDb + 6;
|
||||
@@ -6129,34 +6192,6 @@ ${unsupportedBandSummary()}`;
|
||||
var waterfallGamma = 1;
|
||||
var SPECTRUM_HEADROOM_DB = 20;
|
||||
var SPECTRUM_SMOOTH_ALPHA = 0.42;
|
||||
var _b64Lut = new Uint8Array(128);
|
||||
for (let i = 0; i < 128; i++) _b64Lut[i] = 255;
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("").forEach((c, i) => {
|
||||
_b64Lut[c.charCodeAt(0)] = i;
|
||||
});
|
||||
var _spectrumBinI8 = new Int8Array(0);
|
||||
function isBinsArray(v) {
|
||||
return Array.isArray(v) || ArrayBuffer.isView(v);
|
||||
}
|
||||
function decodeBase64ToInt8(b64) {
|
||||
let end = b64.length;
|
||||
while (end > 0 && b64.charCodeAt(end - 1) === 61) end--;
|
||||
const outLen = end * 3 >>> 2;
|
||||
if (_spectrumBinI8.length !== outLen) _spectrumBinI8 = new Int8Array(outLen);
|
||||
const out = _spectrumBinI8;
|
||||
let j = 0;
|
||||
for (let i = 0; i < end; ) {
|
||||
const a = _b64Lut[b64.charCodeAt(i++)];
|
||||
const b = i < end ? _b64Lut[b64.charCodeAt(i++)] : 0;
|
||||
const c = i < end ? _b64Lut[b64.charCodeAt(i++)] : 0;
|
||||
const d = i < end ? _b64Lut[b64.charCodeAt(i++)] : 0;
|
||||
const n = a << 18 | b << 12 | c << 6 | d;
|
||||
if (j < outLen) out[j++] = n >> 16 & 255;
|
||||
if (j < outLen) out[j++] = n >> 8 & 255;
|
||||
if (j < outLen) out[j++] = n & 255;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
var spectrumCrosshairX = null;
|
||||
var spectrumCrosshairY = null;
|
||||
var _bwDragEdge = null;
|
||||
@@ -6231,13 +6266,13 @@ ${unsupportedBandSummary()}`;
|
||||
let removeCount = 0;
|
||||
for (let i = 0; i < spectrumPeakHoldFrames.length; i++) {
|
||||
const f = spectrumPeakHoldFrames[i];
|
||||
if (f && isBinsArray(f.bins) && now - f.t <= holdMs) break;
|
||||
if (f && isNumericBins(f.bins) && now - f.t <= holdMs) break;
|
||||
removeCount++;
|
||||
}
|
||||
if (removeCount > 0) spectrumPeakHoldFrames.splice(0, removeCount);
|
||||
}
|
||||
function pushSpectrumPeakHoldFrame(frame) {
|
||||
if (!frame || !isBinsArray(frame.bins) || frame.bins.length === 0) {
|
||||
if (!frame || !isNumericBins(frame.bins) || frame.bins.length === 0) {
|
||||
clearSpectrumPeakHoldFrames();
|
||||
return;
|
||||
}
|
||||
@@ -6256,56 +6291,26 @@ ${unsupportedBandSummary()}`;
|
||||
}
|
||||
function buildSpectrumPeakHoldBins(currentBins) {
|
||||
const holdMs = Math.max(0, Number.isFinite(overviewPeakHoldMs) ? overviewPeakHoldMs : 0);
|
||||
if (holdMs <= 0 || !isBinsArray(currentBins) || currentBins.length === 0) {
|
||||
if (holdMs <= 0 || !isNumericBins(currentBins) || currentBins.length === 0) {
|
||||
return null;
|
||||
}
|
||||
pruneSpectrumPeakHoldFrames();
|
||||
if (spectrumPeakHoldFrames.length === 0) return null;
|
||||
const peakBins = currentBins.slice();
|
||||
for (const frame of spectrumPeakHoldFrames) {
|
||||
if (!frame || !isBinsArray(frame.bins) || frame.bins.length !== peakBins.length) continue;
|
||||
if (!frame || !isNumericBins(frame.bins) || frame.bins.length !== peakBins.length) continue;
|
||||
for (let i = 0; i < peakBins.length; i++) {
|
||||
if (frame.bins[i] > peakBins[i]) peakBins[i] = frame.bins[i];
|
||||
}
|
||||
}
|
||||
return peakBins;
|
||||
}
|
||||
function estimateNoiseFloorDb(bins) {
|
||||
if (!isBinsArray(bins) || bins.length === 0) return null;
|
||||
const k = Math.floor(bins.length * 0.15);
|
||||
return nthElement(bins, k);
|
||||
}
|
||||
function nthElement(arr, k) {
|
||||
const tmp = _nthScratch.length >= arr.length ? _nthScratch : new Float64Array(arr.length);
|
||||
if (tmp.length > _nthScratch.length) _nthScratch = tmp;
|
||||
for (let i = 0; i < arr.length; i++) tmp[i] = arr[i];
|
||||
let lo = 0, hi = arr.length - 1;
|
||||
while (lo < hi) {
|
||||
const pivot = tmp[lo + (hi - lo >> 1)];
|
||||
let i = lo, j = hi;
|
||||
while (i <= j) {
|
||||
while (tmp[i] < pivot) i++;
|
||||
while (tmp[j] > pivot) j--;
|
||||
if (i <= j) {
|
||||
const t = tmp[i];
|
||||
tmp[i] = tmp[j];
|
||||
tmp[j] = t;
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
}
|
||||
if (j < k) lo = i;
|
||||
if (k < i) hi = j;
|
||||
}
|
||||
return tmp[k];
|
||||
}
|
||||
var _nthScratch = new Float64Array(0);
|
||||
var _smoothBins = [];
|
||||
function buildSpectrumRenderData(frame) {
|
||||
if (!frame || !isBinsArray(frame.bins)) return frame;
|
||||
if (!frame || !isNumericBins(frame.bins)) return frame;
|
||||
const n = frame.bins.length;
|
||||
const prev = lastSpectrumRenderData;
|
||||
const canBlend = prev && isBinsArray(prev.bins) && prev.bins.length === n && prev.sample_rate === frame.sample_rate && prev.center_hz === frame.center_hz;
|
||||
const canBlend = prev && isNumericBins(prev.bins) && prev.bins.length === n && prev.sample_rate === frame.sample_rate && prev.center_hz === frame.center_hz;
|
||||
if (_smoothBins.length !== n) _smoothBins = new Array(n);
|
||||
const src = frame.bins;
|
||||
if (canBlend) {
|
||||
@@ -6339,7 +6344,7 @@ ${unsupportedBandSummary()}`;
|
||||
return range.visLoHz + cssX / cssW * range.visSpanHz;
|
||||
}
|
||||
function nearestSpectrumPeak(cssX, cssW, data) {
|
||||
if (!data || !isBinsArray(data.bins) || data.bins.length === 0 || cssW <= 0) {
|
||||
if (!data || !isNumericBins(data.bins) || data.bins.length === 0 || cssW <= 0) {
|
||||
return null;
|
||||
}
|
||||
const bins = data.bins;
|
||||
@@ -6400,7 +6405,7 @@ ${unsupportedBandSummary()}`;
|
||||
return nearestSpectrumPeakHz(cssX, cssW, data) ?? Math.round(canvasXToHz(cssX, cssW, range));
|
||||
}
|
||||
function visibleSpectrumPeakIndices(data, limit = 24) {
|
||||
if (!data || !isBinsArray(data.bins) || data.bins.length < 3) {
|
||||
if (!data || !isNumericBins(data.bins) || data.bins.length < 3) {
|
||||
return [];
|
||||
}
|
||||
const bins = data.bins;
|
||||
@@ -6470,7 +6475,7 @@ ${unsupportedBandSummary()}`;
|
||||
const sampleRate = Number(evt.data.slice(commaA + 1, commaB));
|
||||
const b64 = evt.data.slice(commaB + 1);
|
||||
const hadSpectrum = !!lastSpectrumData;
|
||||
const bins = decodeBase64ToInt8(b64);
|
||||
const bins = decodeBase64Int8(b64);
|
||||
const rds = lastSpectrumData?.rds;
|
||||
lastSpectrumData = { bins, center_hz: centerHz, sample_rate: sampleRate, rds };
|
||||
window.lastSpectrumData = lastSpectrumData;
|
||||
@@ -6914,7 +6919,7 @@ ${unsupportedBandSummary()}`;
|
||||
spectrumTmpFillPoints.push(binX(i), binYFromBins(bins, i));
|
||||
}
|
||||
spectrumGl.drawFilledArea(spectrumTmpFillPoints, H, cssColorToRgba(pal.spectrumFill));
|
||||
if (isBinsArray(peakHoldBins) && peakHoldBins.length === n) {
|
||||
if (isNumericBins(peakHoldBins) && peakHoldBins.length === n) {
|
||||
spectrumTmpPeakPoints.length = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
spectrumTmpPeakPoints.push(binX(i), binYFromBins(peakHoldBins, i));
|
||||
@@ -7011,7 +7016,7 @@ ${unsupportedBandSummary()}`;
|
||||
window.addEventListener("resize", _updateCachedCanvasSizes);
|
||||
_updateCachedCanvasSizes();
|
||||
function pushSpectrumWaterfallFrame(data) {
|
||||
if (!spectrumWaterfallCanvas || !data || !isBinsArray(data.bins) || data.bins.length === 0) return;
|
||||
if (!spectrumWaterfallCanvas || !data || !isNumericBins(data.bins) || data.bins.length === 0) return;
|
||||
spectrumWfRows.push(data.bins.slice());
|
||||
spectrumWfPushCount++;
|
||||
trimSpectrumWaterfallRows();
|
||||
@@ -7067,7 +7072,7 @@ ${unsupportedBandSummary()}`;
|
||||
spectrumWfTexHeight = iH;
|
||||
ensureWaterfallLut(pal, minDb, maxDb);
|
||||
function renderRow(dstY, srcBins) {
|
||||
if (!isBinsArray(srcBins) || srcBins.length === 0) return;
|
||||
if (!isNumericBins(srcBins) || srcBins.length === 0) return;
|
||||
const { startIdx, endIdx } = overviewVisibleBinWindow(lastSpectrumData, srcBins.length);
|
||||
const spanBins = Math.max(1, endIdx - startIdx);
|
||||
const rowBase = dstY * rowStride;
|
||||
@@ -7528,7 +7533,7 @@ ${unsupportedBandSummary()}`;
|
||||
} else {
|
||||
let auto = 30;
|
||||
const data = lastSpectrumData || window.lastSpectrumData;
|
||||
if (data && isBinsArray(data.bins) && data.bins.length > 0) {
|
||||
if (data && isNumericBins(data.bins) && data.bins.length > 0) {
|
||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
||||
if (noiseDb != null && Number.isFinite(noiseDb)) {
|
||||
const thresholdDb = noiseDb + 6;
|
||||
|
||||
Reference in New Issue
Block a user