Skip to content

Add Kiwi receive sync#3872

Merged
ten9876 merged 3 commits into
mainfrom
codex/receive-sync-kiwi-audio-display
Jun 28, 2026
Merged

Add Kiwi receive sync#3872
ten9876 merged 3 commits into
mainfrom
codex/receive-sync-kiwi-audio-display

Conversation

@rfoust

@rfoust rfoust commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds Receive Sync support for aligning Flex and Kiwi receive audio with the matching visual presentation path. Auto Assist estimates the Flex/Kiwi offset from the received audio itself using local DSP time-delay estimation: Generalized Cross-Correlation with Phase Transform (GCC-PHAT) plus an envelope/normalized-correlation fallback. It applies bounded per-source presentation delays only to the matched audio-enabled Flex/Kiwi pair on the same frequency, leaves unmatched Kiwi audio sources out of the sync path, preserves held/coasting locks through temporary ambiguity, and avoids audio or waterfall interruptions while tuning or changing receive sources.

The change also adds source-specific visual queues for waterfall/FFT/meter timing, a minimal spectrum overlay status, the Sync Kiwi Audio & Display label with Auto Assist enabled by default for fresh settings, nested receive-sync settings persistence, focused regression coverage, and automation diagnostics for receive-sync troubleshooting. Automation updates include get sync bridge snapshots, bounded PCM audioCapture sampling at raw/post-DSP/output/final audio points, slice rxsource support for user-defined Kiwi receiver/profile names, tools/automation_probe.py updates, and automation bridge documentation.

Constitution principle honored

Principle IV — KiwiSDR integration remains clean-room: this implementation uses observed protocol/app behavior and local audio correlation, not copied Kiwi server code or proprietary reverse-engineered sources.

Principle V — Receive Sync preferences are stored under one AppSettings["ReceivePresentationSync"] JSON object, with migration from temporary local flat keys used during development.

Test plan

  • Local build passes (cmake --build build --target AetherSDR receive_presentation_sync_test)
  • Behavior verified on a real radio if applicable
  • Existing focused tests pass (ctest --test-dir build -R '^(receive_presentation_sync_test|kiwi_sdr_protocol_test|antenna_alias_test)$' --output-on-failure)
  • Reproduction steps documented if user-reported bug

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — use nested-JSON-under-one-key (Principle V)
  • Code is clean-room — not decompiled, disassembled, or reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention)
  • Documentation updated if user-visible behavior changed
  • Security-sensitive changes reference a GHSA if applicable

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 1 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 14
Files with findings: 1
Total findings : 1

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@rfoust

rfoust commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the accessibility warning from the earlier bot comment in commit 19499bc. RxApplet::updateFreqLabel() now emits accessibility value-change updates, debouncing normal frequency announcements and announcing LOCKED immediately.\n\nValidated locally with:\n- cmake --build build --target AetherSDR\n- python3 tools/check_a11y.py src/gui/RxApplet.cpp src/gui/RxApplet.h\n- ctest --test-dir build -R '^(receive_presentation_sync_test|kiwi_sdr_protocol_test|antenna_alias_test)$' --output-on-failure\n\nThe rerun Qt Accessibility Static Analysis check on the new head commit passed with zero annotations.

@rfoust rfoust marked this pull request as ready for review June 28, 2026 03:20
@rfoust rfoust requested review from a team as code owners June 28, 2026 03:20
Copilot AI review requested due to automatic review settings June 28, 2026 03:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@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.

Thanks for this, @rfoust — this is a substantial, carefully-built feature. The clean-room GCC-PHAT + envelope-fallback estimator is well-structured, the bounded automation PCM capture is nicely guarded (frame-aligned, byte/duration-capped, double-checked under lock), settings follow Principle V (one nested ReceivePresentationSync JSON key with flat-key legacy migration), the diagnostics caches are properly std::atomic, and the focused tests + green CI (incl. the a11y rerun) are all there. Nice work.

One concern worth a look before merge:

Possible data race in AudioEngine::setReceivePresentationDelays (missing m_dspMutex)

src/core/AudioEngine.cpp:800 — this method mutates the shared RX buffers directly: it calls trimReceivePresentationBuffers(...) on m_rxBuffer/m_rxPackets/m_rxOutputBuffer, the legacy Kiwi buffers, and each m_externalKiwiSources entry's rxBuffer/rxPackets/outputBuffer, and also iterates the m_externalKiwiSources vector — all without acquiring m_dspMutex.

