Skip to content

AI Noise reduction feature. - #448

Open
vis4573 wants to merge 13 commits into
drexjj:mainfrom
vis4573:main
Open

AI Noise reduction feature.#448
vis4573 wants to merge 13 commits into
drexjj:mainfrom
vis4573:main

Conversation

@vis4573

@vis4573 vis4573 commented Jul 24, 2026

Copy link
Copy Markdown

I've been experimenting with adding neural-network noise reduction (AINR) to the sBitx firmware — it uses Xiph.org's RNNoise, a small AI model trained on thousands of hours of speech, so instead of just filtering frequencies it actually recognizes what a human voice sounds like and mutes everything else.

Results on 40m SSB have been impressive — band hiss between words drops to near silence while the voice stays clean. Works alongside the existing DSP/ANR options (use one at a time).

Two on-screen controls: AINR (on/off, main screen next to REC) and strength/weak-signal tuning. Runs fine on the stock Pi, ~15ms latency, voice modes only (won't touch your FT8).

Code is on my fork of the drexjj 64-bit firmware if anyone wants to try:
github.com/vis4573/sbitx (branch: main)

Build with the usual ./build sbitx. Feedback welcome — especially reports on weak-signal behavior!
73, VU3UBP
vu3ubp@gmail.com

vis4573 and others added 13 commits July 12, 2026 03:26
- src/sbitx.c: the spectral subtraction sigmoid used a hardcoded 0.5
  midpoint; scaled_noise_threshold was computed from the UI slider but
  never referenced anywhere. Wired it in as the sigmoid midpoint so the
  DSP noise threshold field actually affects noise reduction aggressiveness.
- src/sbitx_gtk.c: rescaled scaleNoiseThreshold()'s output range from
  0.001-0.01 to 0.1-2.0 so it's large enough to meaningfully shift the
  sigmoid midpoint (old range was ~50x too small to matter against typical
  SNR values). Verified numerically that the new range produces a real
  spread of reduction factors across the slider's full travel.
- docs/rnnoise-integration-scope.md: scoping notes only, no code, for
  evaluating RNNoise as a future NR mode alongside the existing spectral
  subtraction / Wiener ANR stages.

Not build-tested end to end (wiringPi/GTK deps unavailable in this
environment) - change is small/localized and was verified numerically
in isolation. Recommend testing on actual hardware before merging.
Builds natively on ubuntu-24.04-arm (matches Pi 4/5 architecture, no
cross-compilation/emulation) on push to main/feature branches and PRs
to main. Installs build deps via apt (all available in Ubuntu 24.04
universe, including libwiringpi-dev), builds ft8_lib then sbitx,
verifies the binary exists, and uploads it as a downloadable artifact.

Verified locally on x86_64 with the same dependency set (this repo's
Makefile has no arch-specific flags) - full build succeeds including
linking; the only step that fails here is the Makefile's 'sudo setcap'
call, because sudo isn't installed in this sandbox. That will work
normally on actual GitHub-hosted runners.
New RNN toggle (Menu 1, line 1, next to the other NR controls) applies
RNNoise - a small recurrent neural network trained for real-time speech
denoising - to the RX speaker audio in voice modes. Selectable
independently of the existing DSP (spectral subtraction) and ANR
(Wiener) stages; help text advises using one NR method at a time.

Implementation:
- rnnoise/: vendored xiph/rnnoise v0.1.1 source (BSD-licensed, COPYING
  included). This version embeds the trained model in rnn_data.c, so no
  network access is needed at build time. Its FFT symbols are prefixed
  rnn_fft_*, so no collision with ft8_lib's kiss_fft.
- src/sbitx_rnn.c/h: wrapper handling 96k<->48k rate conversion (plain
  2:1 decimation - audio is band-limited far below 24 kHz by the RX
  bandpass; linear interpolation back up), amplitude scaling between
  sbitx's int32 speaker range and RNNoise's int16-float range, and ring
  buffering into RNNoise's fixed 480-sample frames. ~10-20 ms latency.
- src/sbitx.c: rnn_enabled global; hook in the voice-mode post-processing
  path after modem_rx (decoders see unprocessed audio) and before the RX
  EQ. State is reset when toggled off.
- src/sbitx_gtk.c: #rnn_plugin toggle field, check_plugin_controls
  wiring, and menu placement, following the ANR pattern. Works from the
  console as \rnn on / \rnn off via the generic field handler.
- Makefile: rnnoise sources and include path added; ./build sbitx works
  unchanged since it wraps make.

Verified: full build succeeds; functional test feeding 2s of 1 kHz tone
+ white noise at speaker scale through the wrapper passes signal at
matching amplitude (in 1.061e8 RMS vs out 1.063e8) with no overflow or
silence. Audio quality needs on-air evaluation on real hardware.
Full-strength RNNoise gating swings band gains between ~0 and 1 as its
speech confidence changes, which is audible as volume pumping/breathing,
especially on fading (QSB) SSB signals - reported in on-air testing.

Standard mitigation: blend the denoised (wet) output with an identically
delayed copy of the original (dry) signal. At strength S the output can
never drop below (1 - S/100) of the input, bounding the gain swings that
cause pumping while retaining most of the noise reduction.

- src/sbitx_rnn.c: parallel dry ring buffer kept in lockstep with the
  denoised output; per-block wet/dry mix from new rnn_strength global
  (0-100, default 80).
- src/sbitx_gtk.c: #rnn_strength number field (RNNS, 0-100 step 5) read
  periodically in check_plugin_controls so \rnns console edits apply
  within a second; placed in Menu 1 line 2 under the RNN toggle.
- help_commands.txt: RNNS documented with tuning guidance (70-85 for
  SSB, lower if pumping persists).

Verified: build succeeds; noise-only test shows correct monotonic
behavior - strength 0 passes input (2.51e7 RMS), 50 halves it (1.26e7),
100 gives ~23x suppression (1.09e6).
- #rnn_plugin now a COMMON_CONTROL at (370,5), directly left of REC on
  the main screen top row, always visible - no need to open Menu 1.
  Also positioned in the scaled-UI layout branch.
- #rnn_strength (RNNS) appears at (330,5), immediately left of RNN, only
  while RNN is ON; parked off-screen otherwise. Visibility handled in
  check_plugin_controls (periodic timer, reacts within ~1s of toggling).
  Both fields are COMMON_CONTROL so the generic layout hide-loop leaves
  them alone and check_plugin_controls is the single visibility authority.
- Removed the Menu 1 placements (RNN/RNNS no longer live there).
- Help text updated with the new location.

Build verified (binary links; only the sandbox's missing sudo fails at
the setcap step). Touch layout needs eyes-on verification on the 7-inch
screen: confirm RNN doesn't overlap REC and RNNS appears/disappears with
the toggle.
UI labels and help text only; internal field cmds (#rnn_plugin,
#rnn_strength) and code identifiers unchanged. Console commands become
\ainr on/off and \ainrs <0-100> via the generic label-matched handler.
rnnoise_process_frame() returns the network's per-frame voice-activity
probability, previously discarded. On weak/fading signals that
confidence sits in the uncertain middle and the band gains chatter,
eating fragile speech. Now the wet mix is relaxed as the smoothed VAD
rises:

  effective_wet = AINRS_mix * (1 - 0.5 * vad_smooth)

- Frames the network is confident are noise: full configured suppression
  (silence stays silent).
- Any speech evidence, even weak: suppression depth backs off by up to
  half, protecting the speech at the cost of a little more noise around
  it.
- VAD smoothing is fast-attack (~30 ms, catches syllable onsets) and
  slow-release (~0.5 s, rides through inter-word gaps without flutter).
- vad_ring kept sample-aligned with the wet/dry rings across block
  boundaries; state cleared in rnn_reset().

Verified: synthetic speech-like harmonic content + noise shows ~2x more
residual noise immediately after speech (relaxed) vs settled noise-only
(full suppression), with strength=100. Attack/release/relax constants
are first-pass values pending on-air feedback.
On-air testing reported: noise halo between words on weak signals strong
enough to feel like AINR was off, plus overall slow fading in/out.

Root causes at v1 constants: VAD_RELAX 0.5 blended up to half raw noisy
signal in around speech (on weak signals that is mostly noise), and
VAD_RELEASE 0.02 (~1s+ practical decay) held the relaxed mix through
entire inter-word gaps - producing the halo and a slow pump.

v2: VAD_RELAX 0.5 -> 0.25 (shallower dry blend, halo ~halved) and
VAD_RELEASE 0.02 -> 0.065 (~150 ms time constant; suppression recovers
inside a word gap instead of after it). Attack unchanged.

Synthetic verification: residual noise just after speech <2x settled
floor and returns to settled level within ~200 ms (was >1 s).
Constants tuning by proxy has hit its limit; the relax depth is now a
UI-tunable field so it can be swept live on real weak signals.

- rnn_relax global (0-50 percent, default 25) replaces the fixed
  VAD_RELAX constant; clamped in the mix loop.
- AINRV number field (0-50, step 5) appears at (290,5) beside AINRS
  while AINR is ON, hidden otherwise; read periodically in
  check_plugin_controls like AINRS. Console: \ainrv <0-50>.
- AINRV 0 disables adaptation entirely: with AINRS 100 that reproduces
  the original full-gate behavior from the first well-received test.
- Documented in help_commands.txt.

Once a preferred value is found on-air, it can be hardcoded as the
default (keeping or removing the control as desired).
On-air observation: AINR performed noticeably better at high IF values.
Cause: the wrapper's fixed /16384 scaling. At low IF gain the samples
reaching RNNoise are far below the amplitudes the model was trained on
(speech at int16-style levels), starving its band-energy and pitch
features and degrading its gain/VAD decisions.

Fix: track the running RMS of the input (~100 ms smoothing), apply
makeup gain toward a 3000-RMS target (clamped 0.25x-64x) before the
network, and divide the identical per-sample gain back out after the
wet/dry mix - output volume is untouched, only what the network sees is
leveled. Gain stored in a ring aligned with wet/dry/vad; state reset in
rnn_reset().

Verified: at 100x lower input level, noise suppression now matches or
exceeds the high-level case (3.0x vs 1.8x; previously degraded), with
tone passthrough exactly 1.00 at both levels.

Recommended operating point after this change: IF back at 20-25 for
front-end headroom; AINR quality is now consistent across IF.
@drexjj

drexjj commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Hello. Please rebase and submit to the dev branch. Also, please join us on Discord so we can communicate how changes are approved, tested, etc. Thanks

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.

2 participants