Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)#1508
Closed
aethersdr-agent[bot] wants to merge 1 commit into
Closed
Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)#1508aethersdr-agent[bot] wants to merge 1 commit into
aethersdr-agent[bot] wants to merge 1 commit into
Conversation
#1507) Replace bessI0/bessI1 with bessI0e/bessI1e (exp-scaled variants matching WDSP emnr.c) to eliminate intermediate overflow when the SNR term v exceeds ~1420. Remove the now-unnecessary exp(-0.5*v) factor from the gain expression in computeGainGamma() — the cancellation happens inside the scaled Bessel functions, so no intermediate can reach +inf. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 tasks
NF0T
added a commit
to NF0T/AetherSDR
that referenced
this pull request
Apr 18, 2026
…scaled Bessel functions
Collaborator
|
Claude here — closing this PR. The Bessel overflow fix is mathematically correct but doesn't address the root cause of NR2 crackling with Opus/SmartLink connections. The real issue is that NR2's spectral gain estimator amplifies Opus compression artifacts. The fix is to disable NR2 selection when Opus audio is detected. Filed as a new issue. 73, Jeremy KK7GWY & Claude (AI dev partner) |
7 tasks
jensenpat
pushed a commit
that referenced
this pull request
May 31, 2026
…led variants to eliminate NR2 Gamma crackling (#1507) (#3275) ## Summary - Replaces `bessI0()`/`bessI1()` in `SpectralNR` with exponentially-scaled `bessI0e()`/`bessI1e()` that return `exp(-|x|) * Iₙ(x)` instead of `Iₙ(x)` directly - Removes the now-redundant `std::exp(-0.5 * v)` factor from `computeGainGamma()` — the cancellation happens inside the scaled functions, no overflow possible - Adds `spectral_nr_test` (112 assertions) covering Bessel finiteness, mathematical equivalence, integration, and gain formula at extreme v - OTA validated TC-1 through TC-7 on FLEX-8400 with no crackling at any tested signal level, GainMax setting, or NPE method Fixes #1507. ## Root cause The large-x branch of `bessI0()`/`bessI1()` (A&S 9.8.1/9.8.2) computed `std::exp(ax) / std::sqrt(ax) * poly`. In `computeGainGamma()`, the Ephraim-Malah gain expression is: ``` gain = Γ(3/2) · √v / γ · exp(-v/2) · [(1+v)·I₀(v/2) + v·I₁(v/2)] ``` The `exp(-v/2)` factor is intended to cancel the `exp(ax)` inside `bessI0`/`bessI1`, but because they are computed as separate intermediate values, the Bessel calls overflow to `+inf` when `v/2 > 710` (i.e. `v > 1420`). The product `0 · ∞ = NaN` per IEEE 754. The NaN guard clamps gain to `0.01` (~−40 dB) for that hop, producing a suppression burst every 5.33 ms — audible as crackling. `v = ehr · γ` where `γ` is capped at `GammaMax = 1e4`, so the overflow threshold (`v > 1420`) is crossed for any signal with SNR above ~31.5 dB under the Gamma gain method. This is routine for a strong HF signal or any in-band carrier. PR #1696 (merged April 2026) addressed a distinct mechanism — `gainMax = 1.5` causing output amplification above ±1.0 float32, clipped at the QAudioSink. That fix is orthogonal; the Bessel overflow is still present in the current tree. ## Investigation note re: PR #1508 PR #1508 (the earlier automated Bessel fix) was closed on 2026-04-18 with the rationale that Opus compression artifacts were "the real root cause." After further investigation we believe both issues are real but independent: - **Opus/SmartLink path**: NR2's spectral gain estimator amplifies Opus codec artifacts — valid, should be tracked separately - **Bessel overflow path**: triggered by any signal with SNR > 31.5 dB, regardless of audio codec — present on direct DAX connections with no Opus in the chain The original reporter used a FLEX-8600 on a direct connection ("inject a strong signal — a −10 dBm test tone, or tune to a strong local broadcast"), not a SmartLink session. Disabling NR2 under Opus would not have fixed their report. This PR addresses only the Bessel path; the Opus interaction is left for a separate issue and fix. ## Changes **`src/core/SpectralNR.h`** — rename two private static declarations: ```cpp // Before static double bessI0(double x); static double bessI1(double x); // After static double bessI0e(double x); // returns exp(-|x|) * I0(x) static double bessI1e(double x); // returns exp(-|x|) * I1(x) ``` **`src/core/SpectralNR.cpp`** — rewrite both Bessel implementations and update the calling site: - *Small-x branch*: existing polynomial multiplied by `std::exp(-ax)` — trivial, no overflow possible - *Large-x branch*: `exp(-ax) · exp(ax) / √ax · poly` reduces analytically to `1 / √ax · poly` — **no exponential computed at all**; the overflow path is eliminated by construction - `computeGainGamma()`: remove `std::exp(-0.5 * v)` from the gain expression; rename Bessel calls ```cpp // Before double gain = gf1p5 * std::sqrt(v) / std::max(gamma, EpsFloor) * std::exp(-0.5 * v) * ((1.0 + v) * bessI0(0.5 * v) + v * bessI1(0.5 * v)); // After double gain = gf1p5 * std::sqrt(v) / std::max(gamma, EpsFloor) * ((1.0 + v) * bessI0e(0.5 * v) + v * bessI1e(0.5 * v)); ``` Result is mathematically identical to the original for all `v < 1420`; numerically stable for `v` up to `GammaMax = 1e4` and beyond. **`tests/spectral_nr_test.cpp`** (new) and **`CMakeLists.txt`** — standalone test target `spectral_nr_test`, Qt6::Core only, no FFTW3 dependency: | Group | What it tests | Assertions | |---|---|---| | Bessel finiteness | `bessI0e`/`bessI1e` finite and non-negative at `v/2` for v ∈ {0…10000}, explicitly straddling v=1420 | 52 | | Mathematical equivalence | Matches `exp(-x)·Iₙ_ref(x)` to 1e-9 relative error at x ≤ 50; symmetry / antisymmetry | 34 | | Gain finiteness | `SpectralNR::process()`, Gamma method, 1000 hops full-scale 1 kHz sine: zero NaN/Inf samples | 1 | | Gain formula extreme v | Direct formula at v ∈ {1419, 1420, 1421, 2000, 5000, 10000}: finite, positive, not the 0.01 NaN-clamp sentinel | 24 | ## What this does NOT change - `processNr2()` output clamp and NaN guard — retained as belt-and-suspenders - `GammaMax = 1e4` — no change needed; the full range is now numerically safe - All other gain methods (Linear, Log, Trained) — unaffected; they do not call Bessel functions - No behaviour change for signals below the overflow threshold (`v < 1420`, SNR < 31.5 dB) ## Test plan Automated: `ctest -R spectral_nr_test` — 112/112 pass. OTA (FLEX-8400, Gamma method, FFTW3 enabled production build): - [x] TC-1: Strong broadcast/SSB S9+10, 60 s — no crackling - [x] TC-2: Steady carrier S9+20–S9+40 (maximum-gamma stress) — clean pass-through - [x] TC-3: GainMax 0.5 / 1.0 / 1.5 — no crackling at any value - [x] TC-4: Enable NR2 mid-signal (cold OSMS ramp) — clean transition - [x] TC-5: All four gain methods compared — no regressions - [x] TC-6: Weak-signal NR effectiveness — unchanged - [x] TC-7: MMSE and NSTAT NPE methods — no crackling 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…led variants to eliminate NR2 Gamma crackling (aethersdr#1507) (aethersdr#3275) ## Summary - Replaces `bessI0()`/`bessI1()` in `SpectralNR` with exponentially-scaled `bessI0e()`/`bessI1e()` that return `exp(-|x|) * Iₙ(x)` instead of `Iₙ(x)` directly - Removes the now-redundant `std::exp(-0.5 * v)` factor from `computeGainGamma()` — the cancellation happens inside the scaled functions, no overflow possible - Adds `spectral_nr_test` (112 assertions) covering Bessel finiteness, mathematical equivalence, integration, and gain formula at extreme v - OTA validated TC-1 through TC-7 on FLEX-8400 with no crackling at any tested signal level, GainMax setting, or NPE method Fixes aethersdr#1507. ## Root cause The large-x branch of `bessI0()`/`bessI1()` (A&S 9.8.1/9.8.2) computed `std::exp(ax) / std::sqrt(ax) * poly`. In `computeGainGamma()`, the Ephraim-Malah gain expression is: ``` gain = Γ(3/2) · √v / γ · exp(-v/2) · [(1+v)·I₀(v/2) + v·I₁(v/2)] ``` The `exp(-v/2)` factor is intended to cancel the `exp(ax)` inside `bessI0`/`bessI1`, but because they are computed as separate intermediate values, the Bessel calls overflow to `+inf` when `v/2 > 710` (i.e. `v > 1420`). The product `0 · ∞ = NaN` per IEEE 754. The NaN guard clamps gain to `0.01` (~−40 dB) for that hop, producing a suppression burst every 5.33 ms — audible as crackling. `v = ehr · γ` where `γ` is capped at `GammaMax = 1e4`, so the overflow threshold (`v > 1420`) is crossed for any signal with SNR above ~31.5 dB under the Gamma gain method. This is routine for a strong HF signal or any in-band carrier. PR aethersdr#1696 (merged April 2026) addressed a distinct mechanism — `gainMax = 1.5` causing output amplification above ±1.0 float32, clipped at the QAudioSink. That fix is orthogonal; the Bessel overflow is still present in the current tree. ## Investigation note re: PR aethersdr#1508 PR aethersdr#1508 (the earlier automated Bessel fix) was closed on 2026-04-18 with the rationale that Opus compression artifacts were "the real root cause." After further investigation we believe both issues are real but independent: - **Opus/SmartLink path**: NR2's spectral gain estimator amplifies Opus codec artifacts — valid, should be tracked separately - **Bessel overflow path**: triggered by any signal with SNR > 31.5 dB, regardless of audio codec — present on direct DAX connections with no Opus in the chain The original reporter used a FLEX-8600 on a direct connection ("inject a strong signal — a −10 dBm test tone, or tune to a strong local broadcast"), not a SmartLink session. Disabling NR2 under Opus would not have fixed their report. This PR addresses only the Bessel path; the Opus interaction is left for a separate issue and fix. ## Changes **`src/core/SpectralNR.h`** — rename two private static declarations: ```cpp // Before static double bessI0(double x); static double bessI1(double x); // After static double bessI0e(double x); // returns exp(-|x|) * I0(x) static double bessI1e(double x); // returns exp(-|x|) * I1(x) ``` **`src/core/SpectralNR.cpp`** — rewrite both Bessel implementations and update the calling site: - *Small-x branch*: existing polynomial multiplied by `std::exp(-ax)` — trivial, no overflow possible - *Large-x branch*: `exp(-ax) · exp(ax) / √ax · poly` reduces analytically to `1 / √ax · poly` — **no exponential computed at all**; the overflow path is eliminated by construction - `computeGainGamma()`: remove `std::exp(-0.5 * v)` from the gain expression; rename Bessel calls ```cpp // Before double gain = gf1p5 * std::sqrt(v) / std::max(gamma, EpsFloor) * std::exp(-0.5 * v) * ((1.0 + v) * bessI0(0.5 * v) + v * bessI1(0.5 * v)); // After double gain = gf1p5 * std::sqrt(v) / std::max(gamma, EpsFloor) * ((1.0 + v) * bessI0e(0.5 * v) + v * bessI1e(0.5 * v)); ``` Result is mathematically identical to the original for all `v < 1420`; numerically stable for `v` up to `GammaMax = 1e4` and beyond. **`tests/spectral_nr_test.cpp`** (new) and **`CMakeLists.txt`** — standalone test target `spectral_nr_test`, Qt6::Core only, no FFTW3 dependency: | Group | What it tests | Assertions | |---|---|---| | Bessel finiteness | `bessI0e`/`bessI1e` finite and non-negative at `v/2` for v ∈ {0…10000}, explicitly straddling v=1420 | 52 | | Mathematical equivalence | Matches `exp(-x)·Iₙ_ref(x)` to 1e-9 relative error at x ≤ 50; symmetry / antisymmetry | 34 | | Gain finiteness | `SpectralNR::process()`, Gamma method, 1000 hops full-scale 1 kHz sine: zero NaN/Inf samples | 1 | | Gain formula extreme v | Direct formula at v ∈ {1419, 1420, 1421, 2000, 5000, 10000}: finite, positive, not the 0.01 NaN-clamp sentinel | 24 | ## What this does NOT change - `processNr2()` output clamp and NaN guard — retained as belt-and-suspenders - `GammaMax = 1e4` — no change needed; the full range is now numerically safe - All other gain methods (Linear, Log, Trained) — unaffected; they do not call Bessel functions - No behaviour change for signals below the overflow threshold (`v < 1420`, SNR < 31.5 dB) ## Test plan Automated: `ctest -R spectral_nr_test` — 112/112 pass. OTA (FLEX-8400, Gamma method, FFTW3 enabled production build): - [x] TC-1: Strong broadcast/SSB S9+10, 60 s — no crackling - [x] TC-2: Steady carrier S9+20–S9+40 (maximum-gamma stress) — clean pass-through - [x] TC-3: GainMax 0.5 / 1.0 / 1.5 — no crackling at any value - [x] TC-4: Enable NR2 mid-signal (cold OSMS ramp) — clean transition - [x] TC-5: All four gain methods compared — no regressions - [x] TC-6: Weak-signal NR effectiveness — unchanged - [x] TC-7: MMSE and NSTAT NPE methods — no crackling 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1507
What was changed
Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)
Files modified
src/core/SpectralNR.cppsrc/core/SpectralNR.hGenerated by AetherClaude (automated agent for AetherSDR)