Those same buffers are appended to under m_dspMutex from the audio/network path (feedAudioData, feedKiwiSdrAudioData, processMixedRxAudioData), and setReceivePresentationDelays is driven from the GUI thread via MainWindow::syncReceivePresentationDelaysToAudioEngine. A GUI-thread QByteArray::remove/std::deque mutation racing with a network-thread append is UB (heap corruption / crash).

The asymmetry is the tell: the two sibling methods — resetReceivePresentationAudioBuffers() (:887) and resetReceivePresentationAudioBuffersForKiwiSource() — both take std::lock_guard<std::recursive_mutex> dspLock(m_dspMutex); before touching the very same buffers. setReceivePresentationDelays looks like it just missed it.

Suggested fix: wrap the buffer-trimming/source-iteration body in the same std::lock_guard<std::recursive_mutex> dspLock(m_dspMutex); (the atomic exchanges for the delay values can stay outside if you prefer, but the m_externalKiwiSources iteration and all trimReceivePresentationBuffers calls need to be inside it). It's recursive_mutex, so the nested updateRxBufferStats() call is fine.

Minor / non-blocking

  • The Auto-Assist estimate (m_receiveAudioDelayEstimator.estimate) runs synchronously on the post-DSP-audio signal handler (GUI thread). It's gated by kReceiveSyncEstimateIntervalMs and a min-sample check, so this is fine — just flagging that the FFT cost lands on the GUI thread if the window/interval are ever tuned down.
  • tests/antenna_alias_test.cpp rx_ant_list coverage is slightly tangential to "Kiwi receive sync," but it's test-only and pairs with the slice rxant/rxsource automation additions, so no objection.

Everything else looks clean. Happy to re-look once the locking question is addressed.


🤖 aethersdr-agent · cost: $6.0013 · model: claude-opus-4-8

@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.

Thanks @rfoust — this is an impressively thorough, well-structured contribution. I reviewed the DSP core, settings persistence, automation surface, and the GUI integration. CI is fully green (build/macos/windows/CodeQL/analyze all pass). Summary: looks good to merge after considering a couple of minor points below.

What's solid

  • Conventions honored. Receive-sync prefs are stored as one nested-JSON object under AppSettings["ReceivePresentationSync"] with clean migration from the legacy flat keys (receivePresentationSettingsToJson / …FromJson / removeLegacyReceivePresentationSettings) — exactly Principle V. No new flat-key calls, no QSettings.
  • DSP core is clean and self-contained. Standard iterative radix-2 FFT, GCC-PHAT with a normalized-correlation refine plus an envelope-feature fallback, bounded offsets, and kEpsilon-guarded denominators throughout. ReceivePresentationSync.cpp/.h link only against Qt6::Core, and receive_presentation_sync_test.cpp gives good focused coverage of the pure logic (delay breakdown, manual/auto offsets, surface gating).
  • Defensive integration. resolveReceiveSyncTarget() and friends null-check m_kiwiSdrManager, every SliceModel*, and sliceMayBelongToUs() before use. Ambiguity (multiple audible Flex/Kiwi) is explicitly modeled rather than guessed.
  • Audio-capture path is careful. captureAutomationAudio() does an atomic relaxed fast-path check before taking the mutex (keeps the audio callback cheap), then bounds by both duration and a 64 MB byte cap. doAudioCapture null-checks m_audioEngine and handles file-open/mkpath/short-write failures at the FS boundary. Nicely done.

Minor points (non-blocking)

  1. Estimator cost on the GUI thread. runReceivePresentationAutoAssist() runs on the GUI thread (fed via the queued receivePresentationOutputAudioReady signal). With a 5 s window at 12 kHz, nextPowerOfTwo(60000+60000) = 131072, so each pass does three 131072-point double FFTs plus a ~72k-sample offset scan. It's throttled to 1 Hz, so likely only a few ms, but on slower hardware it could show up as periodic UI jitter. Worth confirming it stays smooth in practice; if it ever bites, moving the estimate() call to a worker (QtConcurrent) would be the natural fix. Not a merge blocker.

  2. audioCapture "AETHER_AUTOMATION-only" claim. The docs state capture is active only in an AETHER_AUTOMATION=1 process. That holds today because AutomationServer is the sole caller and is itself gated — but startAutomationAudioCapture() has no internal guard, so the invariant is enforced one layer up. Fine as-is; just flagging in case the engine API ever gets another caller.

