Skip to content

[wave] Restore WAVE TX scope for digital audio#2688

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/waveform-tx-audio
May 15, 2026
Merged

[wave] Restore WAVE TX scope for digital audio#2688
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/waveform-tx-audio

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

  • Restore the high-rate WAVE TX scope feed for digital transmit bypass paths, including DAX/TCI and RADE, so FT8, Winlink, and other external TX audio sources show waveform activity again.
  • Keep SSB phone waveform monitoring on the existing post-final-limiter voice tap.
  • Change the RX waveform empty-state text from the generic no-audio message to Enable PC Audio.
  • Update the audio pipeline documentation to describe the shared high-rate TX scope behavior for voice and digital paths.

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 shared scopeSamplesReady signal. That left digital-mode TX audio packetizing normally while the WAVE display had no high-rate TX samples to render.

Validation

  • cmake --build build -j22
  • ctest --test-dir build --output-on-failure -j22 (7/7 passed)

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat

Co-authored-by: Codex <noreply@openai.com>
@jensenpat jensenpat changed the title Restore WAVE TX scope for digital audio [wave] Restore WAVE TX scope for digital audio May 15, 2026
@jensenpat jensenpat marked this pull request as ready for review May 15, 2026 15:18

@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 @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 existing m_radioTransmitting check.
  • feedDaxTxAudio (DAX/TCI) — wrapped in daxAudioWillTransmit, 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.

@ten9876 ten9876 merged commit 704582d into aethersdr:main May 15, 2026
5 checks passed
ten9876 pushed a commit to jensenpat/AetherSDR that referenced this pull request Jul 4, 2026
…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>
ten9876 added a commit that referenced this pull request Jul 4, 2026
…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>
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