diff --git a/docs/User-Manual.md b/docs/User-Manual.md index 3350bbc2..778047a7 100644 --- a/docs/User-Manual.md +++ b/docs/User-Manual.md @@ -752,9 +752,30 @@ A dedicated tab with a clock icon provides: ## SDR Noise Blanker The noise blanker suppresses impulse noise (clicks, pops, ignition interference) -on raw IQ samples before any mixing or filtering takes place. It works by -tracking a running RMS level of the signal and replacing any sample whose -magnitude exceeds **threshold x RMS** with the last known clean sample. +on raw IQ samples before any mixing or filtering takes place — the only point in +the chain where an impulse is still short in time, since the narrow channel +filter downstream smears it into un-removable ringing. + +It combines four elements: + +- A **noise-floor tracker** — an exponential estimate of the background level, + updated only from clean samples (and frozen during a blank) so a burst cannot + drag the reference up and blind the detector. +- **Detection** — a sample is flagged when its power exceeds + **threshold² × noise-floor**. +- **Look-ahead** — the stream is delayed a few microseconds so the gate can + begin closing *before* the impulse reaches the output, catching its leading + edge instead of letting it leak through. +- A **tapered gate** — instead of a hard sample-and-hold (which splatters energy + back across the passband and is what made the old blanker sound worse on + SSB/CW/data), the gain ramps smoothly down and back up, so blanking costs only + a short, quiet notch. + +The blank window, look-ahead, taper, and floor time constant are chosen by a +**profile** matched to the interference source. The `threshold` control is +orthogonal — it sets detection sensitivity within the chosen profile. All +profile timings are specified in real time and converted to samples at the +capture rate, so the blanker behaves consistently across SDR sample rates. ### Configuration (server-side) @@ -771,6 +792,7 @@ type = "sdr" [rigs.sdr.noise_blanker] enabled = true threshold = 10.0 # 1 – 100; lower = more aggressive blanking +profile = "spike" # spike | ignition | powerline | broadband ``` For the legacy single-rig (flat) config the path is `[sdr.noise_blanker]`: @@ -779,15 +801,30 @@ For the legacy single-rig (flat) config the path is `[sdr.noise_blanker]`: [sdr.noise_blanker] enabled = true threshold = 10.0 +profile = "spike" ``` -| Field | Type | Default | Range | Description | -|-------------|-------|---------|---------|-------------| -| `enabled` | bool | false | — | Turn the noise blanker on or off. | -| `threshold` | float | 10.0 | 1 – 100 | Multiplier applied to the running RMS. A sample whose magnitude exceeds this multiple is replaced. Lower values blank more aggressively; higher values only catch strong impulses. | +| Field | Type | Default | Range | Description | +|-------------|--------|-----------|---------|-------------| +| `enabled` | bool | false | — | Turn the noise blanker on or off. | +| `threshold` | float | 10.0 | 1 – 100 | Multiplier applied to the tracked noise floor. A sample whose magnitude exceeds this multiple is blanked. Lower values blank more aggressively; higher values only catch strong impulses. | +| `profile` | string | `"spike"` | see below | Tuning profile matched to the interference source. | The noise blanker is off by default. +### Profiles + +Each profile sets the blank-window width, look-ahead, gate taper, and +noise-floor time constant. Pick the one that matches what you are hearing, then +fine-tune with the threshold. + +| Profile | Blank window | Best for | +|--------------|--------------|----------| +| `spike` | Narrowest | Sharp, sparse impulses — ignition sparks, static crashes, keyed relays. The safe default: minimal impact on the wanted signal, good for SSB/CW/digital. | +| `ignition` | Medium | Automotive ignition, electric fences, PWM/LED drivers — clusters of medium-width pulses at a high repetition rate. | +| `powerline` | Wide | Power-line and arcing noise — buzzy bursts locked to the 100/120 Hz mains cycle. Uses a slower floor tracker to ride out the burst. | +| `broadband` | Widest | Dense, continuous impulse noise where suppression matters more than fidelity. Most aggressive gating; expect some softening of the wanted signal. | + ### Choosing a threshold The threshold controls how aggressively the blanker suppresses impulses. @@ -813,39 +850,44 @@ the running average signal level. ### Web UI -When the server reports noise-blanker support, two controls appear in the +When the server reports noise-blanker support, these controls appear in the **SDR Settings** row of the web interface: - **Noise Blanker** checkbox — enables or disables the blanker in real time. + The **N** keyboard shortcut toggles it too. - **NB Threshold** number input (1–100) with a **Set** button — adjusts the - detection threshold. Press Enter or click Set to apply. + detection sensitivity. Press Enter or click Set to apply. +- **NB profile** selector — chooses the profile (Spike / Ignition / Powerline / + Broadband). Changing it applies immediately. -Both controls stay hidden until the server sends filter state containing NB +The controls stay hidden until the server sends filter state containing NB fields, so they only appear when connected to an SDR backend. ### HTTP API ``` -POST /set_sdr_noise_blanker?enabled=true&threshold=10 +POST /set_sdr_noise_blanker?enabled=true&threshold=10&profile=spike ``` | Parameter | Type | Required | Description | |-------------|--------|----------|-------------| | `enabled` | bool | yes | `true` or `false` | | `threshold` | float | yes | Value between 1 and 100 | +| `profile` | string | no | `spike` (default), `ignition`, `powerline`, or `broadband` | ### How it works -The blanker runs on every IQ block (4096 samples) *before* the mixer stage in -the DSP pipeline: +The blanker runs on every IQ block *before* the mixer stage in the DSP pipeline, +one sample at a time: -1. For each sample, compute magnitude² (`re² + im²`). -2. Compare against `threshold² × mean_sq` (the exponentially-smoothed running - mean of magnitude²). -3. If the sample exceeds the threshold, replace it with the previous clean - sample. -4. Otherwise, update the running mean with smoothing factor α = 1/128 and store - the sample as the last clean value. +1. Emit the sample from the look-ahead delay line and ingest the fresh one. +2. Compute the fresh sample's power (`re² + im²`) and compare it against + `threshold² × noise_floor`. +3. If it exceeds the threshold, hold the gate closed for the profile's blank + window; the fresh sample reaches the output a few samples later, by which + time the gate has fully ramped to zero — so the leading edge is removed. +4. Otherwise, update the noise-floor estimate (skipped while blanking) and let + the gate ramp back open. Because the blanker operates on raw IQ before frequency translation, it removes impulse noise across the entire captured bandwidth regardless of the tuned diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index ce058897..2ee50206 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -4766,6 +4766,13 @@ function render(update) { sdrNbThresholdEl.value = String(Math.round(update.filter.sdr_nb_threshold)); } } + if (typeof update.filter.sdr_nb_profile === "string") { + sdrNbProfile = update.filter.sdr_nb_profile; + if (sdrNbProfileWrapEl) sdrNbProfileWrapEl.style.display = ""; + if (sdrNbProfileEl && document.activeElement !== sdrNbProfileEl) { + sdrNbProfileEl.value = update.filter.sdr_nb_profile; + } + } } if (typeof update.filter.sdr_dig_sideband === "string") { sdrDigSidebandSupported = true; @@ -6644,7 +6651,10 @@ var sdrNbEnabledEl = document.getElementById("sdr-nb-enabled"); var sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-controls"); var sdrNbThresholdEl = document.getElementById("sdr-nb-threshold"); var sdrNbThresholdSetBtn = document.getElementById("sdr-nb-threshold-set"); +var sdrNbProfileWrapEl = document.getElementById("sdr-nb-profile-wrap"); +var sdrNbProfileEl = document.getElementById("sdr-nb-profile"); var sdrNbSupported = false; +var sdrNbProfile = "spike"; var sdrDigSidebandWrapEl = document.getElementById("sdr-dig-sideband-wrap"); var sdrDigSidebandEl = document.getElementById("sdr-dig-sideband"); var sdrDigSidebandSupported = false; @@ -7019,7 +7029,7 @@ function submitSdrNbState() { const threshold = sdrNbThresholdEl ? Number.parseFloat(sdrNbThresholdEl.value) : 10; if (!isFiniteNumber(threshold) || threshold < 1 || threshold > 100) return; postPath( - `/set_sdr_noise_blanker?enabled=${enabled ? "true" : "false"}&threshold=${encodeURIComponent(threshold)}` + `/set_sdr_noise_blanker?enabled=${enabled ? "true" : "false"}&threshold=${encodeURIComponent(threshold)}&profile=${encodeURIComponent(sdrNbProfile)}` ).catch(() => { }); } @@ -7028,6 +7038,16 @@ if (sdrNbEnabledEl) { submitSdrNbState(); }); } +if (sdrNbProfileEl) { + sdrNbProfileEl.addEventListener("change", () => { + const profile = sdrNbProfileEl.value || "spike"; + if (profile !== "spike" && profile !== "ignition" && profile !== "powerline" && profile !== "broadband") { + return; + } + sdrNbProfile = profile; + submitSdrNbState(); + }); +} function submitSdrNbThreshold() { if (!sdrNbThresholdEl) return; const parsed = Number.parseFloat(sdrNbThresholdEl.value); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 7caecf67..578fff9e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -385,6 +385,15 @@ SPDX-License-Identifier: GPL-2.0-or-later +