Skip to content

Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)#1508

Closed
aethersdr-agent[bot] wants to merge 1 commit into
mainfrom
aetherclaude/issue-1507
Closed

Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)#1508
aethersdr-agent[bot] wants to merge 1 commit into
mainfrom
aetherclaude/issue-1507

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #1507

What was changed

Fix NR2 Gamma crackling by using exponentially-scaled Bessel functions (#1507)

Files modified

  • src/core/SpectralNR.cpp
  • src/core/SpectralNR.h
 src/core/SpectralNR.cpp | 26 +++++++++++++++-----------
 src/core/SpectralNR.h   |  6 +++---
 2 files changed, 18 insertions(+), 14 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)

#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>
@ten9876

ten9876 commented Apr 18, 2026

Copy link
Copy Markdown
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)

@ten9876 ten9876 closed this Apr 18, 2026
@ten9876 ten9876 deleted the aetherclaude/issue-1507 branch April 25, 2026 21:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NR2 Gamma (MMSE-LSA) produces audible crackling on strong signals — current AetherSDR NR2 does not match the Thetis/WDSP NR2 users expect

2 participants