Background
Add classic Apollo-era "Quindar" tones (2525 Hz intro, 2475 Hz outro, ~250 ms each) to MOX/PTT engagement on phone modes — fun cosmetic feature for SSB ops who want NASA mission-control aesthetic.
Two flavors selectable per-user:
| Style |
Intro |
Outro |
Notes |
Tone (default) |
2525 Hz sine, 250 ms |
2475 Hz sine, 250 ms |
Apollo Quindar |
Morse |
K at 45 WPM (≈ 240 ms) |
BK at 45 WPM (≈ 560 ms) |
Ham-radio convention: K = "go ahead", BK = "back to you" |
Morse style uses the active CW pitch (or a configurable Quindar pitch — TBD) as the carrier, generated with the same envelope as the sine version. Outro BK is intentionally longer than the 250 ms intro K because the signoff convention warrants the extra emphasis.
Ships disabled by default; opt-in per-user.
Trigger surface — single coordinator
Add TransmitModel::requestPttOn(source) / requestPttOff(source) as the entry point for all "user wants to key/unkey" intents (where source ∈ {Mox, TciHardware, TciDax, …}). Existing setMox() / setTransmit() stay as low-level emitters; the coordinator runs the tone state machine before/after them.
| Caller |
Today |
After |
src/gui/TxApplet.cpp:294 — MOX button |
setMox(on) |
requestPttOn/Off(Mox) |
src/core/TciServer.cpp:590 — radio-direct (e.g. Elgato footswitch via streamcontroller plugin) |
setTransmit(true) |
requestPttOn(TciHardware) |
src/core/TciServer.cpp:587 — DAX path (WSJT-X etc.) |
startTxChrono() |
unchanged (digital, not phone) |
State machine
Idle ──pttOn──▶ Engaging (intro tone, 250 ms)
│
▼ tone done
Live (mic audio)
│
pttOff request
▼
Disengaging (outro tone, 250 ms; xmit 1 still set)
│
▼ tone done → emit xmit 0
Idle
Engaging/Disengaging drive an atomic QuindarPhase read by AudioEngine on the audio thread.
DSP — ClientQuindarTone
New stage in src/core/ClientQuindarTone.{h,cpp}, lock-free atomic-driven (mirrors ClientGate / ClientTube shape):
- Tone style: 2525 Hz (intro) / 2475 Hz (outro) sine, 5 ms cos² ramp envelope to avoid clicks.
- Morse style: pre-rendered
K (intro) / BK (outro) at 45 WPM, single carrier frequency (default 750 Hz, configurable). Element timing: dot = 1200/45 ≈ 26.67 ms, dash = 80 ms, intra-char gap = 26.67 ms, inter-letter gap = 80 ms. Each element shaped with the same 5 ms cos² envelope.
K = dah-dit-dah → 9 units → ~240 ms total
BK = dah-dit-dit-dit + inter-letter + dah-dit-dah → 21 units → ~560 ms total
- Replaces mic samples wholesale during Engaging/Disengaging (simpler than ducking).
- Inserted after PooDoo chain in the TX path so the tone/morse is pure (no comp/EQ coloration).
- Injects directly into the DAX TX byte stream
AudioEngine already emits.
MOX-off defer
requestPttOff(source):
- If Quindar enabled + phone mode + currently Live →
audio->startQuindarOutro() + QTimer::singleShot(outroDurationMs, …, sendPttOff).
- Else →
sendPttOff() immediately (current behavior).
outroDurationMs is style-dependent: 250 ms for Tone, ~560 ms for Morse BK. Asymmetric outro durations are fine — the timer length is computed at request time from the active style.
The MOX button visibly stays lit during the outro because m_mox doesn't flip until the timer fires — the "stickiness" is the outro tone duration, which is the correct UX (user sees what's happening).
Phone-mode gate
isPhoneMode() = mode ∈ {USB, LSB, AM, FM, NFM, DIGU, DIGL} checked at request time on the active slice. CW / FT8 / DIGITAL → bypass Quindar entirely.
Settings (AppSettings keys)
QuindarEnabled (default False)
QuindarStyle (default Tone; values Tone | Morse)
QuindarLevelDb (default -6)
- Tone style:
QuindarIntroFreqHz (default 2525)
QuindarOutroFreqHz (default 2475)
QuindarDurationMs (default 250, range 100–500)
- Morse style:
QuindarMorseWpm (default 45, range 20–60)
QuindarMorsePitchHz (default 750, range 400–1200)
UI
Right-click MOX → "Quindar..." popover with:
- Enable toggle
- Style segmented control:
[ Tone ] [ Morse ]
- Level slider (-20 … 0 dB)
- Style-conditional fields:
- Tone: intro freq, outro freq, duration spinner
- Morse: WPM spinner (20–60), pitch slider (400–1200 Hz)
No menu-bar entry — keep it tucked under the right-click.
Files touched
| File |
Change |
src/core/ClientQuindarTone.{h,cpp} |
NEW — DSP stage + envelope |
src/core/AudioEngine.{h,cpp} |
Instantiate, plumb into TX-after-PooDoo path, startQuindarIntro/Outro() |
src/models/TransmitModel.{h,cpp} |
requestPttOn/Off, defer-xmit-0 timer, phase atomic |
src/gui/TxApplet.cpp |
MOX button → requestPttOn/Off; right-click popover |
src/core/TciServer.cpp |
Hardware-style trx: → requestPttOn/Off(TciHardware) |
tests/client_quindar_test.cpp |
NEW — frequency, duration, envelope, phase transitions |
Edge cases
- Rapid MOX toggle within outro window — coalesce: re-engaging during outro skips intro and resumes Live (otherwise the user feels a dead zone equal to outro + intro = up to ~800 ms in Morse style).
- Mode change mid-transmission — keep current phase (don't truncate or restart).
- Other client keys our radio (multi-Flex) — observed-only; doesn't trigger our tone (
m_transmitting rising via radio-side status ≠ our PTT request).
- Hardware PTT via radio-side serial PTT — out of scope. Document as MOX-only.
- Disconnect mid-outro — drop pending xmit-0 timer; radio handles unkey via session teardown.
Open UX question
The "MOX button stays lit ~250 ms after release" feels correct, but worth validating on actual footswitch hardware — the streamcontroller plugin reflects MOX state back to the Stream Deck button, so there's a visible delay on the physical button too. If that delay feels off, alternative is to flip MOX visually immediately and only defer the actual xmit 0 (slight UI dishonesty but maybe worth it).
Risks
- Niche feature — many ham contest ops will find Quindar annoying or unprofessional. Ships disabled by default.
- Doesn't compose with hardware serial PTT (radio-side keying happens before AetherSDR sees it). Footswitch via TCI is fine; serial PTT footswitch is not. Document explicitly.
73, Jeremy KK7GWY & Claude (AI dev partner)
Background
Add classic Apollo-era "Quindar" tones (2525 Hz intro, 2475 Hz outro, ~250 ms each) to MOX/PTT engagement on phone modes — fun cosmetic feature for SSB ops who want NASA mission-control aesthetic.
Two flavors selectable per-user:
Tone(default)MorseKat 45 WPM (≈ 240 ms)BKat 45 WPM (≈ 560 ms)Morse style uses the active CW pitch (or a configurable Quindar pitch — TBD) as the carrier, generated with the same envelope as the sine version. Outro
BKis intentionally longer than the 250 ms introKbecause the signoff convention warrants the extra emphasis.Ships disabled by default; opt-in per-user.
Trigger surface — single coordinator
Add
TransmitModel::requestPttOn(source)/requestPttOff(source)as the entry point for all "user wants to key/unkey" intents (wheresource ∈ {Mox, TciHardware, TciDax, …}). ExistingsetMox()/setTransmit()stay as low-level emitters; the coordinator runs the tone state machine before/after them.src/gui/TxApplet.cpp:294— MOX buttonsetMox(on)requestPttOn/Off(Mox)src/core/TciServer.cpp:590— radio-direct (e.g. Elgato footswitch via streamcontroller plugin)setTransmit(true)requestPttOn(TciHardware)src/core/TciServer.cpp:587— DAX path (WSJT-X etc.)startTxChrono()State machine
Engaging/Disengagingdrive an atomicQuindarPhaseread byAudioEngineon the audio thread.DSP —
ClientQuindarToneNew stage in
src/core/ClientQuindarTone.{h,cpp}, lock-free atomic-driven (mirrorsClientGate/ClientTubeshape):K(intro) /BK(outro) at 45 WPM, single carrier frequency (default 750 Hz, configurable). Element timing: dot = 1200/45 ≈ 26.67 ms, dash = 80 ms, intra-char gap = 26.67 ms, inter-letter gap = 80 ms. Each element shaped with the same 5 ms cos² envelope.K= dah-dit-dah → 9 units → ~240 ms totalBK= dah-dit-dit-dit + inter-letter + dah-dit-dah → 21 units → ~560 ms totalAudioEnginealready emits.MOX-off defer
requestPttOff(source):audio->startQuindarOutro()+QTimer::singleShot(outroDurationMs, …, sendPttOff).sendPttOff()immediately (current behavior).outroDurationMsis style-dependent: 250 ms forTone, ~560 ms forMorse BK. Asymmetric outro durations are fine — the timer length is computed at request time from the active style.The MOX button visibly stays lit during the outro because
m_moxdoesn't flip until the timer fires — the "stickiness" is the outro tone duration, which is the correct UX (user sees what's happening).Phone-mode gate
isPhoneMode() = mode ∈ {USB, LSB, AM, FM, NFM, DIGU, DIGL}checked at request time on the active slice. CW / FT8 / DIGITAL → bypass Quindar entirely.Settings (AppSettings keys)
QuindarEnabled(defaultFalse)QuindarStyle(defaultTone; valuesTone | Morse)QuindarLevelDb(default -6)QuindarIntroFreqHz(default 2525)QuindarOutroFreqHz(default 2475)QuindarDurationMs(default 250, range 100–500)QuindarMorseWpm(default 45, range 20–60)QuindarMorsePitchHz(default 750, range 400–1200)UI
Right-click MOX → "Quindar..." popover with:
[ Tone ] [ Morse ]No menu-bar entry — keep it tucked under the right-click.
Files touched
src/core/ClientQuindarTone.{h,cpp}src/core/AudioEngine.{h,cpp}startQuindarIntro/Outro()src/models/TransmitModel.{h,cpp}requestPttOn/Off, defer-xmit-0 timer, phase atomicsrc/gui/TxApplet.cpprequestPttOn/Off; right-click popoversrc/core/TciServer.cpptrx:→requestPttOn/Off(TciHardware)tests/client_quindar_test.cppEdge cases
m_transmittingrising via radio-side status ≠ our PTT request).Open UX question
The "MOX button stays lit ~250 ms after release" feels correct, but worth validating on actual footswitch hardware — the streamcontroller plugin reflects MOX state back to the Stream Deck button, so there's a visible delay on the physical button too. If that delay feels off, alternative is to flip MOX visually immediately and only defer the actual
xmit 0(slight UI dishonesty but maybe worth it).Risks
73, Jeremy KK7GWY & Claude (AI dev partner)