Skip to content

fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. - #6260

Merged
NF0T merged 3 commits into
aethersdr:mainfrom
jcmerg:fix/lineout-mute-no-drop-notice
Oct 10, 2026
Merged

NF0T merged 3 commits into
aethersdr:mainfrom
jcmerg:fix/lineout-mute-no-drop-notice

Conversation

@jcmerg

@jcmerg jcmerg commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On a Hermes-Lite 2, every click on the title-bar speaker (mute or unmute) shows the notice that the radio does not support the function:

WRN aether.protocol: RadioModel: no command plane for this backend, dropping mixer lineout mute 1

The click mutes this computer's output (AudioEngine::setMuted) and sends RadioModel::setLineoutMute(). On a radio without a command plane that call sends the Flex text mixer lineout mute N, sendCmd() drops it, and the drop notice fires. The HL2 has no line out of its own, so there was never anything to mute there.

Revised after review. The first version gated the text on "no command plane", which also silenced the notice on HL2, Icom and RTL for a request that nothing applies (only ANAN implements the line-out seam). That reversed the convention transmit_seam_drop_notice_test pins for VOX/MON/PROC, so it is replaced by a declared record.

Change

  • RadioCapabilities::lineoutControl, an optional record, the txMonitorControl shape. The radio has its own line out: Flex by its command plane, otherwise IRadioBackend::setLineoutGain/Mute must apply it.
    • Flex and ANAN declare it (ANAN: speaker silence and host-side scaling).
    • HL2, Icom, RTL and Sim state its absence. The Sim still takes the text through its synthetic command plane, so it behaves as a Flex here.
    • It is not a bool, so the frozen-70 ratchet is unchanged.
  • RadioModel::lineoutThroughSeam(): a declaring non-Flex backend takes the request typed and no text is sent. Otherwise the text goes out exactly as on main, so an undeclared radio keeps its drop notice.
  • lineoutControlsAvailable() in MixerControlAvailability.h: !connected || hasCommandPlane || declaresLineout.
    • The title-bar speaker mutes the radio's line out beside this computer's output only where it exists (Title bar speaker button conflates radio line out mute with local PC audio mute #4665: both paths where the radio has both).
    • Where there is no line out and PC Audio is off too (Icom, RTL), the click is refused with the unsupported-control notice and the icon is put back, the way the MIDI master knob refuses (MainWindow_Controllers.cpp).
    • Radio Setup → Radio Audio Outputs → Line Out (slider, value, Mute) is dimmed with its reason ("this radio has no line out AetherSDR can control") on tooltip and accessibleDescription where it does not exist, the same shape as the headphone pair. The gate is re-applied on connectionStateChanged / capabilitiesChanged, so the dialog follows a reconnect to another radio.
  • Capabilities map: new lineoutControl row.
  • User docs: menu-reference.md, "Speaker and master slider", says what the speaker does on a radio with no line out.

Headphone gain and mute (#5482) are not touched, including Radio Setup's ungated headphone Mute.

Evidence

transmit_seam_drop_notice_test:

  • lineoutReachesDeclaredSeamWithoutDropNotice: with the record, mute, unmute and gain reach the backend typed, with no notice.
  • lineoutWithoutRecordKeepsDropNotice: with the HL2's answers, there is no seam call and the notice stands for both mixer lineout mute and mixer lineout gain.

mixer_control_availability_test: four rows for the line-out predicate (HL2 unavailable, ANAN available, Flex available, disconnected available).

Mutations, each red:

Mutation Red row
lineoutThroughSeam() ignores the record "no seam call to a backend that declares no line out" (+1)
lineoutThroughSeam() → false "setLineoutMute() reached a declaring backend for each click" (+2)
lineoutControlsAvailable() ignores the record "a declared line out with no command plane -> available (ANAN)"

Also green locally: anan_backend_test, radiomodel_audio_mute_test, check_capability_records --strict (70 booleans, at the frozen 70).

Not run:

  • The title-bar click on hardware after this revision. The refusal (no line out, PC Audio off) needs an Icom or RTL; the demo radio has a command plane and cannot reach it, so that path is reasoned from code.
  • A Flex.
  • transmit_seam_drop_notice_test and mixer_control_availability_test are not in a ci.yml -R filter, so CI's green does not cover them; the results above are local (macOS 26, Qt 6.12.0).

CHANGELOG.md untouched.

@jcmerg
jcmerg requested a review from a team as a code owner October 7, 2026 19:47

@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 at the PR head 123ad88 (checkout at /tmp/aetherclaude/pr-6260). Everything below is reasoned from source — I have no build, no test run and no radio, so nothing here was reproduced at runtime.

1. Issue fit

No Fixes line; the PR body is the report, and GOVERNANCE.md does not want an RFC for a bug fix with a named root cause, so that's fine. The symptom is real and the root cause is correctly identified: setLineoutMute() sends mixer lineout mute N, sendCmd() drops it at hasCommandPlane() and burns the once-per-session notice.

I checked the cited authority. The #4665 ruling exists — ten9876, 2026-10-03: "The speaker click keeps doing what it does today; muting both paths is harmless on the inactive one." Credit for citing it accurately.

But the body's mechanism claim does not hold on the radio in the report:

"Without a command plane, sendCmd() drops the text and raises commandDropped, even though the seam call already carried the request."

AnanBackend is the only backend that overrides IRadioBackend::setLineoutGain/setLineoutMute (src/core/backends/IRadioBackend.h:432-433, defaults Q_UNUSED). Hl2Backend overrides neither, so on the reporter's HL2 the seam call is a no-op and nothing carried the request. The mute the operator hears is m_audio->setMuted(muted) on the line below in src/gui/MainWindow.cpp:2047. on8st said the same thing in the #4665 thread from an HL2: "there is no radio-side mixer at all… It still does not reach any hardware."

So: Partially. The user-visible outcome on an HL2 is right (PC Audio is pinned on there by setPcAudioLocked, so the line out genuinely is the inactive path), but the gate was placed on the wrong predicate and reaches further than the claim.

2. Scope

File What it changes Claimed? Verdict
src/models/RadioModel.cpp:11910-11916 new sendsLineoutWireText() yes in scope
src/models/RadioModel.cpp:11927-11929, 11951-11953 guards the two mixer lineout sends yes in scope
src/models/RadioModel.h:597-598 declares the predicate (private helper, no public/protocol surface) yes in scope
tests/transmit_seam_drop_notice_test.cpp RecordingBackend lineout overrides + one case yes in scope, but see Blocker 2

Everything in the diff is explained by the stated fix. No new settings key, capability field, protocol verb or config surface. No CHANGELOG.md entry — correct. No guard or symptom-naming comment deleted. Socket tests: none added, modified or removed; the touched test links aethercore + Qt6::Core only and opens nothing.

CI is 5/5 green on 123ad88, but note transmit_seam_drop_notice_test is registered at tests/tests.cmake:573-576 and is not named in any ctest -R step in ci.yml — the green badge did not run the new case.

3. Blockers

1. src/models/RadioModel.cpp:11910-11916 — the guard asks "no command plane?" when the question is "does this backend apply the lineout seam?". Only ANAN does; on every other family the request now vanishes with no notice.

bool RadioModel::sendsLineoutWireText() const
{
    return !m_backend || usesFlexCommandPlane() || hasCommandPlane();
}

AnanBackend::setLineoutMute (AnanBackend.cpp:1029-1032) stores m_lineoutMuted, and it is genuinely consumed at AnanBackend.cpp:1674 — on ANAN the PR's framing is exactly right. Hl2Backend, IcomCivBackend, the RTL backend and the simulator all inherit the Q_UNUSED defaults, so after this change the operator's request is discarded at the seam and the drop notice that said so is gone. That is the #5263 dead-control shape with the alarm removed.

This cuts against three things already in the tree:

  • the convention pinned two functions above the new case, tests/transmit_seam_drop_notice_test.cpp:514-529: "The HL2 implements neither setVox nor setTxMonitor and declares neither record" → notice kept. Same at :501-511 for PROC. Lineout declares no record either, and gets the opposite treatment.
  • src/gui/MixerControlAvailability.h, which states the project's position directly: "A connected radio with no command plane has no such mixer", and prescribes the shape — the headphone pair is dimmed with its reason, not silently accepted (MainWindow_Session.cpp:3239).
  • src/gui/MainWindow_Controllers.cpp:2317-2328: the MIDI global.masterVolume knob on exactly this path logs "refused: no radio mixer and PC Audio is off" and raises the unsupported-control notice. After this PR the title-bar slider and the Radio Setup slider take the same request silently.

Worst instance, and the one with no PC-sink companion to make it harmless: the Radio Setup → Radio Audio Outputs → Line Out slider and Mute button, src/gui/RadioSetupDialog.cpp:4692-4714.

    connect(lineoutSlider, &QSlider::valueChanged, this, [this, lineoutValue](int v) {
        lineoutValue->setText(QString::number(v));
        m_model->setLineoutGain(v);
    });
    connect(lineoutMute, &QPushButton::toggled, m_model, &RadioModel::setLineoutMute);

outGroup is created at :4683 and added at :4781 with no availability or hasCommandPlane gate anywhere in 4600-4790. On an HL2 those two widgets are live and enabled, reach the base-class no-op, and after this PR say nothing at all.

The capability-shaped fix is the record precedent this file already uses for VOX/MON/PROC: an std::optional<RadioCapabilities::LineoutControl> that AnanBackend declares and the others don't, gating both the wire text and the seam call — an optional record needs no FROZEN_BOOL_COUNT ruling. Alternatively, extend MixerControlAvailability.h to the lineout pair and dim it with its reason the way the headphone pair already is. Either keeps #4665's ruling intact (the click still mutes both paths on a radio that has both) without silencing a radio that has neither.

2. tests/transmit_seam_drop_notice_test.cpp:95-96, 534-548 — the new case's fixture gives the HL2 a lineout seam the real Hl2Backend does not have, which is why Blocker 1 is invisible.

    void setLineoutGain(int percent) override { lineoutGains << percent; }
    void setLineoutMute(bool mute) override { lineoutMutes << mute; }

RecordingBackend overrides both, and the case runs it under hostModulatingTransmitter(), documented at :100-102 as "the Hermes-Lite 2's answers". So the test asserts the ANAN arm and labels it HL2 — the one configuration where the PR's premise is true. It proves the seam is called, never that anything applies it. The arm that matters is the one voxAndMonitorWithoutRecordsKeepDropNotice covers for VOX: a backend that does not override the setters, where the notice should be kept (or where the maintainer rules explicitly that an inactive-path request is withheld silently).

Secondary: the case is one-sided. Replacing sendsLineoutWireText() with return false passes every check in it, so nothing pins the Flex-sends-text half. #6255 shows the local pattern — a command sink on an unopened RadioConnection and the text compared whole.

4. Nits (non-blocking)

  • src/models/RadioModel.cpp:11915 — hasCommandPlane() is redundant next to usesFlexCommandPlane(). m_connection is assigned only inside the dynamic_cast<FlexBackend*> block (RadioModel.cpp:882) and m_wanConn only on the SmartLink path (:3802), so no non-Flex backend can make it true, and any built Flex already satisfies the first term. The predicate reads as three cases where there are two.
  • src/models/RadioModel.cpp:11942-11947 — the doc comment above setLineoutMute() is now stale: "Unlike the gain setters, the command is always sent: a mute is a request, and suppressing it on an unchanged model would make model/radio drift unrecoverable from the UI." The new in-function comment says "Sent unconditionally" about the seam call, so a reader gets two contradictory statements about the same function.
  • tests/transmit_seam_drop_notice_test.cpp:531-533 — the "Flex now has its VOX, monitor and PROC setters called…" block is now orphaned above the new case instead of above flexSeamSettersWriteNothing().
  • src/gui/RadioSetupDialog.cpp:4743 — the dialog's headphone Mute is still ungated on a radio with no headphone jack, while the title-bar pair is dimmed. Pre-existing, correctly out of scope here ("Headphone gain and mute are not touched"), and route-separately rather than fold in.

5. What I tried to break, and could not

  • Is the test honest about failing on main? Yes. Without the guard, sendCmd("mixer lineout mute 1") reaches the !hasCommandPlane() branch (RadioModel.cpp:~9834-9845) and emits commandDropped with that exact text, so droppedStartingWith("mixer lineout") is true and the case goes red. The author's stated return true mutation is the right one.
  • Does the gain check self-satisfy? No. m_lineoutGain{50} (RadioModel.h:1923), so setLineoutGain(30) passes the equality early-return and reaches both paths.
  • Is the Flex path really untouched? Yes, two ways: usesFlexCommandPlane() is m_family == "flex", and m_connection = flex->connection() makes hasCommandPlane() true for any built Flex. The seam call stays behind m_backend && !usesFlexCommandPlane(), so text and seam remain mutually exclusive and never both-off while a backend exists.
  • Does the !m_backend arm leak a false notice? It sends text on a bare model, which radiomodel_audio_mute_test depends on, and with no backend nothing carried the request, so a drop notice there is honest. Correct as written.
  • Master volume with PC Audio off on an HL2 — I expected a silently dead slider via applyMasterVolume → setLineoutGain (MainWindow.cpp:7517), but applyTxAudioCapabilities pins PC Audio on for a host-modulating transmitter (MainWindow_Session.cpp:3200-3206), and the MIDI knob refuses before reaching the model. Not reachable on the reporter's radio; it is reachable on an Icom/RTL profile with PC Audio off, which is folded into Blocker 1.
  • Could not check: anything at runtime, on a Flex, or on hardware — the PR already discloses it was not re-run on hardware after the fix. I also did not verify that transmit_seam_drop_notice_test passes; CI never ran it.

6. Recommendation

Needs maintainer decision, with Blocker 2 to fix either way.

The title-bar symptom is real and the HL2 outcome the author wants is defensible — but the seam chosen is wider than the claim. "No command plane" is not "the backend applies this request", and the only backend that applies it is ANAN. Whether an operator request that reaches no hardware should go silent is the question #5919 and MixerControlAvailability.h already answered one way (keep the notice, or dim the control with its reason), and this PR answers the other way for a whole class of radios on the strength of a premise that is false on the reporter's own.

Concrete next step: narrow the gate to a declared capability (an optional LineoutControl record on RadioCapabilities, or the MixerControlAvailability.h lineout pair), and add the test arm where the backend does not override the two setters so the HL2/Icom/RTL case is pinned rather than assumed. If a maintainer prefers the broad "inactive path, no notice" rule, that is a legitimate call — but it wants saying out loud in the body, because it reverses the convention this test file pins fifteen lines above the new case.

Thanks for the careful mutation note and for naming what wasn't re-tested on hardware — that is what made the premise checkable.


🤖 aethersdr-agent · cost: $9.6891 · model: claude-opus-5

@jcmerg jcmerg changed the title fix(audio): no drop notice when the title-bar mute reaches a radio through the seam. Principle VIII. fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. Oct 7, 2026
@jcmerg

jcmerg commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Both blockers were right, and my body's premise was wrong. Hl2Backend does not override setLineoutGain/Mute, so on the HL2 the seam call carried nothing.

Reworked in ae5f874b along the record route you suggested:

  • Blocker 1: RadioCapabilities::lineoutControl (optional record; Flex and ANAN declare it). RadioModel routes by it: declared non-Flex → typed seam, no text; otherwise the text as on main, so HL2/Icom/RTL keep the drop notice for a request nothing applies. The title-bar speaker now asks lineoutControlsAvailable() before touching the line out, so the HL2 click mutes only this computer's output and the notice is gone for the reason that is actually true. Radio Setup's Line Out row is dimmed with its reason where the record is absent.
  • Blocker 2: the HL2-shaped fixture no longer gets a lineout seam. lineoutWithoutRecordKeepsDropNotice pins the undeclared arm (no seam call, notice for mute and gain); lineoutReachesDeclaredSeamWithoutDropNotice sets the record explicitly. The availability predicate has its own rows in mixer_control_availability_test. Mutation results are in the body.
  • Nits: sendsLineoutWireText() and its redundant term are gone with the old gate; the stale "always sent" comment now reads true again (the mute is still sent unconditionally, to whichever route applies); the Flex comment block is back above flexSeamSettersWriteNothing().

Still not done: no hardware click after this revision, no Flex run, and these two tests are outside CI's -R filters. All stated in the body.

@NF0T NF0T self-assigned this Oct 10, 2026

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

Reviewed at ae5f874b. I built it locally (Windows, MinGW 13.1, Qt 6.12.0), ran the two tests, mutated the code myself, and drove main and this head against the demo radio through the automation bridge.

What's right

The revision gets the model right. lineoutThroughSeam() puts the request on exactly one route: typed through the seam for a backend that declares lineoutControl, Flex wire text otherwise. The undeclared radios (HL2, Icom, RTL) keep their mixer lineout drop notice inside RadioModel, which matches the VOX/MON/PROC convention this test file already pins. Both of @aethersdr-agent's blockers are fixed as asked:

  • Blocker 1. The record route replaces the "no command plane" gate. ANAN is the only non-Flex backend that declares the record, and it is the only one with real setLineoutGain/setLineoutMute overrides.
  • Blocker 2. The no-record arm now asserts no seam call and keeps the notice for both mute and gain.

The stale-comment nit and the orphaned Flex comment block are also fixed. The optional record means the frozen-70 bool count stays put.

One blocker: the title-bar speaker goes silently dead on Icom/RTL with PC Audio off

src/gui/MainWindow.cpp:2049-2054. The new gate skips setLineoutMute() whenever lineoutControlsAvailable() is false, then mutes this computer's output. That is right on the HL2, because applyTxAudioCapabilities() pins PC Audio on there (pcAudioRequired = takesTxAudioOverSeam && canTransmit && hostModulates). It doesn't hold on Icom (hostModulates = false) or RTL (canTransmit false). On both, PC Audio can be off, and then:

  1. The click sets the button checked and emits lineoutMuteChanged(true).
  2. The gate is false, so setLineoutMute() is skipped and m_lineoutMute stays false.
  3. m_audio->setMuted(true) emits mutedChanged, which calls syncTitleBarOutput().
  4. With PC Audio off, syncTitleBarOutput() calls m_titleBar->setLineoutMuted(m_radioModel.lineoutMute()), which is false, so the icon snaps back to unmuted.
  5. There's no notice, and PcAudioMuted=True has been saved without the operator seeing it. That part is pre-existing, but it is now the only effect of the click.

On main the same click sends the text, it's dropped, the notice fires, and the icon stays muted. The body says "an undeclared radio keeps its drop notice". That's true in RadioModel, but this path never reaches RadioModel. It's the #5263 dead-control-with-the-alarm-removed shape again, one layer up.

The tree already has the answer for this exact state. The MIDI global.masterVolume handler (MainWindow_Controllers.cpp:2317-2327) refuses with showUnsupportedControlNotice() when there is "no radio mixer and PC Audio is off". The inline suggestion does the same for the speaker: refuse and resync the icon when there is neither a line out nor PC Audio, and otherwise behave exactly as this revision does.

Also asked for in the same push (non-blocking on their own)

  • Radio Setup's Line Out gate is evaluated once. src/gui/RadioSetupDialog.cpp:4716-4731 runs when the Audio page is lazily built. The dialog is a non-modal PersistentDialog that can stay open across a disconnect and reconnect. Open it on an HL2, switch to a Flex, and the Flex's Line Out row stays dimmed until the dialog is closed. The other gates in this dialog re-evaluate live: rebootBtn at :1406-1407, the radio-side record button at :5170-5185, and the calibration item on connectionStateChanged. Re-applying it on connectionStateChanged/capabilitiesChanged, and re-enabling with the tooltip cleared, would match them.
  • The capabilities-map row is wrong about the Sim. The row says "Absent (HL2, Icom, RTL, Sim): the title-bar speaker mutes only this computer's output, Radio Setup's Line Out row is dimmed". The Sim owns a RadioConnection (RadioModel.cpp, the SimBackend branch of setupBackend, m_connection = sim->connection()), so hasCommandPlane() is true and neither change applies there. I confirmed this on the demo: see below. Suggested wording: "Sim: absent, but its synthetic command plane takes the text, so it behaves as a Flex here."
  • User docs. The docs site (#6286) merged on 10-08, the day after your last push, and AGENTS.md now asks every user-visible change to update docs/user/ in the same PR. docs/user/docs/audio-settings.md already says controls for a missing output are dimmed with a reason, and this PR finally makes that true for Line Out. docs/user/docs/menu-reference.md's "Speaker and master slider" table has no row for a radio without a line out. One sentence there covers it once the speaker fix lands.

Scope

Change Claimed Verdict
RadioCapabilities::lineoutControl + six backend declarations yes in scope; optional record, no bool-ratchet growth
RadioModel::lineoutThroughSeam() and the two setters yes in scope; net one fewer usesFlexCommandPlane() branch, no new wire text (#5554 checklist clean)
lineoutControlsAvailable() in MixerControlAvailability.h yes in scope
Title-bar speaker gate (MainWindow.cpp) yes in scope; blocker above
Radio Setup Line Out dimming yes in scope; see the staleness note
radio-capabilities-map.md row yes in scope; Sim wording is wrong
Two tests yes in scope; no sockets (Step-0 clean)

Preference vs fix: a fix. The Radio Setup dimming applies an existing documented rule ("a control the radio lacks is dimmed with a stated reason, never hidden"), and the speaker change follows the #4665 ruling. No default changes. All files are Tier 3. Commits are signed. No CHANGELOG.md entry, which is correct.

What I tried to break

  • Ran both tests at the PR head: transmit_seam_drop_notice_test reported 0 failure(s) and mixer_control_availability_test reported PASS. You're right that CI never runs either of them.
  • My own mutation, different from yours: I restored main's setLineoutMute() (text plus an unconditional seam call). Two rows went red: [FAIL] lineout: no commandDropped where the backend applied it and [FAIL] lineout: no seam call to a backend that declares no line out. The tests pin the routing.
  • Bridge, main (10-06 build) and this head, both against DEMO-0001: I used an isolated settings dir, AETHER_AUTOMATION_NO_TX=1, and connected explicitly to the demo with no autoconnect. I clicked pcAudioBtn off, then clicked Line out mute. Both builds read back {"lineoutMuted":true,"pcAudioEnabled":false} from the titlebar model and logged no drop. That proves no regression on a command-plane radio, and it's how I found the Sim has one. The blocker above can't be reached in demo mode for the same reason. It is reasoned from code: steps 1–5 above, each line cited. I didn't have an Icom or RTL to click it on.
  • Checked and fine: the Flex path (usesFlexCommandPlane() is true, so text only, as on main); the !m_backend arm, which still sends text so radiomodel_audio_mute_test's contract holds; and ANAN, which now gets the seam only with no false notice. The other mute entry points (master_mute_toggle shortcut, MIDI global.masterMute) were and still are PC-only, so they're untouched.

For future reference

  • masterKnobDrivesLocalOutput() still refuses the MIDI master knob on ANAN with PC Audio off, while the title-bar slider now reaches ANAN's line out through the seam. Teaching it the new record would be a natural small follow-up, not this PR.
  • The Line Out tooltip says "Its audio plays on this computer". On an Icom with PC Audio off it plays on the rig's own speaker. Something like "this radio has no line out AetherSDR can control" stays true on every family.

Thanks for taking the record route cleanly and for the mutation table. It made the routing quick to check, and the remaining gap is one lambda.

73, Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

Comment thread src/gui/MainWindow.cpp
@NF0T NF0T removed their assignment Oct 10, 2026
jcmerg and others added 3 commits October 10, 2026 08:22
…rough the seam. Principle VIII.

The title-bar speaker mutes the radio's line out and the PC sink together
(#4665 ruling: muting the inactive path is harmless). On a backend without
a command plane (HL2, ANAN, ...) setLineoutMute() and setLineoutGain()
still sent the Flex `mixer lineout` text as well as the typed seam call,
so every click raised "the radio does not support this" although the seam
had carried the request. The text now goes only to a radio with a command
plane; the seam call is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ng command plane. Principle VIII.

Review of #6260: only ANAN applies setLineoutGain/Mute, so gating the wire
text on "no command plane" silenced the drop notice on HL2, Icom and RTL
for a request nothing applied.

- RadioCapabilities::lineoutControl, an optional record: Flex and ANAN
  declare it, every other family states its absence.
- RadioModel::lineoutThroughSeam(): a declaring non-Flex backend takes the
  request typed and no text is sent; otherwise the text goes out as on main,
  so an undeclared radio keeps its notice.
- The title-bar speaker mutes the line out beside this computer's output
  only where lineoutControlsAvailable() (#4665); Radio Setup's Line Out row
  is dimmed with its reason elsewhere.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…o has no line out and PC Audio is off. Principle VIII.

Review of #6260 (NF0T): on Icom or RTL with PC Audio off, the click skipped
setLineoutMute(), and syncTitleBarOutput() then read the unchanged
lineoutMute() and snapped the icon back, with no notice. Neither output is
AetherSDR's to mute there, so the click now refuses the way the MIDI master
knob does (showUnsupportedControlNotice) and resyncs the icon. Every other
case is unchanged.

Also: Radio Setup's Line Out gate is re-applied on connectionStateChanged
and capabilitiesChanged, since the dialog can stay open across a reconnect,
and its tooltip no longer claims the audio plays on this computer; the
capabilities map says the Sim takes the text through its command plane; and
menu-reference.md gains the no-line-out sentence.

Rebased onto main (ca5e063).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@jcmerg
jcmerg force-pushed the fix/lineout-mute-no-drop-notice branch from ae5f874 to e4feb42 Compare October 10, 2026 06:31
@jcmerg

jcmerg commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. The blocker was right: the gate stopped at RadioModel, and the icon reverted silently one layer up.

Addressed in e4feb42c, rebased onto main (ca5e0634):

  • Blocker, speaker on Icom/RTL with PC Audio off: your suggestion, taken as is. With neither a line out nor PC Audio, the click calls showUnsupportedControlNotice(), then syncTitleBarOutput(), and returns. Every other case is unchanged.
  • Radio Setup gate evaluated once: now a lambda, re-applied on connectionStateChanged and capabilitiesChanged. It re-enables the row and clears the tooltip and accessibleDescription when a line out is available.
  • Sim wording in the capabilities map: corrected along the lines you suggested. Mentioned in the body too.
  • User docs: one sentence under "Speaker and master slider" in menu-reference.md.
  • Tooltip: now "this radio has no line out AetherSDR can control".

Built on macOS 26, Qt 6.12.0. transmit_seam_drop_notice_test reports 0 failures and mixer_control_availability_test passes. Not run: the refusal path itself. It needs an Icom or RTL; the demo radio has a command plane and never reaches it.

The masterKnobDrivesLocalOutput()/ANAN follow-up is noted. I'll leave it out of this PR.

@NF0T NF0T self-assigned this Oct 10, 2026

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

Re-reviewed at e4feb42c (rebased onto ca5e0634). Everything from my review at ae5f874b is addressed, and nothing new turned up.

What changed, checked against the diff

git range-diff between the two heads shows commit 2 changed by the rebase only in context lines (the DocsLinks.h include and an updated speechProcessorControl row from main). Commit 3 is new and covers every item:

  • Blocker, title-bar speaker on Icom/RTL with PC Audio off: the suggestion applied as written (MainWindow.cpp:2055-2074). With neither a line out nor PC Audio, the click calls showUnsupportedControlNotice(), then syncTitleBarOutput() puts the icon back, and it returns. You're right that no extra connected guard is needed: lineoutControlsAvailable() is true while disconnected, so !lineout already implies connected.
  • Radio Setup gate evaluated once: now applyLineoutAvailability, run at build time and again on connectionStateChanged and capabilitiesChanged, with lineoutMute as the connection context, so it dies with the page. It re-enables the row and clears the tooltip and accessibleDescription when a line out exists. That is safe: these four widgets set neither on their own, so clearing them loses nothing.
  • Capabilities map: the Sim now reads as "behaves as a Flex here", and the refusal is documented.
  • User docs: the menu-reference.md sentence covers the no-line-out row and the refusal. "Build docs site" is green.
  • Tooltip: now "this radio has no line out AetherSDR can control", which stays true on every family.

What I ran

  • Rebuilt at e4feb42c (Windows, MinGW 13.1, Qt 6.12.0), because CI's -R filters still don't run these tests and the rebase touched RadioModel context. transmit_seam_drop_notice_test reported 77 PASS, 0 failure(s). mixer_control_availability_test reported 36 PASS. All six line-out rows are green.
  • The routing mutation from my last review still applies unchanged to RadioModel.cpp, which this push didn't touch, so its result carries over.
  • Not reproduced: the refusal path itself. It needs an Icom or RTL, because the demo radio has a command plane, as we both found. It is three lines that copy the MIDI master-knob precedent, and I checked them against syncTitleBarOutput()'s QSignalBlocker path.

CI is 6/6 green on the head, and all three commits are signed.

Verdict

Approved. All files are Tier 3. I'll squash-merge as:

fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. (#6260)

The masterKnobDrivesLocalOutput()/ANAN follow-up stays out of scope, as agreed.

Thanks for a clean turnaround: suggestion taken, every side item handled, and the untested path named.

73, Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

@NF0T
NF0T merged commit eb9a1fb into aethersdr:main Oct 10, 2026
7 checks passed
@NF0T NF0T removed their assignment Oct 10, 2026
on8st added a commit to on8st/AetherSDR that referenced this pull request Oct 10, 2026
Seven conflicts, all one cause: aethersdr#6260 (eb9a1fb) added the lineoutControl
record on the line where this branch adds txWaterfallClientFlag. Both
sides are kept in every file, main's line first, this branch's after it.

- src/core/backends/anan/AnanBackend.cpp: lineoutControl engaged (main),
  then txWaterfallClientFlag absent.
- src/core/backends/flex/FlexBackend.cpp: lineoutControl engaged (main),
  then txWaterfallClientFlag absent.
- src/core/backends/hl2/Hl2Backend.cpp: lineoutControl absent (main), then
  txWaterfallClientFlag engaged with clientPersistsFlag true.
- src/core/backends/icom/IcomCivBackend.cpp: lineoutControl absent (main),
  then txWaterfallClientFlag absent.
- src/core/backends/rtl/RtlSdrBackend.cpp: same.
- src/core/backends/sim/SimBackend.cpp: same.
- docs/architecture/radio-capabilities-map.md: main's lineoutControl row,
  then this branch's txWaterfallClientFlag row.

Everything else merged without conflict. aetherd-touchpoints.md was
regenerated and did not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NF0T pushed a commit that referenced this pull request Oct 11, 2026
- Radio Setup -> Audio -> Radio Audio Outputs: the Headphone row (caption, slider,
  value, mute) is now dimmed, never hidden, on a connected radio with no command
  plane (Hermes-Lite 2, ANAN, Icom), using the title bar's own predicate
  (headphoneControlsAvailable) and its two reason sentences on tooltip and
  accessibleDescription.
- Re-evaluated on connectionStateChanged; neither direction writes the level or
  the mute. The Line Out row (#6260) is untouched and gated separately.
- New socket-free offscreen test pins HL2 and ANAN (real capabilities) dimmed,
  ANAN Line Out live, and a Demo command-plane positive control.
- Refs #5482.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
rfoust pushed a commit that referenced this pull request Oct 12, 2026
#6388)

Refs #5482, #6353

This is the follow-up to the two forward notes in the approving review
of #6353.

## Limits, first

- **macOS, offscreen platform only** (Qt 6.12.0). The pixels are from
`QWidget::grab()` in a test. Nobody looked at the dialog on a screen,
and there is no screenshot.
- **Two themes rendered: Default Dark and Default Light.** No user
theme.
- **The Line Out row's styling is touched; its logic is not.** That row
is @jcmerg's (#6260). His predicate, his reason text and his
`applyLineoutAvailability` lambda are unchanged. Only the three
stylesheets of the row gain a `:disabled` rule, the same one the
Headphone row gets, so the two rows keep agreeing.
- **The Front Speaker row is not touched.** It is never disabled.
- **A dimmed Mute looks the same engaged or not** (from this PR's
review, `c0bfd883`). An engaged Mute on an unavailable row took the
unavailable text on the engaged red background. It now takes the row's
own unchecked background (`color.background.1`), so the engaged state is
not shown while the row is unavailable. §4a names no look for a control
that is unavailable and also has state, so whether the state should stay
visible is the maintainer's call. The checked state is still on the
widget for accessibility clients.
- **Contrast is what the token gives.** `color.control.unavailable` is
2.09:1 (dark) and 1.77:1 (light) by the style guide's own table (§4a).
This PR uses the token and does not change it.
- **The Line Out reason sentence is not hoisted.** It exists in one
place only (`src/gui/RadioSetupDialog.cpp:4774-4775` at `9fabcbf8`); no
other source file and no test repeats it.
- `Refs`, not `Fixes`: closing #5482 stays your call.

## What was wrong

At `9fabcbf8`, in Radio Setup → Audio → "Radio Audio Outputs", a
disabled row shows it on the slider only. The other three widgets of
each row carry their own stylesheet, and none has a `:disabled` rule:

| widget | stylesheet | applied at (Line Out / Headphone) |
|---|---|---|
| caption label | `kLabelStyleTemplate`, `QLabel { color:
{{color.text.primary}}; … }` (`src/gui/RadioSetupDialogCommon.h:31-32`)
| `src/gui/RadioSetupDialog.cpp:4740` / `:4794` |
| value label | `kValueStyleTemplate`, `QLabel { color:
{{color.accent.bright}}; … }` (`RadioSetupDialog.cpp:129-130`) | `:4745`
/ `:4799` |
| Mute button | inline `QPushButton { … color: {{color.text.primary}}; …
}` plus `:checked` | `:4751-4753` / `:4805-4807` |
| slider | none of its own | `:4742` / `:4796` |

An explicit `color` with no `:disabled` rule also applies when the
widget is disabled, so the label, the value and the Mute button render
the same in both states. The review of #6353 measured 0 differing pixels
on them; the test here reproduces that (below).

The two headphone reason sentences were written out three times:
`src/gui/TitleBar.cpp:1297-1302`,
`src/gui/RadioSetupDialog.cpp:4826-4834`, and
`tests/radio_setup_headphone_output_gate_test.cpp:134-139`.

## The change

**The look.** A disabled label, value or Mute button of the two rows
takes `color.control.unavailable`, the token §4a of
`docs/style/theme-style-guide.md` names for the unavailable state:

```
QLabel:disabled      { color: {{color.control.unavailable}}; }
QPushButton:disabled { color: {{color.control.unavailable}}; }
```

This is the pattern already in the tree for the TX timing fields (#6166,
`RadioSetupDialog.cpp:2565-2567`, `QLineEdit:disabled`) and for
`WfmApplet.cpp:58`. The rules go through
`ThemeManager::applyStyleSheet`, so they follow a theme switch like
their neighbours. No colour literal is added. The enabled look is
unchanged: the rule is appended to the existing template, and only for
these six widgets. `makeValueLabel` takes an optional extra rule so
`kValueStyleTemplate` keeps its single reference. The shared
`applyLabelStyle` is not changed, so no other page of the dialog moves.

**The wording.** `src/gui/MixerControlAvailability.h` now holds the two
sentences next to the predicate, as `kHeadphoneLevelUnavailableReason`
and `kHeadphoneMuteUnavailableReason`. They are `constexpr char[]`, so
the header stays free of Qt. `TitleBar::setHeadphoneAvailable`, the
Radio Setup lambda and both tests read them from there. The text itself
is unchanged, character for character.

No predicate, no capability field, no default and no user-visible string
changes. `docs/user/docs/audio-settings.md` already says the controls
"are dimmed with the reason in the tooltip", which stays true.

## Tests

`tests/radio_setup_headphone_output_gate_test.cpp` gains
`dimmedRowsLookUnavailableInBothThemes` (offscreen, no socket). For each
of Default Dark and Default Light it builds the dialog on the HL2
capabilities (both rows dimmed) and on the Demo (both rows live), and
for the label, value and Mute button of each row:

- renders the widget's own region with `grab()` and counts differing
pixels, exactly, no tolerance. It requires more than 0 between dimmed
and live;
- first asserts the two renders are comparable: same size, same value
text, same mute state. So the enabled state is the only difference;
- control 1: the same widget rendered twice differs in 0 pixels;
- control 2: after a disconnect the same HL2 widgets are live again and
render pixel-identical to the Demo's, and different from their own
dimmed render;
- requires the resolved stylesheet to carry the colour
`color.control.unavailable` has in the active theme.

With the change, differing pixels (measured at `6c83ac5a`, the same diff
before it was rebased onto current `main`; the test that asserts them
passes at the head), dimmed vs live, identical in both themes:

| region | Headphone | Line Out |
|---|---|---|
| label | 1329 / 6840 | 852 / 6840 |
| value | 410 / 2280 | 410 / 2280 |
| Mute | 512 / 3800 | 512 / 3800 |

**Fail-before.** Named mutation: this commit with both `:disabled` rule
strings emptied, which is main's styling. Full build and ctest. All
twelve regions read 0 differing pixels, as in the review:

```
PIXELS Default Dark | Headphone: label: dimmed vs live 0 / 6840 (same widget twice: 0)
PIXELS Default Dark | Headphone: value: dimmed vs live 0 / 2280 (same widget twice: 0)
PIXELS Default Dark | Headphone: mute: dimmed vs live 0 / 3800 (same widget twice: 0)
PIXELS Default Dark | Line Out: label: dimmed vs live 0 / 6840 (same widget twice: 0)
PIXELS Default Dark | Line Out: value: dimmed vs live 0 / 2280 (same widget twice: 0)
PIXELS Default Dark | Line Out: mute: dimmed vs live 0 / 3800 (same widget twice: 0)
(the same six lines for Default Light)
FAIL!  : RadioSetupHeadphoneOutputGateTest::dimmedRowsLookUnavailableInBothThemes() 'failures.isEmpty()' returned FALSE. (Default Dark | Headphone: label: dimmed renders as live
Default Dark | Headphone: label: not the unavailable token #3a4a5a
…
Totals: 5 passed, 1 failed, 0 skipped, 0 blacklisted
```

The test could not be run on unmodified main as it stands, because it
reads the sentences from the header. That is why the fail-before is a
mutation and not "the test alone".

**The wording.** `radio_setup_headphone_output_gate_test` compares the
row against the header's two constants.
`tests/mixer_control_availability_test.cpp` gains two rows: the title
bar's mute and slider descriptions equal the header's sentences. Its
existing rows still check the content independently ("Unavailable:", "no
headphone output", "speaker button", "master volume").

## ctest

macOS (Apple Silicon), Qt 6.12.0, `ENABLE_ASR=OFF`, full suite at `-j8`,
failures rerun serially, on `c0bfd883` (this PR's head):

- **765 tests, 763 passed, 2 failed**, both also on the serial rerun:
the dialog test below, and `vkamp_connection_test` with only its two
known checks (#5254).
- `peripheral_auth_dialog_interaction_test` fails, at `-j8` and
serially: "Clear code or Status is clipped at the default dialog size",
`dialog 960x680, viewport 658, page minimum 784`. This is the
host-dependent failure of #6008 (diagnostic in #6134): the Peripherals
page is as wide as the longest serial-port name on this Mac. The page
minimum is 784, the same figure as on clean main here. This PR does not
touch the Peripherals page.
- `radio_setup_headphone_output_gate_test` (6 passed),
`mixer_control_availability_test`, `titlebar_headphone_mute_test`,
`unified_title_bar_test` and `radio_setup_label_theme_token_test` pass.

Static checks, run locally: `tools/check_a11y.py` on
`RadioSetupDialog.cpp` 9 findings before and 9 after, `TitleBar.cpp` 0;
`tools/audit_colours.py` summary identical before and after;
`gen_touchpoint_manifest.py --check` and `docs/gen_reference.py --check`
up to date; the `tools/check_*.py --strict` set passes
(`check_codeql_coverage.py` needs a build directory and was not run).

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@jcmerg
jcmerg deleted the fix/lineout-mute-no-drop-notice branch October 12, 2026 10:26
ten9876 added a commit that referenced this pull request Oct 12, 2026
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants