Skip to content

fix(audio): persist BYPASS state via active flag (#2892)#2978

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
Ozy311:fix/2892-bypass-state-flag
May 23, 2026
Merged

fix(audio): persist BYPASS state via active flag (#2892)#2978
ten9876 merged 1 commit into
aethersdr:mainfrom
Ozy311:fix/2892-bypass-state-flag

Conversation

@Ozy311

@Ozy311 Ozy311 commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

AudioEngine::isTxBypassed() and isRxBypassed() returned
!m_txBypassSnapshot.isEmpty(), conflating "no bypass engaged" with "bypass
engaged while every chain stage was already disabled." When BYPASS is clicked
in the latter state, the snapshot stays empty, so the getter returns false
moments after setTxBypassed(true) was called.

AetherialAudioStrip.cpp:257-258 re-reads isTxBypassed() / isRxBypassed()
on every TX/RX side-toggle to resync the BYPASS button's checked state. The
stale false unchecks the button and the engine's view of bypass silently
desynchronises from the GUI.

Why

This is exactly the failure reported in #2892 by @TnxQSO-Admin: BYPASS engages,
user switches the strip TX→RX→TX (or the inverse), and the BYPASS button
spontaneously unchecks. The reporter's root-cause analysis pinpointed the
empty-snapshot edge case and recommended dedicated bool members as the source
of truth — this PR implements that recommendation.

The fix also covers the restart variant the issue body mentions: when a prior
session ended with bypass engaged, all stages were persisted as disabled. The
next BYPASS click in a fresh session hits the same empty-snapshot case; with
the flag, the button now stays checked.

Scope

  • src/core/AudioEngine.h — declare bool m_txBypassActive{false} and
    bool m_rxBypassActive{false} alongside the existing snapshot vectors;
    comments updated to clarify the new division of responsibility.
  • src/core/AudioEngine.cpp — getters return the flag; setters assign the
    flag before emit. The snapshot retains its restore role unchanged — it
    still records which stages were enabled at the moment of bypass so we can
    re-enable exactly that set on release.

+14 / -8 lines. No protocol, persistence, or UX change; no signal-emission
change beyond timing the flag write before the emit.

Test plan

  • Local incremental ninja -C build clean on Linux (Nobara 43, Qt 6.10.3, gcc 15.2.1)
  • Bug reproduced on baseline upstream/main (b9df0b3) against a FLEX-6700 — TX side, all stages off, click BYPASS, switch RX, switch back to TX → BYPASS unchecks itself
  • Fix verified against the same FLEX-6700, same repro steps — BYPASS stays checked across the TX/RX/TX cycle
  • Mac smoke — open to running this on macOS if requested

Closes #2892


73, Ozy K6OZY
AI compute partnership: cloaked.agency — drafted with Don, our VP of Engineering agent, on Linux dev stack (nobara-dell).

isTxBypassed() / isRxBypassed() returned !m_txBypassSnapshot.isEmpty(),
which collapses two distinct states into one whenever BYPASS is engaged
with no chain stages enabled: the snapshot stays empty even though
setTxBypassed(true) was just called, so subsequent reads return false.
AetherialAudioStrip.cpp:257-258 reads isTxBypassed()/isRxBypassed() on
every TX/RX side-toggle to re-sync the BYPASS button checked state -
the false return unchecks the button and the engine's idea of bypass
silently desynchronises from the GUI.

Add bool m_txBypassActive / m_rxBypassActive members and treat them as
the source of truth for currently-bypassed. The snapshot retains its
restore role unchanged: it still records which stages were enabled at
the moment of bypass so we can re-enable exactly that set on release.

Reproduces deterministically on Linux per the issue body; verified on
nobara-dell against a FLEX-6700 in both directions (baseline binary
unchecks BYPASS on TX/RX/TX cycle; fix binary keeps it checked).

Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
@Ozy311 Ozy311 marked this pull request as ready for review May 23, 2026 01:01
@Ozy311 Ozy311 requested review from jensenpat and ten9876 as code owners May 23, 2026 01:01

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

Reviewed the diff against AudioEngine.{cpp,h} and confirmed the root cause analysis. The empty-snapshot edge case is real: when BYPASS is clicked with all chain stages already disabled, m_txBypassSnapshot stays empty and the old isTxBypassed() returns false, which then unchecks the strip's BYPASS button on the next TX/RX side-toggle (AetherialAudioStrip.cpp:257-258 re-reads the getter). Adding m_txBypassActive/m_rxBypassActive as the source of truth — with the snapshot vector retained purely for its restore role — is the right factoring.

A few things I verified:

  • The flag is assigned before emit txBypassChanged(on) in both setters, so any slot that calls back into isTxBypassed() (e.g. ClientChainApplet.cpp:404) sees the new state. Correct ordering.
  • All external readers in AetherialAudioStrip.cpp and ClientChainApplet.cpp go through the getter, not the snapshot directly — no stragglers using !snapshot.isEmpty() semantics.
  • The restart-variant case the PR body mentions is covered: on a fresh session both flags default to false, so the first BYPASS click takes the on != isTxBypassed() branch and sets the flag even when the snapshot ends up empty.
  • The early-return guard if (on == isTxBypassed()) return; correctly routes through the new flag.
  • +14 / -8 matches the stated scope; nothing leaks outside AudioEngine.

The header comments are also updated to call out that the snapshot is now restore-only and the flag is the source of truth, which will save the next reader the same chase you did.

Looks good. Thanks for the careful root-cause write-up @Ozy311 — the diagnostic detail in the PR body (including the flush() parallel from #2968 you didn't even need here) makes this very easy to review. 73 de AetherClaude.


🤖 aethersdr-agent · cost: $10.4173 · model: claude-opus-4-7

@ten9876 ten9876 merged commit fd7f335 into aethersdr:main May 23, 2026
5 checks passed
@ten9876

ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Merged — thanks @Ozy311! The flag-before-emit ordering matters for the slot graph (anyone re-reading isTxBypassed() from a txBypassChanged slot sees the new state immediately), and the header-comment update calling out the snapshot's now-restore-only role will save the next reader the same chase you and @TnxQSO-Admin did.

Also nice attribution to @TnxQSO-Admin's root-cause writeup in the issue body — the suggested-fix-by-reporter → implemented-by-fellow-community-contributor handoff is the exact pattern we love seeing.

Shipping in the next release.

73,
Jeremy KK7GWY & Claude (AI dev partner)

@TnxQSO-Admin

Copy link
Copy Markdown
Contributor

As it seems, as implemented the bypass button now affects all channel strip presets instead of the active.

@Ozy311 Ozy311 deleted the fix/2892-bypass-state-flag branch June 13, 2026 23:27
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…thersdr#2978)

## Summary

`AudioEngine::isTxBypassed()` and `isRxBypassed()` returned
`!m_txBypassSnapshot.isEmpty()`, conflating "no bypass engaged" with
"bypass
engaged while every chain stage was already disabled." When BYPASS is
clicked
in the latter state, the snapshot stays empty, so the getter returns
`false`
moments after `setTxBypassed(true)` was called.

`AetherialAudioStrip.cpp:257-258` re-reads `isTxBypassed()` /
`isRxBypassed()`
on every TX/RX side-toggle to resync the BYPASS button's checked state.
The
stale `false` unchecks the button and the engine's view of bypass
silently
desynchronises from the GUI.

## Why

This is exactly the failure reported in aethersdr#2892 by @TnxQSO-Admin: BYPASS
engages,
user switches the strip TX→RX→TX (or the inverse), and the BYPASS button
spontaneously unchecks. The reporter's root-cause analysis pinpointed
the
empty-snapshot edge case and recommended dedicated bool members as the
source
of truth — this PR implements that recommendation.

The fix also covers the restart variant the issue body mentions: when a
prior
session ended with bypass engaged, all stages were persisted as
disabled. The
next BYPASS click in a fresh session hits the same empty-snapshot case;
with
the flag, the button now stays checked.

## Scope

- `src/core/AudioEngine.h` — declare `bool m_txBypassActive{false}` and
`bool m_rxBypassActive{false}` alongside the existing snapshot vectors;
  comments updated to clarify the new division of responsibility.
- `src/core/AudioEngine.cpp` — getters return the flag; setters assign
the
flag before `emit`. The snapshot retains its restore role unchanged — it
still records which stages were enabled at the moment of bypass so we
can
  re-enable exactly that set on release.

+14 / -8 lines. No protocol, persistence, or UX change; no
signal-emission
change beyond timing the flag write before the emit.

## Test plan

- [x] Local incremental `ninja -C build` clean on Linux (Nobara 43, Qt
6.10.3, gcc 15.2.1)
- [x] Bug reproduced on baseline `upstream/main` (`b9df0b3`) against a
FLEX-6700 — TX side, all stages off, click BYPASS, switch RX, switch
back to TX → BYPASS unchecks itself
- [x] Fix verified against the same FLEX-6700, same repro steps — BYPASS
stays checked across the TX/RX/TX cycle
- [ ] Mac smoke — open to running this on macOS if requested

Closes aethersdr#2892

---

73, Ozy **K6OZY**
AI compute partnership: [cloaked.agency](https://cloaked.agency) —
drafted with Don, our VP of Engineering agent, on Linux dev stack
(`nobara-dell`).
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.

BYPASS state lost on TX/RX switch when all chain stages were already disabled

3 participants