From 6676b669930a72aea37a6dfed9664e6a3d2c6f81 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 02:07:32 +0200 Subject: [PATCH] [fix](trx-frontend): account for WFM interference in auto bandwidth --- .../trx-frontend-http/assets/web/app.js | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 5d40aeff..b6a0b257 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -840,6 +840,8 @@ let jogMult = loadSetting("jogMult", 1); // divisor: 1, 10, 100 let jogStep = Math.max(Math.round(jogUnit / jogMult), 1); let minFreqStepHz = 1; let lastModeName = ""; +let lastWfmCci = 0; +let lastWfmAci = 0; const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"]; function vfoColor(idx) { if (idx < VFO_COLORS.length) return VFO_COLORS[idx]; @@ -3293,8 +3295,14 @@ function render(update) { wfmStFlagEl.classList.toggle("wfm-st-flag-stereo", detected); wfmStFlagEl.classList.toggle("wfm-st-flag-mono", !detected); } - if (typeof update.filter.wfm_cci === "number") updateIntfBar(wfmCciFillEl, wfmCciValEl, update.filter.wfm_cci); - if (typeof update.filter.wfm_aci === "number") updateIntfBar(wfmAciFillEl, wfmAciValEl, update.filter.wfm_aci); + if (typeof update.filter.wfm_cci === "number") { + lastWfmCci = Math.max(0, Math.min(100, update.filter.wfm_cci)); + updateIntfBar(wfmCciFillEl, wfmCciValEl, lastWfmCci); + } + if (typeof update.filter.wfm_aci === "number") { + lastWfmAci = Math.max(0, Math.min(100, update.filter.wfm_aci)); + updateIntfBar(wfmAciFillEl, wfmAciValEl, lastWfmAci); + } if (samStereoWidthEl && typeof update.filter.sam_stereo_width === "number") { samStereoWidthEl.value = String(Math.round(update.filter.sam_stereo_width * 100)); } @@ -4348,7 +4356,7 @@ async function applyBandwidthFromInput() { } catch (_) {} } -function estimateOccupiedBandwidth(data, centerHz) { +function estimateOccupiedBandwidth(data, centerHz, interference = {}) { if (!data || !isBinsArray(data.bins) || data.bins.length < 3 || !Number.isFinite(centerHz)) { return null; } @@ -4428,13 +4436,30 @@ function estimateOccupiedBandwidth(data, centerHz) { // 60 kHz mode floor above: a narrower filter trades stereo/RDS content for // a useful improvement in intelligibility when the signal is very poor. rawBw *= isWfm ? 1.08 : 1.12; + if (isWfm) { + const aci = Math.max(0, Math.min(100, Number(interference.aci) || 0)) / 100; + const cci = Math.max(0, Math.min(100, Number(interference.cci) || 0)) / 100; + // Adjacent-channel energy is outside the wanted modulation, so ACI can + // safely drive the cap all the way from the 300 kHz ceiling to 60 kHz. + const aciCap = maxBw - (maxBw - minBw) * aci; + // CCI overlaps the wanted station and cannot be removed by an RF filter. + // Only distrust the widest edge estimates, retaining at least 65% of the + // useful range between the weak-signal floor and nominal WFM bandwidth. + const cciFloor = minBw + (defaultBw - minBw) * 0.65; + const cciCap = maxBw - (maxBw - cciFloor) * cci; + rawBw = Math.min(rawBw, aciCap, cciCap); + } const clamped = Math.max(minBw, Math.min(maxBw, rawBw)); return Math.max(stepBw, Math.round(clamped / stepBw) * stepBw); } async function applyAutoBandwidth() { if (!lastSpectrumData || lastFreqHz == null) return; - const estimated = estimateOccupiedBandwidth(lastSpectrumData, lastFreqHz); + // WFM interference telemetry belongs to the primary DSP channel. Do not + // apply it to a virtual channel, where it would describe the wrong signal. + const onVirtual = typeof vchanIsOnVirtual === "function" && vchanIsOnVirtual(); + const interference = onVirtual ? {} : { cci: lastWfmCci, aci: lastWfmAci }; + const estimated = estimateOccupiedBandwidth(lastSpectrumData, lastFreqHz, interference); if (!Number.isFinite(estimated) || estimated <= 0) { syncBandwidthInput(currentBandwidthHz); return;