Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aefd36c4b1 | ||
|
|
25b9c31c9b |
@@ -2244,6 +2244,7 @@ function formatSigStrength(dbm) {
|
|||||||
return `${dbfs.toFixed(1)} ${sigUnit("dBFS")}`;
|
return `${dbfs.toFixed(1)} ${sigUnit("dBFS")}`;
|
||||||
}
|
}
|
||||||
function refreshSigStrengthDisplay() {
|
function refreshSigStrengthDisplay() {
|
||||||
|
renderSdrSquelch();
|
||||||
if (!sigStrengthEl) return;
|
if (!sigStrengthEl) return;
|
||||||
sigStrengthEl.innerHTML = formatSigStrength(sigLastDbm);
|
sigStrengthEl.innerHTML = formatSigStrength(sigLastDbm);
|
||||||
}
|
}
|
||||||
@@ -6209,11 +6210,14 @@ var modeControlsRow = document.getElementById("mode-controls-row");
|
|||||||
var samStereoWidthEl = document.getElementById("sam-stereo-width");
|
var samStereoWidthEl = document.getElementById("sam-stereo-width");
|
||||||
var samCarrierSyncEl = document.getElementById("sam-carrier-sync");
|
var samCarrierSyncEl = document.getElementById("sam-carrier-sync");
|
||||||
var sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
|
var sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
|
||||||
var sdrSquelchEl = document.getElementById("sdr-squelch");
|
var sdrSquelchDbEl = document.getElementById("sdr-squelch-db");
|
||||||
var sdrSquelchPctEl = document.getElementById("sdr-squelch-pct");
|
var sdrSquelchStateEl = document.getElementById("sdr-squelch-state");
|
||||||
|
var sdrSquelchToggleBtn = document.getElementById("sdr-squelch-toggle");
|
||||||
|
var squelchLineEl = document.getElementById("spectrum-squelch-line");
|
||||||
|
var squelchGripEl = document.getElementById("spectrum-squelch-grip");
|
||||||
|
var squelchLabelEl = document.getElementById("spectrum-squelch-label");
|
||||||
var SDR_SQUELCH_MIN_DB = -120;
|
var SDR_SQUELCH_MIN_DB = -120;
|
||||||
var SDR_SQUELCH_MAX_DB = -30;
|
var SDR_SQUELCH_MAX_DB = -30;
|
||||||
var syncFromServerSdrSquelch = false;
|
|
||||||
var sdrNbWrapEl = document.getElementById("sdr-nb-wrap");
|
var sdrNbWrapEl = document.getElementById("sdr-nb-wrap");
|
||||||
var sdrNbEnabledEl = document.getElementById("sdr-nb-enabled");
|
var sdrNbEnabledEl = document.getElementById("sdr-nb-enabled");
|
||||||
var sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-controls");
|
var sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-controls");
|
||||||
@@ -6302,89 +6306,162 @@ function normalizeWfmDenoiseLevel(value) {
|
|||||||
if (next === "off" || next === "auto" || next === "low" || next === "medium" || next === "high") return next;
|
if (next === "off" || next === "auto" || next === "low" || next === "medium" || next === "high") return next;
|
||||||
return "auto";
|
return "auto";
|
||||||
}
|
}
|
||||||
function clampSdrSquelchPercent(value) {
|
var sdrSquelchEnabled = loadSetting("sdrSquelchEnabled", false);
|
||||||
if (!isFiniteNumber(value)) return 0;
|
var sdrSquelchThresholdDb = clampSdrSquelchDb(Number(loadSetting("sdrSquelchThresholdDb", -95)));
|
||||||
return Math.max(0, Math.min(100, Math.round(value)));
|
function clampSdrSquelchDb(value) {
|
||||||
|
if (!isFiniteNumber(value)) return SDR_SQUELCH_MIN_DB;
|
||||||
|
return Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, Math.round(value)));
|
||||||
}
|
}
|
||||||
function sdrSquelchPercentToServer(percent) {
|
function sdrSquelchIsPassing() {
|
||||||
const pct = clampSdrSquelchPercent(percent);
|
if (!sdrSquelchEnabled) return true;
|
||||||
if (pct <= 0) {
|
if (!isFiniteNumber(sigLastDbm)) return false;
|
||||||
return { enabled: false, thresholdDb: SDR_SQUELCH_MIN_DB };
|
return sigLastDbm >= sdrSquelchThresholdDb;
|
||||||
|
}
|
||||||
|
function renderSdrSquelch() {
|
||||||
|
if (sdrSquelchDbEl && document.activeElement !== sdrSquelchDbEl) {
|
||||||
|
sdrSquelchDbEl.value = String(sdrSquelchThresholdDb);
|
||||||
}
|
}
|
||||||
const ratio = pct / 100;
|
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
||||||
const thresholdDb = SDR_SQUELCH_MIN_DB + ratio * (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
if (sdrSquelchToggleBtn) {
|
||||||
return { enabled: true, thresholdDb };
|
sdrSquelchToggleBtn.textContent = sdrSquelchEnabled ? "On" : "Off";
|
||||||
|
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
||||||
|
}
|
||||||
|
const state = !sdrSquelchEnabled ? "off" : sdrSquelchIsPassing() ? "open" : "closed";
|
||||||
|
if (sdrSquelchStateEl) {
|
||||||
|
sdrSquelchStateEl.dataset.state = state;
|
||||||
|
sdrSquelchStateEl.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
state === "off" ? "Squelch off" : state === "open" ? "Squelch open" : "Squelch closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
||||||
|
if (squelchGripEl) squelchGripEl.setAttribute("aria-valuenow", String(sdrSquelchThresholdDb));
|
||||||
|
if (squelchLineEl) squelchLineEl.dataset.state = state === "open" ? "open" : "closed";
|
||||||
|
positionSquelchLine();
|
||||||
}
|
}
|
||||||
function sdrSquelchServerToPercent(enabled, thresholdDb) {
|
function positionSquelchLine() {
|
||||||
if (!enabled) return 0;
|
if (!squelchLineEl) return;
|
||||||
if (!isFiniteNumber(thresholdDb)) return 0;
|
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||||
const ratio = (thresholdDb - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
const canvas = document.getElementById("spectrum-canvas");
|
||||||
return clampSdrSquelchPercent(ratio * 100);
|
const visible = sdrSquelchSupported && sdrSquelchEnabled && mode !== "WFM" && !!canvas && canvas.clientHeight > 0 && getComputedStyle(requiredElement("spectrum-panel")).display !== "none";
|
||||||
|
if (!visible) {
|
||||||
|
squelchLineEl.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dbMin = spectrumFloor;
|
||||||
|
const dbMax = spectrumFloor + spectrumRange;
|
||||||
|
const frac = Math.max(0, Math.min(1, (sdrSquelchThresholdDb - dbMin) / Math.max(1, dbMax - dbMin)));
|
||||||
|
squelchLineEl.style.display = "";
|
||||||
|
squelchLineEl.style.top = `${canvas.offsetTop + canvas.clientHeight * (1 - frac)}px`;
|
||||||
}
|
}
|
||||||
function updateSdrSquelchPctLabel() {
|
function submitSdrSquelch() {
|
||||||
if (!sdrSquelchEl || !sdrSquelchPctEl) return;
|
if (!sdrSquelchSupported) return;
|
||||||
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
saveSetting("sdrSquelchEnabled", sdrSquelchEnabled);
|
||||||
sdrSquelchPctEl.textContent = pct <= 0 ? "Open" : `${pct}%`;
|
saveSetting("sdrSquelchThresholdDb", sdrSquelchThresholdDb);
|
||||||
|
postPath(
|
||||||
|
`/set_sdr_squelch?enabled=${sdrSquelchEnabled ? "true" : "false"}&threshold_db=${encodeURIComponent(sdrSquelchThresholdDb.toFixed(2))}`
|
||||||
|
).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function setSdrSquelch(thresholdDb, enabled, options = {}) {
|
||||||
|
sdrSquelchThresholdDb = clampSdrSquelchDb(thresholdDb);
|
||||||
|
sdrSquelchEnabled = enabled;
|
||||||
|
renderSdrSquelch();
|
||||||
|
if (options.submit !== false) submitSdrSquelch();
|
||||||
|
}
|
||||||
|
function autoSquelchThresholdDb() {
|
||||||
|
const data = lastSpectrumData || window.lastSpectrumData;
|
||||||
|
if (!data || !isNumericBins(data.bins) || data.bins.length === 0) return null;
|
||||||
|
const noiseDb = estimateNoiseFloorDb(data.bins);
|
||||||
|
if (noiseDb == null || !isFiniteNumber(noiseDb)) return null;
|
||||||
|
return clampSdrSquelchDb(noiseDb + 6);
|
||||||
}
|
}
|
||||||
function updateSdrSquelchControlVisibility() {
|
function updateSdrSquelchControlVisibility() {
|
||||||
if (!sdrSquelchWrapEl) return;
|
if (!sdrSquelchWrapEl) return;
|
||||||
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||||
sdrSquelchWrapEl.style.display = sdrSquelchSupported && mode !== "WFM" ? "" : "none";
|
sdrSquelchWrapEl.style.display = sdrSquelchSupported && mode !== "WFM" ? "" : "none";
|
||||||
|
renderSdrSquelch();
|
||||||
}
|
}
|
||||||
function syncSdrSquelchFromServer(enabled, thresholdDb) {
|
function syncSdrSquelchFromServer(enabled, thresholdDb) {
|
||||||
if (!sdrSquelchEl) return;
|
if (squelchDragPointerId !== null) return;
|
||||||
if (document.activeElement === sdrSquelchEl) return;
|
if (sdrSquelchDbEl && document.activeElement === sdrSquelchDbEl) return;
|
||||||
const pct = sdrSquelchServerToPercent(enabled, thresholdDb);
|
sdrSquelchEnabled = enabled;
|
||||||
syncFromServerSdrSquelch = true;
|
if (isFiniteNumber(thresholdDb)) sdrSquelchThresholdDb = clampSdrSquelchDb(thresholdDb);
|
||||||
sdrSquelchEl.value = String(pct);
|
saveSetting("sdrSquelchEnabled", sdrSquelchEnabled);
|
||||||
updateSdrSquelchPctLabel();
|
saveSetting("sdrSquelchThresholdDb", sdrSquelchThresholdDb);
|
||||||
syncFromServerSdrSquelch = false;
|
renderSdrSquelch();
|
||||||
saveSetting("sdrSquelchPct", pct);
|
|
||||||
}
|
}
|
||||||
function submitSdrSquelchPercent(percent) {
|
var squelchDragPointerId = null;
|
||||||
if (!sdrSquelchSupported) return;
|
var squelchDragSubmitAt = 0;
|
||||||
const { enabled, thresholdDb } = sdrSquelchPercentToServer(percent);
|
if (sdrSquelchDbEl) {
|
||||||
postPath(
|
sdrSquelchDbEl.addEventListener("change", () => {
|
||||||
`/set_sdr_squelch?enabled=${enabled ? "true" : "false"}&threshold_db=${encodeURIComponent(thresholdDb.toFixed(2))}`
|
setSdrSquelch(Number(sdrSquelchDbEl.value), sdrSquelchEnabled);
|
||||||
).catch(() => {
|
});
|
||||||
|
sdrSquelchDbEl.addEventListener("blur", () => {
|
||||||
|
renderSdrSquelch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (sdrSquelchEl) {
|
if (sdrSquelchToggleBtn) {
|
||||||
const savedPct = clampSdrSquelchPercent(Number(loadSetting("sdrSquelchPct", 0)));
|
sdrSquelchToggleBtn.addEventListener("click", () => {
|
||||||
sdrSquelchEl.value = String(savedPct);
|
if (!sdrSquelchSupported) return;
|
||||||
updateSdrSquelchPctLabel();
|
setSdrSquelch(sdrSquelchThresholdDb, !sdrSquelchEnabled);
|
||||||
sdrSquelchEl.addEventListener("input", () => {
|
|
||||||
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
|
||||||
sdrSquelchEl.value = String(pct);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", pct);
|
|
||||||
if (!syncFromServerSdrSquelch) {
|
|
||||||
submitSdrSquelchPercent(pct);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var sdrSquelchAutoBtn = document.getElementById("sdr-squelch-auto");
|
var sdrSquelchAutoBtn = document.getElementById("sdr-squelch-auto");
|
||||||
if (sdrSquelchAutoBtn) {
|
if (sdrSquelchAutoBtn) {
|
||||||
sdrSquelchAutoBtn.addEventListener("click", () => {
|
sdrSquelchAutoBtn.addEventListener("click", () => {
|
||||||
if (!sdrSquelchSupported) return;
|
if (!sdrSquelchSupported) return;
|
||||||
let pct = 0;
|
const threshold = autoSquelchThresholdDb();
|
||||||
const data = lastSpectrumData || window.lastSpectrumData;
|
if (threshold == null) {
|
||||||
if (data && isNumericBins(data.bins) && data.bins.length > 0) {
|
showHint("No spectrum to measure the noise from", 1800);
|
||||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
return;
|
||||||
if (noiseDb != null && isFiniteNumber(noiseDb)) {
|
|
||||||
const thresholdDb = noiseDb + 6;
|
|
||||||
const clamped = Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, thresholdDb));
|
|
||||||
pct = clampSdrSquelchPercent(
|
|
||||||
(clamped - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB) * 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (sdrSquelchEl) {
|
setSdrSquelch(threshold, true);
|
||||||
sdrSquelchEl.value = String(pct);
|
showHint(`Squelch ${threshold} dB`, 1500);
|
||||||
updateSdrSquelchPctLabel();
|
});
|
||||||
saveSetting("sdrSquelchPct", pct);
|
}
|
||||||
|
if (squelchGripEl) {
|
||||||
|
const dbFromClientY = (clientY) => {
|
||||||
|
const canvas = document.getElementById("spectrum-canvas");
|
||||||
|
if (!canvas || !canvas.clientHeight) return sdrSquelchThresholdDb;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const frac = 1 - (clientY - rect.top) / rect.height;
|
||||||
|
return clampSdrSquelchDb(spectrumFloor + frac * spectrumRange);
|
||||||
|
};
|
||||||
|
squelchGripEl.addEventListener("pointerdown", (event) => {
|
||||||
|
if (!sdrSquelchSupported) return;
|
||||||
|
squelchDragPointerId = event.pointerId;
|
||||||
|
squelchGripEl.setPointerCapture(event.pointerId);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
squelchGripEl.addEventListener("pointermove", (event) => {
|
||||||
|
if (squelchDragPointerId !== event.pointerId) return;
|
||||||
|
sdrSquelchThresholdDb = dbFromClientY(event.clientY);
|
||||||
|
renderSdrSquelch();
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - squelchDragSubmitAt > 200) {
|
||||||
|
squelchDragSubmitAt = now;
|
||||||
|
submitSdrSquelch();
|
||||||
}
|
}
|
||||||
submitSdrSquelchPercent(pct);
|
});
|
||||||
|
const endDrag = (event) => {
|
||||||
|
if (squelchDragPointerId !== event.pointerId) return;
|
||||||
|
squelchDragPointerId = null;
|
||||||
|
submitSdrSquelch();
|
||||||
|
};
|
||||||
|
squelchGripEl.addEventListener("pointerup", endDrag);
|
||||||
|
squelchGripEl.addEventListener("pointercancel", endDrag);
|
||||||
|
squelchGripEl.addEventListener("keydown", (event) => {
|
||||||
|
const step = event.shiftKey ? 10 : 1;
|
||||||
|
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
||||||
|
setSdrSquelch(sdrSquelchThresholdDb + step, sdrSquelchEnabled);
|
||||||
|
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
||||||
|
setSdrSquelch(sdrSquelchThresholdDb - step, sdrSquelchEnabled);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wfmAudioModeEl) {
|
if (wfmAudioModeEl) {
|
||||||
@@ -7244,15 +7321,10 @@ function volWheel(slider, pctEl, getGain, storageKey) {
|
|||||||
}
|
}
|
||||||
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
||||||
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
||||||
if (sdrSquelchEl) {
|
if (sdrSquelchDbEl) {
|
||||||
sdrSquelchEl.addEventListener("wheel", (e) => {
|
sdrSquelchDbEl.addEventListener("wheel", (event) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
const step = e.deltaY < 0 ? 2 : -2;
|
setSdrSquelch(sdrSquelchThresholdDb + (event.deltaY < 0 ? 1 : -1), sdrSquelchEnabled);
|
||||||
const next = clampSdrSquelchPercent(Number(sdrSquelchEl.value) + step);
|
|
||||||
sdrSquelchEl.value = String(next);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", next);
|
|
||||||
submitSdrSquelchPercent(next);
|
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
requiredElement("copyright-year").textContent = String((/* @__PURE__ */ new Date()).getFullYear());
|
requiredElement("copyright-year").textContent = String((/* @__PURE__ */ new Date()).getFullYear());
|
||||||
@@ -8299,6 +8371,7 @@ function drawSpectrum(data) {
|
|||||||
}
|
}
|
||||||
spectrumGl.drawSegments(spectrumTmpGridSegments, cssColorToRgba(pal.spectrumGrid), 1);
|
spectrumGl.drawSegments(spectrumTmpGridSegments, cssColorToRgba(pal.spectrumGrid), 1);
|
||||||
updateSpectrumDbAxis(DB_MIN, DB_MAX, gridStep, H, dpr);
|
updateSpectrumDbAxis(DB_MIN, DB_MAX, gridStep, H, dpr);
|
||||||
|
positionSquelchLine();
|
||||||
function hzToX(hz) {
|
function hzToX(hz) {
|
||||||
return (hz - range.visLoHz) / range.visSpanHz * W;
|
return (hz - range.visLoHz) / range.visSpanHz * W;
|
||||||
}
|
}
|
||||||
@@ -8927,31 +9000,15 @@ window.addEventListener("keydown", (event) => {
|
|||||||
}
|
}
|
||||||
if (key === "q") {
|
if (key === "q") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (sdrSquelchSupported && sdrSquelchEl) {
|
if (sdrSquelchSupported) {
|
||||||
const current = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
if (sdrSquelchEnabled) {
|
||||||
let nextPct;
|
setSdrSquelch(sdrSquelchThresholdDb, false);
|
||||||
if (current > 0) {
|
showHint("Squelch off", 1200);
|
||||||
nextPct = 0;
|
|
||||||
} else {
|
} else {
|
||||||
let auto = 30;
|
const threshold = sdrSquelchThresholdDb > SDR_SQUELCH_MIN_DB ? sdrSquelchThresholdDb : autoSquelchThresholdDb() ?? SDR_SQUELCH_MIN_DB + 25;
|
||||||
const data = lastSpectrumData || window.lastSpectrumData;
|
setSdrSquelch(threshold, true);
|
||||||
if (data && isNumericBins(data.bins) && data.bins.length > 0) {
|
showHint(`Squelch ${clampSdrSquelchDb(threshold)} dB`, 1200);
|
||||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
|
||||||
if (noiseDb != null && isFiniteNumber(noiseDb)) {
|
|
||||||
const thresholdDb = noiseDb + 6;
|
|
||||||
const clamped = Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, thresholdDb));
|
|
||||||
auto = clampSdrSquelchPercent(
|
|
||||||
(clamped - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB) * 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextPct = auto;
|
|
||||||
}
|
}
|
||||||
sdrSquelchEl.value = String(nextPct);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", nextPct);
|
|
||||||
submitSdrSquelchPercent(nextPct);
|
|
||||||
showHint(nextPct > 0 ? `Squelch ${nextPct}%` : "Squelch Off", 1200);
|
|
||||||
} else {
|
} else {
|
||||||
showHint("Squelch N/A", 1200);
|
showHint("Squelch N/A", 1200);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div>
|
<div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div>
|
||||||
<canvas id="spectrum-canvas" tabindex="0" role="img" aria-label="Spectrum display" aria-describedby="spectrum-text-summary"></canvas>
|
<canvas id="spectrum-canvas" tabindex="0" role="img" aria-label="Spectrum display" aria-describedby="spectrum-text-summary"></canvas>
|
||||||
<p id="spectrum-text-summary" class="visually-hidden">Spectrum data is waiting for the receiver.</p>
|
<p id="spectrum-text-summary" class="visually-hidden">Spectrum data is waiting for the receiver.</p>
|
||||||
|
<div id="spectrum-squelch-line" class="spectrum-squelch-line" style="display:none;" data-state="closed">
|
||||||
|
<span class="spectrum-squelch-grip" id="spectrum-squelch-grip" role="slider" tabindex="0"
|
||||||
|
aria-label="Squelch threshold" aria-valuemin="-120" aria-valuemax="-30" aria-valuenow="-95">SQL <span id="spectrum-squelch-label">-95</span> dB</span>
|
||||||
|
</div>
|
||||||
<div id="spectrum-zoom-indicator" aria-hidden="true"></div>
|
<div id="spectrum-zoom-indicator" aria-hidden="true"></div>
|
||||||
<div id="spectrum-minimap" aria-hidden="true"><div class="minimap-view"></div></div>
|
<div id="spectrum-minimap" aria-hidden="true"><div class="minimap-view"></div></div>
|
||||||
<div id="spectrum-db-axis" aria-hidden="true"></div>
|
<div id="spectrum-db-axis" aria-hidden="true"></div>
|
||||||
@@ -411,7 +415,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<button id="tx-audio-btn" type="button">Transmit Audio</button>
|
<button id="tx-audio-btn" type="button">Transmit Audio</button>
|
||||||
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
||||||
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
||||||
<label class="vol-label" id="sdr-squelch-wrap" style="display:none;">SQL<input type="range" id="sdr-squelch" min="0" max="100" value="0" class="vol-slider" /><small class="vol-pct" id="sdr-squelch-pct">Open</small><button id="sdr-squelch-auto" type="button" class="sql-auto-btn" title="Set squelch to current noise level">Auto</button></label>
|
<span class="sql-control" id="sdr-squelch-wrap" style="display:none;">
|
||||||
|
<span class="sql-title">SQL</span>
|
||||||
|
<span class="sql-state" id="sdr-squelch-state" data-state="off" role="img" aria-label="Squelch state"></span>
|
||||||
|
<label class="sql-db"><input type="number" id="sdr-squelch-db" min="-120" max="-30" step="1" value="-95" inputmode="numeric" aria-label="Squelch threshold in dB" /><span class="sql-db-unit">dB</span></label>
|
||||||
|
<button id="sdr-squelch-auto" type="button" class="sql-auto-btn" title="Set the threshold just above the noise floor">Auto</button>
|
||||||
|
<button id="sdr-squelch-toggle" type="button" class="sql-auto-btn" aria-pressed="false" title="Enable or disable the squelch">Off</button>
|
||||||
|
</span>
|
||||||
<div id="audio-level">
|
<div id="audio-level">
|
||||||
<div id="audio-level-fill"></div>
|
<div id="audio-level-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1845,6 +1845,106 @@ small { color: var(--text-muted); }
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
/* Squelch: a threshold in dB, the unit the spectrum's own axis is labelled in,
|
||||||
|
with a dot showing whether the gate is passing audio right now. The old
|
||||||
|
percentage was a number with nothing on screen to relate it to. */
|
||||||
|
.sql-control {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sql-title {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.sql-state {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: color-mix(in srgb, var(--text-muted) 45%, transparent);
|
||||||
|
transition: background var(--dur-fast) var(--ease-standard),
|
||||||
|
box-shadow var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
.sql-state[data-state="open"] {
|
||||||
|
background: var(--status-ok);
|
||||||
|
box-shadow: 0 0 0 0.14rem color-mix(in srgb, var(--status-ok) 25%, transparent);
|
||||||
|
}
|
||||||
|
.sql-state[data-state="closed"] {
|
||||||
|
background: var(--accent-yellow);
|
||||||
|
box-shadow: 0 0 0 0.14rem color-mix(in srgb, var(--accent-yellow) 22%, transparent);
|
||||||
|
}
|
||||||
|
.sql-db {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
.sql-db input {
|
||||||
|
width: 3.4rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 0.3rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.sql-db-unit {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
.sql-control button[aria-pressed="true"] {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-green) 55%, var(--border-light));
|
||||||
|
color: var(--accent-text);
|
||||||
|
}
|
||||||
|
/* The line is the control: it spans the plot at its threshold and carries its
|
||||||
|
own grip, so the level is set where the noise it has to clear is visible. */
|
||||||
|
.spectrum-squelch-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0;
|
||||||
|
/* Above the overlays that share the plot: the split control sits at the
|
||||||
|
right edge on z-index 9 and would otherwise swallow the grip's pointer. */
|
||||||
|
z-index: 10;
|
||||||
|
border-top: 1px dashed color-mix(in srgb, var(--accent-yellow) 75%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.spectrum-squelch-line[data-state="open"] {
|
||||||
|
border-top-color: color-mix(in srgb, var(--status-ok) 80%, transparent);
|
||||||
|
}
|
||||||
|
.spectrum-squelch-grip {
|
||||||
|
position: absolute;
|
||||||
|
/* Clear of the split control's column so the two never sit on top of each
|
||||||
|
other, whatever height the threshold is at. */
|
||||||
|
right: 3.6rem;
|
||||||
|
top: -0.72rem;
|
||||||
|
padding: 0.05rem 0.35rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-yellow) 60%, var(--border-light));
|
||||||
|
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.3;
|
||||||
|
cursor: ns-resize;
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.spectrum-squelch-line[data-state="open"] .spectrum-squelch-grip {
|
||||||
|
border-color: color-mix(in srgb, var(--status-ok) 60%, var(--border-light));
|
||||||
|
}
|
||||||
|
.spectrum-squelch-grip:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-text);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
.sql-auto-btn {
|
.sql-auto-btn {
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|||||||
@@ -946,6 +946,8 @@ function formatSigStrength(dbm: number | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshSigStrengthDisplay() {
|
function refreshSigStrengthDisplay() {
|
||||||
|
// The squelch indicator reads the same meter, so it follows it here.
|
||||||
|
renderSdrSquelch();
|
||||||
if (!sigStrengthEl) return;
|
if (!sigStrengthEl) return;
|
||||||
sigStrengthEl.innerHTML = formatSigStrength(sigLastDbm);
|
sigStrengthEl.innerHTML = formatSigStrength(sigLastDbm);
|
||||||
}
|
}
|
||||||
@@ -5151,11 +5153,14 @@ const modeControlsRow = document.getElementById("mode-controls-row");
|
|||||||
const samStereoWidthEl = document.getElementById("sam-stereo-width") as HTMLInputElement | null;
|
const samStereoWidthEl = document.getElementById("sam-stereo-width") as HTMLInputElement | null;
|
||||||
const samCarrierSyncEl = document.getElementById("sam-carrier-sync") as HTMLSelectElement | null;
|
const samCarrierSyncEl = document.getElementById("sam-carrier-sync") as HTMLSelectElement | null;
|
||||||
const sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
|
const sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
|
||||||
const sdrSquelchEl = document.getElementById("sdr-squelch") as HTMLInputElement | null;
|
const sdrSquelchDbEl = document.getElementById("sdr-squelch-db") as HTMLInputElement | null;
|
||||||
const sdrSquelchPctEl = document.getElementById("sdr-squelch-pct");
|
const sdrSquelchStateEl = document.getElementById("sdr-squelch-state");
|
||||||
|
const sdrSquelchToggleBtn = document.getElementById("sdr-squelch-toggle") as HTMLButtonElement | null;
|
||||||
|
const squelchLineEl = document.getElementById("spectrum-squelch-line");
|
||||||
|
const squelchGripEl = document.getElementById("spectrum-squelch-grip");
|
||||||
|
const squelchLabelEl = document.getElementById("spectrum-squelch-label");
|
||||||
const SDR_SQUELCH_MIN_DB = -120;
|
const SDR_SQUELCH_MIN_DB = -120;
|
||||||
const SDR_SQUELCH_MAX_DB = -30;
|
const SDR_SQUELCH_MAX_DB = -30;
|
||||||
let syncFromServerSdrSquelch = false;
|
|
||||||
const sdrNbWrapEl = document.getElementById("sdr-nb-wrap");
|
const sdrNbWrapEl = document.getElementById("sdr-nb-wrap");
|
||||||
const sdrNbEnabledEl = document.getElementById("sdr-nb-enabled") as HTMLInputElement | null;
|
const sdrNbEnabledEl = document.getElementById("sdr-nb-enabled") as HTMLInputElement | null;
|
||||||
const sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-controls");
|
const sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-controls");
|
||||||
@@ -5255,71 +5260,131 @@ function normalizeWfmDenoiseLevel(value: unknown) {
|
|||||||
return "auto";
|
return "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampSdrSquelchPercent(value: number) {
|
// The threshold is held in dB, the scale the spectrum axis and the S-meter are
|
||||||
if (!isFiniteNumber(value)) return 0;
|
// labelled in and the one the server compares against. It used to be a
|
||||||
return Math.max(0, Math.min(100, Math.round(value)));
|
// percentage over that range, which left the operator no way to relate the
|
||||||
|
// control to anything on screen — and 0% doubled as "disabled", so turning the
|
||||||
|
// squelch off to listen threw the setting away.
|
||||||
|
let sdrSquelchEnabled: boolean = loadSetting("sdrSquelchEnabled", false);
|
||||||
|
let sdrSquelchThresholdDb = clampSdrSquelchDb(Number(loadSetting("sdrSquelchThresholdDb", -95)));
|
||||||
|
|
||||||
|
function clampSdrSquelchDb(value: number) {
|
||||||
|
if (!isFiniteNumber(value)) return SDR_SQUELCH_MIN_DB;
|
||||||
|
return Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, Math.round(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function sdrSquelchPercentToServer(percent: number) {
|
/** Passing right now, as far as the meter can tell: the same comparison the
|
||||||
const pct = clampSdrSquelchPercent(percent);
|
* DSP makes, against the same number it reports. */
|
||||||
if (pct <= 0) {
|
function sdrSquelchIsPassing() {
|
||||||
return { enabled: false, thresholdDb: SDR_SQUELCH_MIN_DB };
|
if (!sdrSquelchEnabled) return true;
|
||||||
|
if (!isFiniteNumber(sigLastDbm)) return false;
|
||||||
|
return sigLastDbm >= sdrSquelchThresholdDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSdrSquelch() {
|
||||||
|
if (sdrSquelchDbEl && document.activeElement !== sdrSquelchDbEl) {
|
||||||
|
sdrSquelchDbEl.value = String(sdrSquelchThresholdDb);
|
||||||
}
|
}
|
||||||
const ratio = pct / 100;
|
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
||||||
const thresholdDb = SDR_SQUELCH_MIN_DB + ratio * (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
if (sdrSquelchToggleBtn) {
|
||||||
return { enabled: true, thresholdDb };
|
sdrSquelchToggleBtn.textContent = sdrSquelchEnabled ? "On" : "Off";
|
||||||
|
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
||||||
|
}
|
||||||
|
const state = !sdrSquelchEnabled ? "off" : (sdrSquelchIsPassing() ? "open" : "closed");
|
||||||
|
if (sdrSquelchStateEl) {
|
||||||
|
sdrSquelchStateEl.dataset.state = state;
|
||||||
|
sdrSquelchStateEl.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
state === "off" ? "Squelch off" : (state === "open" ? "Squelch open" : "Squelch closed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
||||||
|
if (squelchGripEl) squelchGripEl.setAttribute("aria-valuenow", String(sdrSquelchThresholdDb));
|
||||||
|
if (squelchLineEl) squelchLineEl.dataset.state = state === "open" ? "open" : "closed";
|
||||||
|
positionSquelchLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sdrSquelchServerToPercent(enabled: boolean, thresholdDb: number | null) {
|
/** Places the line at its threshold on the spectrum's dB axis. */
|
||||||
if (!enabled) return 0;
|
function positionSquelchLine() {
|
||||||
if (!isFiniteNumber(thresholdDb)) return 0;
|
if (!squelchLineEl) return;
|
||||||
const ratio = (thresholdDb - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||||
return clampSdrSquelchPercent(ratio * 100);
|
const canvas = document.getElementById("spectrum-canvas");
|
||||||
|
const visible = sdrSquelchSupported && sdrSquelchEnabled && mode !== "WFM"
|
||||||
|
&& !!canvas && canvas.clientHeight > 0
|
||||||
|
&& getComputedStyle(requiredElement("spectrum-panel")).display !== "none";
|
||||||
|
if (!visible) {
|
||||||
|
squelchLineEl.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dbMin = spectrumFloor;
|
||||||
|
const dbMax = spectrumFloor + spectrumRange;
|
||||||
|
// Pinned to the plot edge when the threshold sits outside the visible dB
|
||||||
|
// window rather than hidden: the grip is how it gets dragged back, and the
|
||||||
|
// label still reads the real value.
|
||||||
|
const frac = Math.max(0, Math.min(1, (sdrSquelchThresholdDb - dbMin) / Math.max(1, dbMax - dbMin)));
|
||||||
|
squelchLineEl.style.display = "";
|
||||||
|
squelchLineEl.style.top = `${canvas.offsetTop + canvas.clientHeight * (1 - frac)}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSdrSquelchPctLabel() {
|
function submitSdrSquelch() {
|
||||||
if (!sdrSquelchEl || !sdrSquelchPctEl) return;
|
if (!sdrSquelchSupported) return;
|
||||||
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
saveSetting("sdrSquelchEnabled", sdrSquelchEnabled);
|
||||||
sdrSquelchPctEl.textContent = pct <= 0 ? "Open" : `${pct}%`;
|
saveSetting("sdrSquelchThresholdDb", sdrSquelchThresholdDb);
|
||||||
|
postPath(
|
||||||
|
`/set_sdr_squelch?enabled=${sdrSquelchEnabled ? "true" : "false"}`
|
||||||
|
+ `&threshold_db=${encodeURIComponent(sdrSquelchThresholdDb.toFixed(2))}`,
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSdrSquelch(thresholdDb: number, enabled: boolean, options: { submit?: boolean } = {}) {
|
||||||
|
sdrSquelchThresholdDb = clampSdrSquelchDb(thresholdDb);
|
||||||
|
sdrSquelchEnabled = enabled;
|
||||||
|
renderSdrSquelch();
|
||||||
|
if (options.submit !== false) submitSdrSquelch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Just above the noise, which is the level the gate has to clear. */
|
||||||
|
function autoSquelchThresholdDb(): number | null {
|
||||||
|
const data = lastSpectrumData || window.lastSpectrumData;
|
||||||
|
if (!data || !isBinsArray(data.bins) || data.bins.length === 0) return null;
|
||||||
|
const noiseDb = estimateNoiseFloorDb(data.bins);
|
||||||
|
if (noiseDb == null || !isFiniteNumber(noiseDb)) return null;
|
||||||
|
return clampSdrSquelchDb(noiseDb + 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSdrSquelchControlVisibility() {
|
function updateSdrSquelchControlVisibility() {
|
||||||
if (!sdrSquelchWrapEl) return;
|
if (!sdrSquelchWrapEl) return;
|
||||||
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||||
sdrSquelchWrapEl.style.display = sdrSquelchSupported && mode !== "WFM" ? "" : "none";
|
sdrSquelchWrapEl.style.display = sdrSquelchSupported && mode !== "WFM" ? "" : "none";
|
||||||
|
renderSdrSquelch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSdrSquelchFromServer(enabled: boolean, thresholdDb: number | null) {
|
function syncSdrSquelchFromServer(enabled: boolean, thresholdDb: number | null) {
|
||||||
if (!sdrSquelchEl) return;
|
// Not while the operator is on the control: dragging the line or typing a
|
||||||
if (document.activeElement === sdrSquelchEl) return;
|
// level would fight the echo of the value the server last confirmed.
|
||||||
const pct = sdrSquelchServerToPercent(enabled, thresholdDb);
|
if (squelchDragPointerId !== null) return;
|
||||||
syncFromServerSdrSquelch = true;
|
if (sdrSquelchDbEl && document.activeElement === sdrSquelchDbEl) return;
|
||||||
sdrSquelchEl.value = String(pct);
|
sdrSquelchEnabled = enabled;
|
||||||
updateSdrSquelchPctLabel();
|
if (isFiniteNumber(thresholdDb)) sdrSquelchThresholdDb = clampSdrSquelchDb(thresholdDb);
|
||||||
syncFromServerSdrSquelch = false;
|
saveSetting("sdrSquelchEnabled", sdrSquelchEnabled);
|
||||||
saveSetting("sdrSquelchPct", pct);
|
saveSetting("sdrSquelchThresholdDb", sdrSquelchThresholdDb);
|
||||||
|
renderSdrSquelch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitSdrSquelchPercent(percent: number) {
|
let squelchDragPointerId: number | null = null;
|
||||||
if (!sdrSquelchSupported) return;
|
let squelchDragSubmitAt = 0;
|
||||||
const { enabled, thresholdDb } = sdrSquelchPercentToServer(percent);
|
|
||||||
postPath(
|
if (sdrSquelchDbEl) {
|
||||||
`/set_sdr_squelch?enabled=${enabled ? "true" : "false"}&threshold_db=${encodeURIComponent(thresholdDb.toFixed(2))}`,
|
sdrSquelchDbEl.addEventListener("change", () => {
|
||||||
).catch(() => {});
|
setSdrSquelch(Number(sdrSquelchDbEl.value), sdrSquelchEnabled);
|
||||||
|
});
|
||||||
|
sdrSquelchDbEl.addEventListener("blur", () => { renderSdrSquelch(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sdrSquelchEl) {
|
if (sdrSquelchToggleBtn) {
|
||||||
const savedPct = clampSdrSquelchPercent(Number(loadSetting("sdrSquelchPct", 0)));
|
sdrSquelchToggleBtn.addEventListener("click", () => {
|
||||||
sdrSquelchEl.value = String(savedPct);
|
if (!sdrSquelchSupported) return;
|
||||||
updateSdrSquelchPctLabel();
|
setSdrSquelch(sdrSquelchThresholdDb, !sdrSquelchEnabled);
|
||||||
sdrSquelchEl.addEventListener("input", () => {
|
|
||||||
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
|
||||||
sdrSquelchEl.value = String(pct);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", pct);
|
|
||||||
if (!syncFromServerSdrSquelch) {
|
|
||||||
submitSdrSquelchPercent(pct);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5327,25 +5392,60 @@ const sdrSquelchAutoBtn = document.getElementById("sdr-squelch-auto") as HTMLBut
|
|||||||
if (sdrSquelchAutoBtn) {
|
if (sdrSquelchAutoBtn) {
|
||||||
sdrSquelchAutoBtn.addEventListener("click", () => {
|
sdrSquelchAutoBtn.addEventListener("click", () => {
|
||||||
if (!sdrSquelchSupported) return;
|
if (!sdrSquelchSupported) return;
|
||||||
let pct = 0; // default: Off
|
const threshold = autoSquelchThresholdDb();
|
||||||
const data = lastSpectrumData || window.lastSpectrumData;
|
if (threshold == null) {
|
||||||
if (data && isBinsArray(data.bins) && data.bins.length > 0) {
|
showHint("No spectrum to measure the noise from", 1800);
|
||||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
return;
|
||||||
if (noiseDb != null && isFiniteNumber(noiseDb)) {
|
|
||||||
// Set threshold slightly above noise floor so squelch closes on noise
|
|
||||||
const thresholdDb = noiseDb + 6;
|
|
||||||
const clamped = Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, thresholdDb));
|
|
||||||
pct = clampSdrSquelchPercent(
|
|
||||||
((clamped - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB)) * 100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (sdrSquelchEl) {
|
setSdrSquelch(threshold, true);
|
||||||
sdrSquelchEl.value = String(pct);
|
showHint(`Squelch ${threshold} dB`, 1500);
|
||||||
updateSdrSquelchPctLabel();
|
});
|
||||||
saveSetting("sdrSquelchPct", pct);
|
}
|
||||||
|
|
||||||
|
// Dragging the line, the same gesture as the bandwidth edges. Submits are
|
||||||
|
// throttled so the gate follows the drag by ear without a request per pixel.
|
||||||
|
if (squelchGripEl) {
|
||||||
|
const dbFromClientY = (clientY: number) => {
|
||||||
|
const canvas = document.getElementById("spectrum-canvas");
|
||||||
|
if (!canvas || !canvas.clientHeight) return sdrSquelchThresholdDb;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const frac = 1 - (clientY - rect.top) / rect.height;
|
||||||
|
return clampSdrSquelchDb(spectrumFloor + frac * spectrumRange);
|
||||||
|
};
|
||||||
|
squelchGripEl.addEventListener("pointerdown", (event) => {
|
||||||
|
if (!sdrSquelchSupported) return;
|
||||||
|
squelchDragPointerId = event.pointerId;
|
||||||
|
squelchGripEl.setPointerCapture(event.pointerId);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
squelchGripEl.addEventListener("pointermove", (event) => {
|
||||||
|
if (squelchDragPointerId !== event.pointerId) return;
|
||||||
|
sdrSquelchThresholdDb = dbFromClientY(event.clientY);
|
||||||
|
renderSdrSquelch();
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - squelchDragSubmitAt > 200) {
|
||||||
|
squelchDragSubmitAt = now;
|
||||||
|
submitSdrSquelch();
|
||||||
}
|
}
|
||||||
submitSdrSquelchPercent(pct);
|
});
|
||||||
|
const endDrag = (event: PointerEvent) => {
|
||||||
|
if (squelchDragPointerId !== event.pointerId) return;
|
||||||
|
squelchDragPointerId = null;
|
||||||
|
submitSdrSquelch();
|
||||||
|
};
|
||||||
|
squelchGripEl.addEventListener("pointerup", endDrag);
|
||||||
|
squelchGripEl.addEventListener("pointercancel", endDrag);
|
||||||
|
squelchGripEl.addEventListener("keydown", (event) => {
|
||||||
|
const step = event.shiftKey ? 10 : 1;
|
||||||
|
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
||||||
|
setSdrSquelch(sdrSquelchThresholdDb + step, sdrSquelchEnabled);
|
||||||
|
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
||||||
|
setSdrSquelch(sdrSquelchThresholdDb - step, sdrSquelchEnabled);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6176,15 +6276,10 @@ function volWheel(slider: HTMLInputElement, pctEl: HTMLElement, getGain: () => G
|
|||||||
}
|
}
|
||||||
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
||||||
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
||||||
if (sdrSquelchEl) {
|
if (sdrSquelchDbEl) {
|
||||||
sdrSquelchEl.addEventListener("wheel", (e) => {
|
sdrSquelchDbEl.addEventListener("wheel", (event) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
const step = e.deltaY < 0 ? 2 : -2;
|
setSdrSquelch(sdrSquelchThresholdDb + (event.deltaY < 0 ? 1 : -1), sdrSquelchEnabled);
|
||||||
const next = clampSdrSquelchPercent(Number(sdrSquelchEl.value) + step);
|
|
||||||
sdrSquelchEl.value = String(next);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", next);
|
|
||||||
submitSdrSquelchPercent(next);
|
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7398,6 +7493,9 @@ function drawSpectrum(data: SpectrumFrame) {
|
|||||||
}
|
}
|
||||||
spectrumGl.drawSegments(spectrumTmpGridSegments, cssColorToRgba(pal.spectrumGrid), 1);
|
spectrumGl.drawSegments(spectrumTmpGridSegments, cssColorToRgba(pal.spectrumGrid), 1);
|
||||||
updateSpectrumDbAxis(DB_MIN, DB_MAX, gridStep, H, dpr);
|
updateSpectrumDbAxis(DB_MIN, DB_MAX, gridStep, H, dpr);
|
||||||
|
// The squelch line rides the same axis: reposition it whenever the axis or
|
||||||
|
// the plot geometry moves under it.
|
||||||
|
positionSquelchLine();
|
||||||
|
|
||||||
function hzToX(hz: number) {
|
function hzToX(hz: number) {
|
||||||
return ((hz - range.visLoHz) / range.visSpanHz) * W;
|
return ((hz - range.visLoHz) / range.visSpanHz) * W;
|
||||||
@@ -8125,35 +8223,21 @@ window.addEventListener("keydown", (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q — toggle squelch (cycle 0 → auto → 0)
|
// Q — gate on or off, keeping the threshold. Picks one off the noise floor
|
||||||
|
// the first time, when there is nothing to keep.
|
||||||
if (key === "q") {
|
if (key === "q") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (sdrSquelchSupported && sdrSquelchEl) {
|
if (sdrSquelchSupported) {
|
||||||
const current = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
if (sdrSquelchEnabled) {
|
||||||
let nextPct;
|
setSdrSquelch(sdrSquelchThresholdDb, false);
|
||||||
if (current > 0) {
|
showHint("Squelch off", 1200);
|
||||||
nextPct = 0; // turn off
|
|
||||||
} else {
|
} else {
|
||||||
// Auto: estimate from noise floor
|
const threshold = sdrSquelchThresholdDb > SDR_SQUELCH_MIN_DB
|
||||||
let auto = 30;
|
? sdrSquelchThresholdDb
|
||||||
const data = lastSpectrumData || window.lastSpectrumData;
|
: (autoSquelchThresholdDb() ?? SDR_SQUELCH_MIN_DB + 25);
|
||||||
if (data && isBinsArray(data.bins) && data.bins.length > 0) {
|
setSdrSquelch(threshold, true);
|
||||||
const noiseDb = estimateNoiseFloorDb(data.bins);
|
showHint(`Squelch ${clampSdrSquelchDb(threshold)} dB`, 1200);
|
||||||
if (noiseDb != null && isFiniteNumber(noiseDb)) {
|
|
||||||
const thresholdDb = noiseDb + 6;
|
|
||||||
const clamped = Math.max(SDR_SQUELCH_MIN_DB, Math.min(SDR_SQUELCH_MAX_DB, thresholdDb));
|
|
||||||
auto = clampSdrSquelchPercent(
|
|
||||||
((clamped - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB)) * 100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextPct = auto;
|
|
||||||
}
|
}
|
||||||
sdrSquelchEl.value = String(nextPct);
|
|
||||||
updateSdrSquelchPctLabel();
|
|
||||||
saveSetting("sdrSquelchPct", nextPct);
|
|
||||||
submitSdrSquelchPercent(nextPct);
|
|
||||||
showHint(nextPct > 0 ? `Squelch ${nextPct}%` : "Squelch Off", 1200);
|
|
||||||
} else {
|
} else {
|
||||||
showHint("Squelch N/A", 1200);
|
showHint("Squelch N/A", 1200);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
|
|||||||
import { chromium } from "playwright-core";
|
import { chromium } from "playwright-core";
|
||||||
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
||||||
|
|
||||||
/* global document */
|
/* global document, getComputedStyle */
|
||||||
|
|
||||||
const BOOKMARKS = [
|
const BOOKMARKS = [
|
||||||
{ id: "b1", name: "40m FT8", freq_hz: 7074000, mode: "DIG", category: "Digital", comment: "", locator: "" },
|
{ id: "b1", name: "40m FT8", freq_hz: 7074000, mode: "DIG", category: "Digital", comment: "", locator: "" },
|
||||||
@@ -120,6 +120,67 @@ try {
|
|||||||
assert.equal(hits.chip, "chip", `chip is not clickable, hit ${hits.chip}`);
|
assert.equal(hits.chip, "chip", `chip is not clickable, hit ${hits.chip}`);
|
||||||
assert.equal(hits.besideChip, "overview-canvas", `rail swallows events, hit ${hits.besideChip}`);
|
assert.equal(hits.besideChip, "overview-canvas", `rail swallows events, hit ${hits.besideChip}`);
|
||||||
|
|
||||||
|
// Squelch: the threshold is in the dB the spectrum axis is labelled in, so the
|
||||||
|
// line is the control. It used to be a percentage on a slider in the audio
|
||||||
|
// row, with nothing on screen to relate the number to.
|
||||||
|
await page.locator("summary", { hasText: "Audio controls" }).click();
|
||||||
|
await page.locator("#sdr-squelch-toggle").click();
|
||||||
|
// Auto parks it just above the noise, which is mid-axis and leaves room to
|
||||||
|
// drag in either direction.
|
||||||
|
await page.locator("#sdr-squelch-auto").click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const squelchOn = await page.evaluate(() => {
|
||||||
|
const line = document.getElementById("spectrum-squelch-line");
|
||||||
|
return {
|
||||||
|
shown: getComputedStyle(line).display !== "none",
|
||||||
|
db: Number(document.getElementById("sdr-squelch-db").value),
|
||||||
|
label: Number(document.getElementById("spectrum-squelch-label").textContent),
|
||||||
|
toggle: document.getElementById("sdr-squelch-toggle").textContent,
|
||||||
|
top: Math.round(line.getBoundingClientRect().top),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.equal(squelchOn.shown, true, "the threshold line did not appear with the squelch on");
|
||||||
|
assert.equal(squelchOn.toggle, "On", "the toggle did not follow the squelch state");
|
||||||
|
assert.equal(squelchOn.label, squelchOn.db, "the line and the readout disagree on the threshold");
|
||||||
|
|
||||||
|
// Dragging the line down lowers the threshold and tells the server.
|
||||||
|
const submitted = [];
|
||||||
|
page.on("request", (request) => {
|
||||||
|
if (request.url().includes("/set_sdr_squelch")) {
|
||||||
|
submitted.push(Number(new URL(request.url()).searchParams.get("threshold_db")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const grip = await page.locator("#spectrum-squelch-grip").boundingBox();
|
||||||
|
await page.mouse.move(grip.x + grip.width / 2, grip.y + grip.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(grip.x + grip.width / 2, grip.y + grip.height / 2 + 60, { steps: 8 });
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
const dragged = await page.evaluate(() => ({
|
||||||
|
db: Number(document.getElementById("sdr-squelch-db").value),
|
||||||
|
label: Number(document.getElementById("spectrum-squelch-label").textContent),
|
||||||
|
top: Math.round(document.getElementById("spectrum-squelch-line").getBoundingClientRect().top),
|
||||||
|
}));
|
||||||
|
assert.ok(dragged.db < squelchOn.db,
|
||||||
|
`dragging down left the threshold at ${dragged.db} dB (was ${squelchOn.db})`);
|
||||||
|
assert.equal(dragged.label, dragged.db, "the line label did not follow the drag");
|
||||||
|
assert.ok(dragged.top > squelchOn.top, "the line did not move with the drag");
|
||||||
|
assert.ok(submitted.includes(dragged.db),
|
||||||
|
`the server was never told about ${dragged.db} dB (saw ${JSON.stringify(submitted)})`);
|
||||||
|
|
||||||
|
// Turning it off leaves the threshold alone — the old control conflated the
|
||||||
|
// two, so dropping to zero to listen threw the setting away.
|
||||||
|
await page.locator("#sdr-squelch-toggle").click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const squelchOff = await page.evaluate(() => ({
|
||||||
|
db: Number(document.getElementById("sdr-squelch-db").value),
|
||||||
|
shown: getComputedStyle(document.getElementById("spectrum-squelch-line")).display !== "none",
|
||||||
|
dot: document.getElementById("sdr-squelch-state").dataset.state,
|
||||||
|
}));
|
||||||
|
assert.equal(squelchOff.db, dragged.db, "turning the squelch off discarded the threshold");
|
||||||
|
assert.equal(squelchOff.shown, false, "the line stayed up with the squelch off");
|
||||||
|
assert.equal(squelchOff.dot, "off", "the indicator did not follow the squelch off");
|
||||||
|
|
||||||
assert.deepEqual(runtimeErrors, []);
|
assert.deepEqual(runtimeErrors, []);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
@@ -111,7 +111,17 @@ export async function startWebFixture({
|
|||||||
signal_meter: spectrum,
|
signal_meter: spectrum,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: { freq: { hz: 100_000_000 }, mode: "FM", tx_en: false, vfo: null, tx: null, rx: null, lock: null },
|
status: { freq: { hz: 100_000_000 }, mode: "FM", tx_en: false, vfo: null, tx: null, rx: { sig: -70 }, lock: null },
|
||||||
|
// Reported only by SDR backends, and what makes the client show the
|
||||||
|
// squelch control at all.
|
||||||
|
filter: spectrum
|
||||||
|
? {
|
||||||
|
bandwidth_hz: 12_000,
|
||||||
|
sdr_squelch_enabled: false,
|
||||||
|
sdr_squelch_threshold_db: -95,
|
||||||
|
sdr_agc_enabled: false,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
band: null,
|
band: null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
@@ -184,8 +194,11 @@ export async function startWebFixture({
|
|||||||
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 200 means "audio is configured": the client hides the whole audio row —
|
||||||
|
// and the squelch control with it — when this 404s.
|
||||||
if (url.pathname === "/audio") {
|
if (url.pathname === "/audio") {
|
||||||
response.writeHead(404).end();
|
response.writeHead(200, { "content-type": "application/json" });
|
||||||
|
response.end(JSON.stringify({ sample_rate: 48_000, channels: 1 }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (spectrum && url.pathname === "/spectrum") {
|
if (spectrum && url.pathname === "/spectrum") {
|
||||||
|
|||||||
@@ -837,12 +837,6 @@ impl ChannelDsp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let signal_power = decimated
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.re * s.re + s.im * s.im)
|
|
||||||
.sum::<f32>()
|
|
||||||
/ decimated.len() as f32;
|
|
||||||
let signal_db = 10.0 * signal_power.max(1e-12).log10();
|
|
||||||
const WFM_OUTPUT_GAIN: f32 = 0.50;
|
const WFM_OUTPUT_GAIN: f32 = 0.50;
|
||||||
let mut audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
let mut audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||||
let mut out = decoder.process_iq(decimated);
|
let mut out = decoder.process_iq(decimated);
|
||||||
@@ -884,7 +878,14 @@ impl ChannelDsp {
|
|||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !self.squelch.update(&self.mode, signal_db) {
|
// Against the meter reading, not the block level after the IQ AGC.
|
||||||
|
// The threshold arrives in the scale the operator sets it from — the
|
||||||
|
// S-meter and the spectrum — while the level measured here had been
|
||||||
|
// through the AGC, whose whole job is to hold it at a setpoint. For
|
||||||
|
// every mode that has one (FM, PKT, AIS, AM, SAM) the squelch was
|
||||||
|
// therefore comparing against a near-constant, and for the rest it was
|
||||||
|
// still off by the decimation correction the meter applies.
|
||||||
|
if !self.squelch.update(&self.mode, self.last_signal_db) {
|
||||||
audio.fill(0.0);
|
audio.fill(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,6 +934,93 @@ mod tests {
|
|||||||
dsp.process_block(&block);
|
dsp.process_block(&block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feeds one signal twice, with the squelch threshold set from the channel's
|
||||||
|
/// own meter reading: 6 dB above it must gate the audio, 6 dB below it must
|
||||||
|
/// pass. FM runs an IQ AGC, so a squelch measured after that stage compares
|
||||||
|
/// against a level pinned near the AGC setpoint — some 20 dB adrift of the
|
||||||
|
/// scale the operator reads the threshold off, and open on plain noise.
|
||||||
|
#[test]
|
||||||
|
fn squelch_follows_the_meter_the_threshold_is_set_from() {
|
||||||
|
const AMPLITUDE: f32 = 0.0025;
|
||||||
|
|
||||||
|
let (pcm_tx, mut pcm_rx) = broadcast::channel::<Vec<f32>>(4096);
|
||||||
|
let (iq_tx, _iq_rx) = broadcast::channel::<Vec<Complex<f32>>>(8);
|
||||||
|
let mut dsp = ChannelDsp::new(
|
||||||
|
0.0,
|
||||||
|
&RigMode::FM,
|
||||||
|
48_000,
|
||||||
|
8_000,
|
||||||
|
1,
|
||||||
|
20,
|
||||||
|
12_000,
|
||||||
|
75,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
|
NoiseBlankerConfig::default(),
|
||||||
|
pcm_tx,
|
||||||
|
iq_tx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A 1 kHz tone on the carrier, so an open gate is audibly non-zero and
|
||||||
|
// a closed one is unambiguously silent.
|
||||||
|
let mut phase = 0.0_f32;
|
||||||
|
let mut mod_phase = 0.0_f32;
|
||||||
|
let mut feed = |dsp: &mut ChannelDsp, blocks: usize| {
|
||||||
|
for _ in 0..blocks {
|
||||||
|
let mut block = Vec::with_capacity(4096);
|
||||||
|
for _ in 0..4096 {
|
||||||
|
mod_phase += std::f32::consts::TAU * 1_000.0 / 48_000.0;
|
||||||
|
phase += std::f32::consts::TAU * (3_000.0 * mod_phase.sin()) / 48_000.0;
|
||||||
|
block.push(Complex::new(
|
||||||
|
AMPLITUDE * phase.cos(),
|
||||||
|
AMPLITUDE * phase.sin(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
dsp.process_block(&block);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let drain = |rx: &mut broadcast::Receiver<Vec<f32>>| {
|
||||||
|
let mut audio = Vec::new();
|
||||||
|
while let Ok(frame) = rx.try_recv() {
|
||||||
|
audio.extend_from_slice(&frame);
|
||||||
|
}
|
||||||
|
audio
|
||||||
|
};
|
||||||
|
let peak = |audio: &[f32]| audio.iter().fold(0.0_f32, |acc, s| acc.max(s.abs()));
|
||||||
|
|
||||||
|
// Settle the meter on this signal, then read what the operator would.
|
||||||
|
feed(&mut dsp, 24);
|
||||||
|
let meter_db = dsp.signal_db();
|
||||||
|
assert!(
|
||||||
|
meter_db > -120.0,
|
||||||
|
"the meter never moved off its floor ({meter_db} dB)"
|
||||||
|
);
|
||||||
|
|
||||||
|
dsp.set_squelch(true, meter_db + 6.0);
|
||||||
|
let _ = drain(&mut pcm_rx);
|
||||||
|
feed(&mut dsp, 24);
|
||||||
|
let gated = drain(&mut pcm_rx);
|
||||||
|
assert!(!gated.is_empty(), "no audio frames were produced at all");
|
||||||
|
// From the second half on: the first frame out still carries the audio
|
||||||
|
// that was already buffered when the threshold changed.
|
||||||
|
assert_eq!(
|
||||||
|
peak(&gated[gated.len() / 2..]),
|
||||||
|
0.0,
|
||||||
|
"squelch set 6 dB above the meter ({meter_db} dB) still passed audio"
|
||||||
|
);
|
||||||
|
|
||||||
|
dsp.set_squelch(true, meter_db - 6.0);
|
||||||
|
let _ = drain(&mut pcm_rx);
|
||||||
|
feed(&mut dsp, 24);
|
||||||
|
let passed = drain(&mut pcm_rx);
|
||||||
|
assert!(!passed.is_empty(), "no audio frames were produced at all");
|
||||||
|
assert!(
|
||||||
|
peak(&passed[passed.len() / 2..]) > 0.0,
|
||||||
|
"squelch set 6 dB below the meter ({meter_db} dB) gated the audio"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_dsp_set_mode() {
|
fn channel_dsp_set_mode() {
|
||||||
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
||||||
|
|||||||
Reference in New Issue
Block a user