[fix](trx-frontend-http): fix waterfall freeze, RDS bandwidth, add jog multiplier
Fix waterfall overview freezing at steady state by tracking a monotonic push counter instead of row array length — the array size stays constant once the waterfall is full, so the previous row-count comparison never triggered the incremental draw path. Fix WFM RDS not decoding when switching to WFM from a narrowband mode: set_mode now resets audio_bandwidth_hz to the mode-appropriate default (180 kHz for WFM) before rebuilding the FIR, preventing the 57 kHz RDS subcarrier from being filtered out. Add 1×/10×/100× multiplier button group next to the jog unit selector. jogUnit × jogMult gives the effective jog step; both are persisted to localStorage independently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -349,7 +349,9 @@ let sigMeasureWeighted = 0;
|
||||
let sigMeasurePeak = null;
|
||||
let lastFreqHz = null;
|
||||
let centerFreqDirty = false;
|
||||
let jogStep = loadSetting("jogStep", 1000);
|
||||
let jogUnit = loadSetting("jogUnit", 1000); // base unit: 1, 1000, 1000000
|
||||
let jogMult = loadSetting("jogMult", 1); // multiplier: 1, 10, 100
|
||||
let jogStep = Math.max(jogUnit * jogMult, 1);
|
||||
let minFreqStepHz = 1;
|
||||
const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"];
|
||||
function vfoColor(idx) {
|
||||
@@ -625,14 +627,15 @@ let reconnectTimer = null;
|
||||
let overviewSignalSamples = [];
|
||||
let overviewSignalTimer = null;
|
||||
let overviewWaterfallRows = [];
|
||||
let overviewWaterfallPushCount = 0; // monotonically increments on every push
|
||||
const HEADER_SIG_WINDOW_MS = 10_000;
|
||||
|
||||
// Offscreen waterfall cache — reused across frames to avoid full redraws
|
||||
let _wfOC = null; // OffscreenCanvas
|
||||
let _wfOCPalKey = ""; // palette signature when offscreen was last built
|
||||
let _wfOCRowCount = 0; // number of rows currently rendered into offscreen
|
||||
let _wfOC = null; // OffscreenCanvas
|
||||
let _wfOCPalKey = ""; // palette signature when offscreen was last built
|
||||
let _wfOCPushCount = 0; // overviewWaterfallPushCount when offscreen was last updated
|
||||
|
||||
function _wfResetOffscreen() { _wfOC = null; _wfOCRowCount = 0; _wfOCPalKey = ""; }
|
||||
function _wfResetOffscreen() { _wfOC = null; _wfOCPushCount = 0; _wfOCPalKey = ""; }
|
||||
function _wfPalKey(pal) {
|
||||
return `${pal.waterfallHue}|${pal.waterfallSat}|${pal.waterfallLight}|${pal.waterfallAlpha}`;
|
||||
}
|
||||
@@ -700,6 +703,7 @@ function overviewVisibleBinWindow(data, binCount) {
|
||||
function pushOverviewWaterfallFrame(data) {
|
||||
if (!overviewCanvas || !data || !Array.isArray(data.bins) || data.bins.length === 0) return;
|
||||
overviewWaterfallRows.push(data.bins.slice());
|
||||
overviewWaterfallPushCount++;
|
||||
trimOverviewWaterfallRows();
|
||||
scheduleOverviewDraw();
|
||||
}
|
||||
@@ -762,27 +766,29 @@ function drawOverviewWaterfall(ctx, w, h, pal) {
|
||||
const iH = Math.ceil(h);
|
||||
const palKey = _wfPalKey(pal);
|
||||
const steadyState = rows.length >= maxVisible;
|
||||
// How many rows were pushed since the offscreen was last updated
|
||||
const newPushes = overviewWaterfallPushCount - _wfOCPushCount;
|
||||
|
||||
// Detect conditions that require a full redraw
|
||||
const sizeChanged = !_wfOC || _wfOC.width !== iW || _wfOC.height !== iH;
|
||||
const palChanged = _wfOCPalKey !== palKey;
|
||||
const rowsShrank = rows.length < _wfOCRowCount;
|
||||
const needsFull = sizeChanged || palChanged || rowsShrank || _wfOCRowCount === 0;
|
||||
const needsFull = sizeChanged || palChanged || _wfOCPushCount === 0;
|
||||
|
||||
if (sizeChanged || !_wfOC) {
|
||||
_wfOC = new OffscreenCanvas(iW, iH);
|
||||
_wfOCRowCount = 0;
|
||||
_wfOCPushCount = 0;
|
||||
}
|
||||
const oct = _wfOC.getContext("2d");
|
||||
|
||||
if (needsFull) {
|
||||
oct.clearRect(0, 0, iW, iH);
|
||||
_wfDrawRows(oct, rows, 0, rows.length, iW, iH, pal);
|
||||
_wfOCRowCount = rows.length;
|
||||
_wfOCPalKey = palKey;
|
||||
} else if (steadyState && rows.length > _wfOCRowCount) {
|
||||
// Steady state: scroll up and paint only the new rows at the bottom
|
||||
const newCount = rows.length - _wfOCRowCount;
|
||||
_wfOCPushCount = overviewWaterfallPushCount;
|
||||
_wfOCPalKey = palKey;
|
||||
} else if (steadyState && newPushes > 0) {
|
||||
// Steady state: scroll up and paint only the new rows at the bottom.
|
||||
// newPushes new rows are at the tail of `rows`; each replaces one old row.
|
||||
const newCount = Math.min(newPushes, rows.length);
|
||||
const rowH = iH / rows.length;
|
||||
const scrollPx = Math.round(newCount * rowH);
|
||||
if (scrollPx > 0 && scrollPx < iH) {
|
||||
@@ -791,7 +797,7 @@ function drawOverviewWaterfall(ctx, w, h, pal) {
|
||||
oct.clearRect(0, iH - scrollPx, iW, scrollPx);
|
||||
}
|
||||
_wfDrawRows(oct, rows, rows.length - newCount, rows.length, iW, iH, pal);
|
||||
_wfOCRowCount = rows.length;
|
||||
_wfOCPushCount = overviewWaterfallPushCount;
|
||||
}
|
||||
|
||||
ctx.drawImage(_wfOC, 0, 0, w, h);
|
||||
@@ -1649,6 +1655,16 @@ const jogIndicator = document.getElementById("jog-indicator");
|
||||
const jogDownBtn = document.getElementById("jog-down");
|
||||
const jogUpBtn = document.getElementById("jog-up");
|
||||
const jogStepEl = document.getElementById("jog-step");
|
||||
const jogMultEl = document.getElementById("jog-mult");
|
||||
|
||||
function applyJogStep() {
|
||||
jogStep = Math.max(jogUnit * jogMult, minFreqStepHz);
|
||||
saveSetting("jogUnit", jogUnit);
|
||||
saveSetting("jogMult", jogMult);
|
||||
saveSetting("jogStep", jogStep);
|
||||
refreshFreqDisplay();
|
||||
refreshCenterFreqDisplay();
|
||||
}
|
||||
|
||||
async function jogFreq(direction) {
|
||||
if (lastLocked) { showHint("Locked", 1500); return; }
|
||||
@@ -1716,29 +1732,51 @@ window.addEventListener("mouseup", () => {
|
||||
if (jogWheel) jogWheel.style.cursor = "grab";
|
||||
});
|
||||
|
||||
// Step selector
|
||||
// Step unit selector
|
||||
jogStepEl.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-step]");
|
||||
if (!btn) return;
|
||||
jogStep = Math.max(parseInt(btn.dataset.step, 10), minFreqStepHz);
|
||||
jogUnit = parseInt(btn.dataset.step, 10);
|
||||
jogStepEl.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
saveSetting("jogStep", jogStep);
|
||||
refreshFreqDisplay();
|
||||
refreshCenterFreqDisplay();
|
||||
applyJogStep();
|
||||
});
|
||||
|
||||
// Restore active jog step button from saved setting
|
||||
// Step multiplier selector
|
||||
if (jogMultEl) {
|
||||
jogMultEl.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-mult]");
|
||||
if (!btn) return;
|
||||
jogMult = parseInt(btn.dataset.mult, 10);
|
||||
jogMultEl.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
applyJogStep();
|
||||
});
|
||||
}
|
||||
|
||||
// Restore active jog step buttons from saved settings
|
||||
{
|
||||
const buttons = Array.from(jogStepEl.querySelectorAll("button[data-step]"));
|
||||
const active =
|
||||
buttons.find((b) => parseInt(b.dataset.step, 10) === jogStep) ||
|
||||
buttons.find((b) => parseInt(b.dataset.step, 10) === 1000) ||
|
||||
buttons[0];
|
||||
if (active) {
|
||||
jogStep = parseInt(active.dataset.step, 10);
|
||||
buttons.forEach((b) => b.classList.toggle("active", b === active));
|
||||
const unitBtns = Array.from(jogStepEl.querySelectorAll("button[data-step]"));
|
||||
const activeUnit =
|
||||
unitBtns.find((b) => parseInt(b.dataset.step, 10) === jogUnit) ||
|
||||
unitBtns.find((b) => parseInt(b.dataset.step, 10) === 1000) ||
|
||||
unitBtns[0];
|
||||
if (activeUnit) {
|
||||
jogUnit = parseInt(activeUnit.dataset.step, 10);
|
||||
unitBtns.forEach((b) => b.classList.toggle("active", b === activeUnit));
|
||||
}
|
||||
if (jogMultEl) {
|
||||
const multBtns = Array.from(jogMultEl.querySelectorAll("button[data-mult]"));
|
||||
const activeMult =
|
||||
multBtns.find((b) => parseInt(b.dataset.mult, 10) === jogMult) ||
|
||||
multBtns.find((b) => parseInt(b.dataset.mult, 10) === 1) ||
|
||||
multBtns[0];
|
||||
if (activeMult) {
|
||||
jogMult = parseInt(activeMult.dataset.mult, 10);
|
||||
multBtns.forEach((b) => b.classList.toggle("active", b === activeMult));
|
||||
}
|
||||
}
|
||||
jogStep = Math.max(jogUnit * jogMult, minFreqStepHz);
|
||||
}
|
||||
|
||||
async function applyModeFromPicker() {
|
||||
@@ -2848,6 +2886,7 @@ function startSpectrumStreaming() {
|
||||
if (evt.data === "null") {
|
||||
lastSpectrumData = null;
|
||||
overviewWaterfallRows = [];
|
||||
overviewWaterfallPushCount = 0;
|
||||
_wfResetOffscreen();
|
||||
scheduleOverviewDraw();
|
||||
clearSpectrumCanvas();
|
||||
@@ -2883,6 +2922,7 @@ function stopSpectrumStreaming() {
|
||||
spectrumDrawPending = false;
|
||||
lastSpectrumData = null;
|
||||
overviewWaterfallRows = [];
|
||||
overviewWaterfallPushCount = 0;
|
||||
_wfResetOffscreen();
|
||||
scheduleOverviewDraw();
|
||||
updateRdsPsOverlay(null);
|
||||
|
||||
@@ -120,6 +120,14 @@
|
||||
</div>
|
||||
<div class="label"><span>Unit</span></div>
|
||||
</div>
|
||||
<div class="freq-field mult-col">
|
||||
<div class="jog-mult" id="jog-mult">
|
||||
<button type="button" data-mult="1" class="active">1×</button>
|
||||
<button type="button" data-mult="10">10×</button>
|
||||
<button type="button" data-mult="100">100×</button>
|
||||
</div>
|
||||
<div class="label"><span>Mult</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full-row controls-tray-shell">
|
||||
|
||||
@@ -343,6 +343,38 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mult-col {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.mult-col .label {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.jog-mult {
|
||||
display: flex;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: 3.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.jog-mult button {
|
||||
border: none;
|
||||
border-right: 1px solid var(--border-light);
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.jog-mult button:last-child { border-right: none; }
|
||||
.jog-mult button.active {
|
||||
background: var(--btn-bg);
|
||||
color: var(--accent-green);
|
||||
font-weight: 600;
|
||||
}
|
||||
.label-below-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user