[wave] Restore WAVE TX scope for digital audio#2688
Conversation
Co-authored-by: Codex <noreply@openai.com>
There was a problem hiding this comment.
Thanks @jensenpat — clean fix. The new emitTxPostChainScopeFromFloat32Stereo() mirrors the existing emitRxPostChainScopeFromFloat32Stereo() exactly (same throttle/scratch fields shared with the int16 voice path, same NaN guard, same clamp), and the two call sites are correctly gated:
sendModemTxAudio(RADE) — wrapped in the existingm_radioTransmittingcheck.feedDaxTxAudio(DAX/TCI) — wrapped indaxAudioWillTransmit, which already excludes the "DAX suppressed by mic voice TX" case, so the WAVE display won't light up on muted DAX bursts.
Sharing m_lastTxPostChainScopeEmit and m_scopeTxPostChainScratch with the int16 voice variant is fine since voice / DAX / RADE TX paths don't run concurrently on the same engine.
The "Enable PC Audio" RX empty-state text change is in scope (called out in the summary) and the source == "RX" comparison matches the exact literals used by the only two callers (StripWaveform.cpp:185 and WaveformWidget.cpp:179).
Doc updates to audio-pipeline.md correctly reflect that txPostChainScopeReady is now a shared voice + digital tap.
No blocking concerns from me — LGTM.
…shared widget Replace the per-repaint full-window rescan (three O(window) passes: ring copy, stats, per-pixel columns) with WaveformScopeModel — samples fold into ~2048 fixed-duration bins as they arrive, and repaints merge bins into pixel columns in O(bins + width). Per-frame cost no longer scales with the time window; fps is now the only cost knob. Paint-path cuts (visually identical): grid + dB labels cached in a QPixmap keyed on size/dpr/zoom/theme/font (kills per-frame line drawing and HarfBuzz text shaping, a top cost in the aethersdr#3283 profile); one drawLines() batch instead of ~columnCount drawLine() calls; drawPolyline() over reused buffers instead of four per-frame QPainterPaths; data repaints invalidate only the plot area with the RMS/PK readout refreshed at ~fps/5 (steadier to read, no per-frame text shaping). Bands mode now copies only its <=1536-sample analysis tail instead of the whole window. Fold the StripWaveform fork (~900 duplicated lines) into WaveformWidget(Profile::Strip): the profile carries the fork's real deltas (30 s window ceiling, antialiased stroke); ring policy and the 120 Hz repaint ceiling are shared. Pausing now snapshots the model, so changing the window while paused re-windows the frozen audio instead of pulling live samples. Applet FPS slider is 5-60 (default 60, was 5-30/24). Measured on a live FLEX-8400M session: 60 fps costs ~209 ms/s of main-thread paint vs ~97 ms/s at 24 fps (+15% process CPU at defaults) — the remaining per-frame cost is Qt's antialiased polyline stroking, which is the Phase-2 GPU target. Phase-2 hooks: ColumnStats is a contiguous POD array uploadable as a 1-D texture, and generation() is the upload dirty flag. TX/RX safety (aethersdr#2688): no AudioEngine changes; appendScopeSamples() contract unchanged. Live proof on ANT2 dummy load: scope flips to TX on PTT, back to RX on unkey. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stats/scrollTo bridge verbs (Phases 1+2) (#3955) ## Summary Phase 1 of the WAVE waveform meter performance plan — CPU-side fixes and proof tooling. Phase 2 (QRhi GPU render following SpectrumWidget's `AETHER_GPU_SPECTRUM` pattern) comes separately; its hooks are in place here. - **`WaveformScopeModel`** (new): incremental sample→bin reduction. Samples fold into ~2048 fixed-duration bins per window as they arrive (O(new samples)); repaints merge bins → pixel columns (O(bins + width)). Replaces three full O(window) passes per paint (ring copy, stats pass, per-pixel column build), so per-frame cost no longer scales with the time window — a 20 s strip window now costs the same per frame as 240 ms. - **StripWaveform fork folded in**: `WaveformWidget(Profile::Strip)` carries the fork's real deltas (30 s window ceiling, antialiased stroke); ~900 duplicated lines deleted. - **Paint-path cuts (visually identical)**: grid + dB labels cached in a QPixmap (kills per-frame grid drawing and HarfBuzz text shaping — a top cost in the #3283 trace); one `drawLines()` batch instead of ~205 `drawLine()` calls; `drawPolyline()` over reused buffers instead of four per-frame `QPainterPath`s; data repaints invalidate only the plot area with the RMS/PK readout refreshing at ~fps/5 (steadier to read, too). - **Bands mode** reads only its ≤1536-sample analysis tail instead of copying the whole window first. - **Pause** snapshots the model — changing the window while paused re-windows the frozen audio instead of silently pulling live samples. - **Applet FPS slider 5–60, default 60** (was 5–30, default 24) — measured cost below; flip `kDefaultFps` in WaveApplet.cpp if the tradeoff reads wrong. Users who previously moved the slider keep their saved value. - **`get wavestats` bridge verb**: per-scope paint/append counters (`paintMsPerSec` = main-thread paint budget consumed) for profiler-free before/after proofs, documented in docs/automation-bridge.md. WAVE drawer controls gained objectNames/accessibleNames (a11y backlog + bridge targeting). ## Measurements Mac Studio (Apple Silicon, Retina 2×), live FLEX-8400M session, RX audio at 48 kHz, display held awake, whole-process CPU via cputime delta over 40 s, BASE (76d8993) and NEW interleaved back-to-back per scenario. Per-widget attribution via `get wavestats` (NEW only). | Scenario (Scope mode, docked unless noted) | BASE | NEW | NEW paint ms/s | |---|---:|---:|---:| | 24 fps, 1 s window | 99.8% | 98.9% | 97 | | 30 fps, 1 s | 96.9% | — | — | | 60 fps, 1 s | n/a (30 cap) | 113.5% | 209 | | 30 fps, 10 s | 97.9% | 97.4% | 115 | | 60 fps, 10 s | n/a | 105.3% | 193 | | Floated window, 30 fps, 10 s | 89.9% | 85.3% | 164 | | WAVE hidden (control) | 76.7% | 72.1% | 0 | Honest reading: - **At identical docked configs the two builds are at parity** — the WAVE meter's cost is dominated by Qt's *antialiased polyline stroking* (~4.5 ms/frame for Scope/Envelope at applet size; History/Bands are ~0.6–0.8 ms). That is the piece only Phase 2's GPU path removes. The #3283 profile said exactly this ("~92% of CPU is QPainter software rasterization"). - **What Phase 1 buys**: the window-scaling term is gone (NEW per-frame cost is *flat* from 1 s to 10 s — fps is now the only knob), which is what makes long windows, 192 kHz rates, and the strip's 20–30 s envelopes safe; plus the consolidation, the pause fix, the Bands tail fix, and the measurement verb that makes Phase 2's win provable. - **24 vs 60 fps** (the requested numbers): 97 → 209 ms/s of main-thread paint; +14.6 CPU points whole-process at the 1 s default window. 60 fps ships as the default per the project decision — it's one constant to revert if the interim cost reads wrong before Phase 2 lands. - Strip finding: the strip's waveform panel receives **no paint events at all** while scrolled out of its scroll-area viewport (both builds), so its idle cost is ~0 and its in-view cost is this same shared code path. (Follow-up: the bridge needs a scroll/ensureVisible verb to measure it live.) ## TX/RX safety (#2688 regression guard) No AudioEngine changes; `appendScopeSamples(mono, sr, tx)` contract unchanged, so the digital-path (DAX/modem) TX scope emits restored in #2688 are untouched. Live bridge proof on the FLEX-8400M into the ANT2 dummy load (LSB, silent mic): PTT on → scope header flips to TX at the 24 kHz post-chain rate and keeps painting; PTT off → RX resumes at 48 kHz immediately. Pause/resume click, all four view modes, and the popped-out floating window were visually verified via bridge grabs. ## Phase 2 hooks - `WaveformScopeModel::mergeColumns()` fills a contiguous POD `ColumnStats` array — directly uploadable as a 1-D texture by a QRhi render path. - `generation()` is the dirty counter for upload skipping. - Render functions consume pre-merged columns, so a QRhiWidget subclass swaps the raster backend without touching the reduction. ## Related issues - #3283 — QPainter software rasterization dominates CPU (WAVE share: structural fix here, stroking cost remains for Phase 2). - #3938 — detached Waveform → choppy panadapters: floated WAVE process CPU is ~4.6 points lower with this change, but the reported issue is present-cadence contention from a second top-level window (FPS-independent per the reporter), so this PR does not claim to fix it. 💻 Generated with Claude Code (claude-fable-5.0 7/1/26) with architecture by @jensenpat --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Summary
Enable PC Audio.Root Cause
Recent WAVE wiring moved the display to
txPostChainScopeReady, but DAX/TCI transmit audio intentionally bypasses the voice chain and continued to emit only the older sharedscopeSamplesReadysignal. That left digital-mode TX audio packetizing normally while the WAVE display had no high-rate TX samples to render.Validation
cmake --build build -j22ctest --test-dir build --output-on-failure -j22(7/7 passed)👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat