Skip to content

Add network audio packet-loss concealment (#2731). Principle I.#2732

Merged
ten9876 merged 6 commits into
mainfrom
aetherclaude/issue-2731
May 16, 2026
Merged

Add network audio packet-loss concealment (#2731). Principle I.#2732
ten9876 merged 6 commits into
mainfrom
aetherclaude/issue-2731

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #2731

What was changed

Add network audio packet-loss concealment (#2731). Principle I.

Files modified

  • src/core/OpusCodec.cpp
  • src/core/OpusCodec.h
  • src/core/PanadapterStream.cpp
  • src/core/PanadapterStream.h
  • src/gui/RadioSetupDialog.cpp
 src/core/OpusCodec.h          |   6 ++
 src/core/PanadapterStream.cpp | 145 +++++++++++++++++++++++++++++++++++++++---
 src/core/PanadapterStream.h   |  33 +++++++++-
 src/gui/RadioSetupDialog.cpp  |  37 +++++++++++
 5 files changed, 229 insertions(+), 12 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)

When a VITA-49 audio packet is dropped, PanadapterStream previously
detected the sequence skip but handed the next received packet straight
to AudioEngine. The two waveform endpoints don't line up at zero gap,
producing the broadband click the reporter described — original SmartSDR
exhibits the same artifact.

Concealment runs in three flavors, gated on a new "Smooth packet loss"
toggle in Radio Settings → Audio (default on, persisted in
AppSettings["AudioPacketLossConcealment"]):

  * PCC_IF_NARROW (LAN default, float32 stereo): prepend a cosine
    fade-down from the cached tail to zero, zero-pad the rest of the
    gap, cosine fade-up into the new packet's head (~2 ms each).
  * PCC_IF_NARROW_REDUCED (int16 mono): same shape after the mono→stereo
    expansion.
  * PCC_OPUS: synthesize one frame per missed packet via the new
    OpusCodec::concealLost(), which calls opus_decode(nullptr, 0, …) —
    libopus's native PLC produces a perceptually smooth fill from
    internal state rather than raw silence.

Gap count is capped at 8 packets (~80 ms) so a long outage drops to
clean silence rather than back-filling forever. DAX and IQ paths are
left untouched (downstream apps do their own buffering, and inserting
silence there could confuse digital decoders).

Honors FlexLib protocol authority (Principle I): VITA-49 packet
sequence semantics (4-bit modular count in word-0 bits 19:16) match
FlexLib's VitaParser; Opus native PLC matches the radio's own
recovery path. No protocol additions, no firmware dependency.

Blast radius: risk_score=0.025 (processDatagram), 0 high-risk callers
on the audio dispatch. Header edit flags MainWindow/RadioModel as
transitive includers, but those callers' consumed surface (existing
public methods, signals) is unchanged — only new symbols added.
The original comment described accepting a "rare race on toggle" when
clearing m_audioPlc, but the only caller (RadioSetupDialog audio tab
checkbox) routes the call through QMetaObject::invokeMethod with
Qt::QueuedConnection — so the clear() always runs on the network
worker thread that owns the map.  There's no race in any reachable
code path.

Tighten the comment to describe what's actually happening and remove
the misleading race language so a future reader doesn't trust the
"we accept the race" framing to defend a cross-thread call that the
codebase wouldn't allow anyway.

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

Roll the two follow-up issues from the PR review directly into this PR
since the work is small and the refactor lands cleaner as part of the
original change than as a separate PR.

## #2738 — m_audioPlc cleanup on stream teardown

Per-stream PLC state in `PanadapterStream::m_audioPlc` never had any
removal path — entries accumulated until the process exited.  Practical
impact tiny (≤20 entries × ~32 bytes per session), but a real slow leak
across reconnects.  Two hooks:

- `clearRegisteredStreams()` (the disconnect/reset path) now `clear()`s
  the map alongside the rest of the per-stream tables.
- `unregisterDaxStream()` drops the matching entry, since DAX RX audio
  streams use the PLC path too.

The remote_audio_rx slow-leak case (one stream id per connection) is
covered by the `clearRegisteredStreams` hook on disconnect.

## #2739 — extract PLC math + unit tests

Extracted the pure fade-math into a new translation unit
`src/core/PacketLossConcealment.{h,cpp}` containing:

- `struct AudioPlcState` — was a private nested struct of
  PanadapterStream; now a top-level type in the AetherSDR namespace.
- `constexpr int kMaxConcealPackets` — same, lifted out.
- `QByteArray applyConcealmentFade(QByteArray, AudioPlcState&, bool)`
  — pure free function.  Was a method on PanadapterStream that read
  the m_plcEnabled atomic; `enabled` now passed as a parameter so the
  function is pure and unit-testable without bringing up the network
  worker thread or instantiating the QObject host.

`PanadapterStream.h` now `#include`s the new header and the two
callsites in `decodeNarrowAudio` / `decodeReducedBwAudio` pass
`m_plcEnabled.load()` to the free function.

New `tests/packet_loss_concealment_test.cpp` covers nine cases:

1. No-loss path — input passes through verbatim
2. First-packet path — concealment skipped (no prior tail to fade from)
3. Disabled path — concealment skipped even with pending missed
4. Single-packet loss — output size doubles (concealment + new packet)
5. Fade-down envelope — first 48 frames monotone-decreasing from tail
6. Fade-up envelope — last 48 frames of concealment monotone-increasing
7. Multi-packet loss — fillFrames = pendingMissed × lastFrames
8. Tail update — plc.tailL/R reflect input's last samples after run
9. Empty input — graceful no-op + pendingMissed reset

Test target `packet_loss_concealment_test` only links the new
`PacketLossConcealment.cpp` — no QObject, no socket, no RadioConnection,
just `Qt6::Core` for QByteArray.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested a review from AetherClaude as a code owner May 16, 2026 16:09
ten9876 and others added 3 commits May 16, 2026 09:58
M_PI is a POSIX extension and MSVC only exposes it when
_USE_MATH_DEFINES is set before <cmath>.  The original
PanadapterStream.cpp got it via a transitive Qt include path; the
newly-extracted PacketLossConcealment.cpp doesn't have that path,
so the check-windows CI step failed with C2065 'undeclared
identifier'.

Define a portable kPi constant in an anonymous namespace and use it
in the two cosine-fade callsites.  No behaviour change — kPi is bit-
for-bit identical to the M_PI value Linux/macOS toolchains exposed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ow-up)

