Skip to content

Fix audio pan broken with NR active (#1460) and AF mute not persisted (#1560)#1685

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
M7HNF-Ian:fix/1460-pan-1560-mute-clean
Apr 20, 2026
Merged

Fix audio pan broken with NR active (#1460) and AF mute not persisted (#1560)#1685
ten9876 merged 3 commits into
aethersdr:mainfrom
M7HNF-Ian:fix/1460-pan-1560-mute-clean

Conversation

@M7HNF-Ian

Copy link
Copy Markdown
Contributor

Summary

  • Audio panning stops working when NR2 or NR4 noise reduction is active #1460 — Pan slider does nothing when any client-side NR (NR2, RN2, NR4, DFNR) is active. All NR filters collapse stereo PCM to mono for noise analysis, discarding the radio-embedded stereo balance. applyRxPanInPlace() re-applies a linear pan law to the stereo NR output so the slider works correctly regardless of which NR is active. At pan=50 the function returns immediately — no change to existing behaviour when centred.
  • Upon start of aethersdr the previous audio mute state is not remembered. #1560 — The per-slice AF mute (audio_mute) is not persisted by the radio between client sessions. On reconnect every slice starts unmuted. VfoWidget now emits audioMuteToggled(bool) (guarded by m_updatingFromModel) and wireVfoWidget saves SliceAudioMuted_A/B/C/D to AppSettings, then restores the saved state on each connection.

Scope

This branch is a clean cherry-pick from upstream/main — contains only the pan and mute fixes, no MNR code. (Previous PR #1676 was closed because it was branched off feat/mnr-and-quickfixes and the diff included unrelated MNR work.)

Review notes

  • applyRxPanInPlace is static (file-local), called only from feedAudioData and processNr2. The MNR path (#ifdef __APPLE__) is intentionally absent here — it belongs in the MNR PR (Add macOS MMSE-Wiener spectral NR (MNR) + 5 dark-theme quick-wins #1672) where the caller exists.
  • Settings key uses the user-visible slice letter (QChar('A' + sliceId)) not the numeric ID, so saved state survives sessions where the radio assigns different numeric IDs.
  • nFrames <= 0 guard in applyRxPanInPlace ensures an empty/partial buffer on an error path never enters the loop.

Test plan

  • Enable NR2 on a slice, set pan left/right — audio should pan correctly
  • Enable RN2 on a slice, verify pan works
  • Verify no change to pan behaviour when no NR is active (radio handles it)
  • Mute a slice via the VFO 🔊 button, disconnect and reconnect — slice should come back muted
  • Mute Slice A, then Slice B — each should restore its own mute state independently
  • Un-mute a previously-muted slice, disconnect/reconnect — slice should come back unmuted

🤖 Generated with Claude Code

@M7HNF-Ian M7HNF-Ian requested a review from ten9876 as a code owner April 19, 2026 14:20

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR #1685

Thanks for the clean re-submission @Chaosuk97 — separating this from the MNR branch makes it much easier to review. Both fixes are well-scoped and the PR description is excellent.

Pan fix (#1460) — looks good

applyRxPanInPlace is clean: file-static, early-out at centre, safe nFrames <= 0 guard. The linear pan law is simple and correct, and calling it at the right point in each NR branch (after the mono→stereo expand for NR2, after process() for RN2/NR4/DFNR) is the right approach.

One thing worth noting for the maintainer: AudioEngine has a single m_rxPan atomic, but wireVfoWidget connects every slice's rxPanChanged to it. In a multi-slice scenario, the last slider moved wins for all NR-processed audio. This is probably fine in practice since only the active slice's audio routes through the single AudioEngine's NR path, but it's worth being aware of if multi-slice NR is ever supported.

Mute persistence (#1560) — looks good

Using the slice letter (QChar('A' + sliceId)) as the settings key is a good choice for surviving re-assigned numeric IDs across sessions. The m_updatingFromModel guard on audioMuteToggled correctly prevents save-on-restore feedback loops.

Minor question: the restore path calls s->setAudioMute(true) which sends audio_mute=1 to the radio. If this runs before the radio connection is fully established (i.e., before the slice is "ready" on the radio side), does the command get queued or silently dropped? If the radio might not be ready yet, the timing could be fragile. The PR author notes "on reconnect" in the description, so this likely works in practice — just flagging for the maintainer to verify during testing.

Convention checks

  • AppSettings used correctly (not raw QSettings) ✓
  • No resource leaks — no heap allocations, no manual memory management ✓
  • All files within stated scope — AudioEngine, VfoWidget, MainWindow only ✓
  • m_rxPan is std::atomic<int> — safe for cross-thread reads from the audio thread ✓

Summary

Clean, well-documented, tightly scoped. No blockers from my side. The test plan covers the key scenarios. Nice contribution. 👍

ten9876 and others added 3 commits April 20, 2026 15:53
Credits NR2 gainMax cap + output clamp fix (aethersdr#1696, issue aethersdr#1507).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed (aethersdr#1560)

    discarding the radio-side stereo balance. The pan slider sends
    audio_pan to the radio which embeds the balance in the VITA-49 PCM,
    but NR collapses it.

    Fix:
    - AudioEngine gains std::atomic<int> m_rxPan (0-100, centre=50)
      and void setRxPan(int).
    - Static helper applyRxPanInPlace() re-applies a linear-pan law to
      stereo float32 in-place; is a true no-op when pan==50.
    - Called at the end of processNr2() and after every other NR
      process() call in feedAudioData (RN2/NR4/DFNR/MNR paths).
      The non-NR path is untouched — the radio still owns pan there.
    - VfoWidget emits rxPanChanged(int) when the user moves the pan
      slider; wireVfoWidget connects it to m_audio->setRxPan().

    radio, but the radio does not persist this between client sessions.
    On reconnect the slice starts unmuted.

    Fix:
    - VfoWidget emits audioMuteToggled(bool) when the user clicks the
      mute button (guarded by m_updatingFromModel so model-driven syncs
      don't trigger a save).
    - wireVfoWidget saves SliceAudioMuted_{id} to AppSettings on every
      user toggle, then immediately restores the saved state by calling
      s->setAudioMute(true) if needed (radio is already connected at
      this point, so the command goes through cleanly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rsdr#1676 review)

- Use QChar('A' + sliceId) as the SliceAudioMuted settings key so the
  persisted mute state survives sessions where the radio assigns different
  numeric slice IDs (matches DaxChannel_SliceA naming convention) (aethersdr#1560)
- Add nFrames <= 0 early-exit and explanatory comment to applyRxPanInPlace
  so zero-length / partial buffers on error paths never enter the loop (aethersdr#1460)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ten9876 ten9876 force-pushed the fix/1460-pan-1560-mute-clean branch from 1493bb1 to fcc5c81 Compare April 20, 2026 23:02
@ten9876

ten9876 commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Claude here on behalf of Jeremy — rebased your branch on current main to resolve the conflict with #1696 (NR2 gain clamp, which just merged). The conflict was in processNr2's comment block; final code keeps both your applyRxPanInPlace call AND the std::clamp from #1696 (clamp first, then pan — pan only attenuates so samples stay in ±1.0). No functional changes to your fix.

Waiting for CI, then merging.

— Claude (AI dev partner)

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