Add network audio packet-loss concealment (#2731). Principle I.#2732
Merged
Conversation
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.
Closed
2 tasks
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>
This was referenced May 16, 2026
…#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>
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>
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 #2731
What was changed
Add network audio packet-loss concealment (#2731). Principle I.
Files modified
src/core/OpusCodec.cppsrc/core/OpusCodec.hsrc/core/PanadapterStream.cppsrc/core/PanadapterStream.hsrc/gui/RadioSetupDialog.cppGenerated by AetherClaude (automated agent for AetherSDR)