PR #2726's dBm-scale tooltip used:

    static const QString tip = QStringLiteral(
        "..."
    #ifdef Q_OS_MAC
        "..."
    #else
        "..."
    #endif
        "...");

Preprocessor directives inside a function-like macro call are undefined
behaviour per the C++ standard.  GCC and Clang accept the construct,
MSVC strictly rejects it:

    SpectrumWidget.cpp(3328): error C2121: '#': invalid character:
    possibly the result of a macro expansion
    SpectrumWidget.cpp(3328): error C2146: syntax error: missing ')'
    before identifier 'ifdef'

#2726's check-windows step was SKIPPED via path filter so the bug
landed on main unnoticed.  PR #2732's broader file touches caused a
non-skipped check-windows run which surfaced it.

Fix: drop the QStringLiteral macro and use plain adjacent-string-
literal concatenation.  Preprocessor directives sit at the
preprocessor level rather than inside a macro argument and resolve
before the C++ parser sees the assignment.  Static const QString
initialised once at first use — runtime UTF-16 conversion has no
measurable cost in this path.

Rolling into the PLC PR rather than a separate fix-on-main since the
PLC PR already touches SpectrumWidget.cpp and the merge would have
hit this on Windows anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit 8e51707 into main May 16, 2026
5 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2731 branch May 16, 2026 17:46
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.

Network packet loss smoother - for better audio in poor network conditions - less crackle.

1 participant