Constitution principles IV (clean-room) and V (settings) both look respected. Nice work — no changes required from my side.


🤖 aethersdr-agent · cost: $4.2390 · model: claude-opus-4-8

@rfoust

rfoust commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the latest receive-sync review feedback in 8bf96fc:

  • Wrapped AudioEngine::setReceivePresentationDelays() buffer/source mutation in m_dspMutex, matching the reset paths and avoiding GUI/audio-thread races while trimming receive-presentation queues.
  • Moved the Auto Assist delay estimator work off the GUI thread. runReceivePresentationAutoAssist() now snapshots the analysis audio/config and runs ReceiveAudioDelayEstimator::estimate() via QtConcurrent; completed results are applied back on the GUI thread and stale worker results are discarded on reset/source/frequency changes.
  • Added an internal AETHER_AUTOMATION guard to AudioEngine::startAutomationAudioCapture(), so the engine API now enforces the same automation-only invariant the docs describe.

Validated with:

  • cmake --build build --target AetherSDR receive_presentation_sync_test
  • ctest --test-dir build -R '^(receive_presentation_sync_test|kiwi_sdr_protocol_test|antenna_alias_test)$' --output-on-failure
  • git diff --check

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. Reviewed the DSP core, threading, settings, automation surface, and GUI integration on the current head (8bf96fc):

  • Both prior review concerns are resolved in 8bf96fc:
    • The GUI/audio-thread data race in setReceivePresentationDelays is fixed — m_dspMutex now guards the buffer trims + m_externalKiwiSources iteration, matching the sibling reset methods.
    • The GCC-PHAT estimate() now runs on QtConcurrent's thread pool (not the GUI thread), guarded by an in-flight flag, a generation counter for stale-result discard, a 1 Hz throttle, and input-vector copies — no contention with the real-time audio path. QtConcurrent is the right home (the estimate spans both Flex+Kiwi audio; the Kiwi/AudioEngine workers must stay real-time).
  • Clean-room (Principle IV) — standard radix-2 FFT + GCC-PHAT + normalized-correlation refine + envelope fallback, kEpsilon-guarded; ReceivePresentationSync.{h,cpp} link only Qt6::Core, with a 425-line focused unit test.
  • Principle V — one nested AppSettings["ReceivePresentationSync"] JSON key with flat-key legacy migration.
  • No TX/keying path (RX audio+visual sync; Principle VI N/A). Defensive null-checks throughout; ambiguity (multiple audible Flex/Kiwi) explicitly modeled with held/coasting locks.
  • audioCapture bounded (duration + 64 MB cap), atomic fast-path before the mutex, FS-boundary error handling, reachable only via the AETHER_AUTOMATION-gated bridge.

CI green on all 6. Excellent, carefully-built feature — thanks @rfoust.

@ten9876 ten9876 merged commit be15fb1 into main Jun 28, 2026
6 checks passed
@ten9876 ten9876 deleted the codex/receive-sync-kiwi-audio-display branch June 28, 2026 14:52
ten9876 added a commit that referenced this pull request Jun 28, 2026
## Release prep for v26.6.5

Docs + version-string updates for the **50 merges** since v26.6.4. No
code change — safe to merge without a rebuild.

### Changes
- **CHANGELOG.md** — new `[v26.6.5] — 2026-06-28` section, headlined by
**KiwiSDR receive sync** (#3872), **SmartMTR TX meters** (#3776), the
**PROF profile-switcher applet** (#3829), and a large **agent automation
bridge** expansion (#3851/#3856/#3883/#3842/#3832/#3819); categorized
Added/Changed/Fixed/Performance/Removed compiled from the full merge
list. Folds in the prior `[Unreleased]` entries and resets
`[Unreleased]` to empty.
- **CMakeLists.txt / README.md / AGENTS.md** — `26.6.4` → `26.6.5`.
- **ROADMAP.md** — current cycle → `post-v26.6.5`; four v26.6.5
highlights added atop *Recently shipped*.

### Not included
- Tagging `v26.6.5` is a separate, explicit step (not done here).
- The untracked `docs/aetherd-headless-engine-design.md` RFC and
`prototypes/` are intentionally left out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <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.

3 participants