Plan: Aetherial Audio Channel Strip — unified TX DSP window
Goal
Centralize all 7 Aetherial Audio TX DSP control panels into a single
window — the Aetherial Audio Channel Strip — laid out per the
maintainer's concept render (CS-7 "Studio TX Processor"). The
existing per-stage applets and floating editors stay in place during
multi-day iteration so the live UX is never disturbed; once the strip
is feature-complete, the per-stage applets/editors are removed and
the strip becomes the canonical control surface.
Motivation
- TX DSP is currently 7 floating editors + a CHAIN tile. Power users
open 3–4 editors at once and tile them by hand. A single window
removes the tiling work and lets us show the full signal flow.
- The render establishes a "channel strip" mental model that maps
cleanly to the chain order radio engineers already think in.
- The work is multi-day. A duplicate-then-cut-over approach lets us
iterate freely on the strip without touching anything that's
currently working.
Architecture decisions
Coexist via duplication (not extraction)
For each of the 7 stages we copy the existing editor source to a
new Strip*Panel.{h,cpp} file, swap the class name, and leave the
engine wiring identical. The strip embeds the duplicates; the live
applets and floating editors keep using the originals.
Rejected alternative: extract a reusable *Body widget per stage
(the AetherDspWidget-from-AetherDspDialog pattern). Cleaner code
shape long-term, but lands a 7-stage refactor during a multi-day
iteration on visual fit — every dimension tweak risks regressions in
the live editors. Duplication trades a temporary code-doubling tax
for total isolation; cutover is a single delete pass.
Naming: Strip*Panel
StripTubePanel, StripGatePanel, StripEqPanel, StripCompPanel,
StripDeEssPanel, StripPuduPanel, StripReverbPanel. Easy to
grep, distinct from the live Client*Editor, makes the cutover
delete unambiguous.
Launch path: Easter-egg nub
Tiny unlabeled button on the AetherialAudio (tx_dsp) container
header, bottom-right corner, color one shade lighter than the panel
background. Click toggles strip visibility. No menu item, no tooltip,
no label — discoverable only to people who know it's there. When the
strip becomes the canonical surface, the nub graduates to a real
"Channel Strip" button.
Bypass: handled by horizontal CHAIN at the top of the strip
The render shows the chain redrawn as one long horizontal link at
the top of the unified window. We embed (or duplicate, see below)
the existing ClientChainWidget there; per-stage bypass continues
to work via the chain tile's existing single-click semantics.
The stage panels themselves do not sprout bypass affordances in
the first iteration — bypass is a chain-tile concern only. (Some
stages already render a "BYPASS" badge in their editor body when
disabled; that visual state is preserved by the duplicate.)
Stages (mapped to render)
| Render section |
Source editor |
Duplicate target |
| Tube Preamp |
ClientTubeEditor |
StripTubePanel |
| Expander / Gate |
ClientGateEditor |
StripGatePanel |
| 10-Band Parametric EQ |
ClientEqEditor (+ ClientEqEditorCanvas) |
StripEqPanel (+ StripEqCanvas) |
| Compressor |
ClientCompEditor |
StripCompPanel |
| De-Esser |
ClientDeEssEditor |
StripDeEssPanel |
| Exciter · PooDoo |
ClientPuduEditor |
StripPuduPanel |
| Reverb |
ClientReverbEditor |
StripReverbPanel |
Layout (iteration 1)
┌────────────────────────────────────────────────────────────┐
│ [horizontal CHAIN: MIC → TUBE → EQ → DESS → COMP → GATE …] │
├────────────────────────────────────────────────────────────┤
│ Tube Preamp │ Expander / Gate │
├────────────────────────┴────────────────────────────────────┤
│ 10-Band Parametric EQ (full width) │
├────────────────────────┬────────────────────────────────────┤
│ Compressor │ De-Esser │
├────────────────────────┼────────────────────────────────────┤
│ Exciter (PooDoo) │ Reverb │
└────────────────────────┴────────────────────────────────────┘
Footer elements from the render (TX OUTPUT meter, PHASE / DIM /
MASTER / MUTE, S/N decoration, header status LEDs) are iteration
2+ — not part of this plan.
Step-by-step
Step 1 — Launch nub on the AetherialAudio container header
- File:
src/gui/AppletPanel.cpp (and possibly the
ContainerWidget header bar code).
- Add a small
QPushButton (or QToolButton) with no text, fixed
size ~10×10 px, background color one step lighter than the
panel's #0f0f1a (or whatever the current tx_dsp header
background resolves to — check at the styling code).
- Place it at the bottom-right of the
tx_dsp container header,
not in the row with the existing pop-out / close affordances.
- Wire
clicked to a new slot MainWindow::toggleAetherialStrip()
— initially a no-op stub; will be wired to actual strip
show/hide in step 4.
- AppSettings key reserved:
AetherialStripVisible (default
False).
Acceptance: Nub renders; clicking does nothing (stub). No
regression in container drag / pop-out / close behavior. Build clean.
Step 2 — Duplicate the 7 control panels
One commit per stage. Each commit:
- Copies
ClientFooEditor.{h,cpp} to StripFooPanel.{h,cpp}.
- Renames the class everywhere inside (sed-style).
- Adjusts include guards.
- Adds the new files to
CMakeLists.txt.
- Does NOT instantiate the duplicate anywhere — it's dead code
that compiles. No call sites added until step 3.
- Does NOT modify the original editor.
Order suggestion (smallest/safest first to build confidence):
StripReverbPanel
StripPuduPanel
StripDeEssPanel
StripCompPanel
StripGatePanel
StripTubePanel
StripEqPanel (drag along StripEqCanvas since the EQ editor
uses a paint helper)
Acceptance per commit: Build clean; no symbol collisions; no
behavior change in the live applet/editor (since nothing instantiates
the duplicate yet).
Step 3 — Build the AetherialAudioStrip window
- New files:
src/gui/AetherialAudioStrip.{h,cpp}.
class AetherialAudioStrip : public QWidget (toplevel — no parent
for window-manager status; Qt::Window flag).
- Constructor takes
AudioEngine* and RadioModel* (whatever the
underlying editors need — pattern-match on the existing editors'
constructors).
- Internally:
- Vertical layout.
- Top: horizontal
ClientChainWidget (or a strip-local
StripChainWidget duplicate if we want freedom to restyle the
chain layout — likely yes).
- 4 rows beneath, per the layout diagram. Use
QGridLayout with
column-spanned cells for the EQ row.
- Each cell holds one
Strip*Panel.
- Window title:
"Aetherial Audio — Channel Strip".
- Persists size/position via AppSettings keys
AetherialStripGeometry, AetherialStripVisible.
Acceptance: AetherialAudioStrip exists and can be
manually instantiated and shown; all 7 stage panels render; engine
parameter changes round-trip both ways (strip ↔ engine ↔ live editor)
because both surfaces share the same AudioEngine atomics.
Step 4 — Wire the nub
MainWindow::toggleAetherialStrip() lazy-creates the strip and
toggles visibility.
- Restore
AetherialStripVisible on startup.
- Save geometry on close.
Acceptance: Click nub → strip appears. Click nub again → strip
hides. Quit + relaunch with strip visible → strip reopens at last
position/size. No regression to per-stage applets/editors.
Step 5 — Iterate dimensions, fit, polish
This is the multi-day phase.
- Tune knob sizes / label widths / row heights inside each
Strip*Panel for the unified layout.
- Add
setCompactMode(bool) per panel as needed (mirroring the
AetherDspWidget pattern).
- Adjust grid stretches and minimum sizes on
AetherialAudioStrip.
- Visual fixes only — no behavior changes, no engine wiring changes.
- Ship-as-you-go: small PRs, each gated on the nub still working
and the live applets/editors still untouched.
Acceptance: Strip looks and behaves like the concept render at
its target window size; all controls are reachable; nothing
overflows; per-stage signal flow is correct.
Step 6 — Cutover (separate plan, not in this issue)
Out of scope for this issue — tracked separately when iteration 5
converges. Sketch:
- Delete
Client*Applet and Client*Editor files.
- Drop them from
AppletPanel's tx_dsp container.
- Promote the nub to a real "Channel Strip" toolbar / menu entry.
- Migrate any AppSettings keys that lived on the per-stage widgets
if they aren't already engine-side.
Risks
- Code doubling during iteration. Bug fixes to engine-side
signal handling may need to be mirrored to both live editor and
strip duplicate if they touch widget code (rare; most engine
fixes are in AudioEngine itself, which both surfaces share).
Mitigation: keep iteration phase short.
- EQ canvas duplication.
ClientEqEditor uses a paint helper
(ClientEqEditorCanvas). The duplicate must clone both. Verify
no static state that would be shared incorrectly.
- Window-state persistence collisions. Strip geometry keys must
not collide with existing applet pop-out keys.
- Nub discoverability. Intentional Easter egg; document in the
cutover plan when promoting it.
Out of scope for iteration 1
- Footer (TX OUTPUT meter / PHASE / DIM / MASTER / MUTE).
- Header status LEDs (SIG / TX OUT / POWER / CLIP / BYPASS).
- "2-STAGE CHAIN" preset selector.
- LIMIT button on compressor (we don't have a separate limiter
stage).
- AIR knob on de-esser (if not already a control on the existing
editor).
- S/N decoration.
- Any rebranding (CS-7 / Studio TX Processor labels).
- Removal of per-stage applets/editors (step 6, separate issue).
🤖 Plan drafted by Claude. Local copy at ~/.claude/plans/aetherial-audio-strip-unified-window.md.
73, Jeremy KK7GWY & Claude (AI dev partner)
Plan: Aetherial Audio Channel Strip — unified TX DSP window
Goal
Centralize all 7 Aetherial Audio TX DSP control panels into a single
window — the Aetherial Audio Channel Strip — laid out per the
maintainer's concept render (CS-7 "Studio TX Processor"). The
existing per-stage applets and floating editors stay in place during
multi-day iteration so the live UX is never disturbed; once the strip
is feature-complete, the per-stage applets/editors are removed and
the strip becomes the canonical control surface.
Motivation
open 3–4 editors at once and tile them by hand. A single window
removes the tiling work and lets us show the full signal flow.
cleanly to the chain order radio engineers already think in.
iterate freely on the strip without touching anything that's
currently working.
Architecture decisions
Coexist via duplication (not extraction)
For each of the 7 stages we copy the existing editor source to a
new
Strip*Panel.{h,cpp}file, swap the class name, and leave theengine wiring identical. The strip embeds the duplicates; the live
applets and floating editors keep using the originals.
Rejected alternative: extract a reusable
*Bodywidget per stage(the
AetherDspWidget-from-AetherDspDialogpattern). Cleaner codeshape long-term, but lands a 7-stage refactor during a multi-day
iteration on visual fit — every dimension tweak risks regressions in
the live editors. Duplication trades a temporary code-doubling tax
for total isolation; cutover is a single delete pass.
Naming:
Strip*PanelStripTubePanel,StripGatePanel,StripEqPanel,StripCompPanel,StripDeEssPanel,StripPuduPanel,StripReverbPanel. Easy togrep, distinct from the live
Client*Editor, makes the cutoverdelete unambiguous.
Launch path: Easter-egg nub
Tiny unlabeled button on the AetherialAudio (
tx_dsp) containerheader, bottom-right corner, color one shade lighter than the panel
background. Click toggles strip visibility. No menu item, no tooltip,
no label — discoverable only to people who know it's there. When the
strip becomes the canonical surface, the nub graduates to a real
"Channel Strip" button.
Bypass: handled by horizontal CHAIN at the top of the strip
The render shows the chain redrawn as one long horizontal link at
the top of the unified window. We embed (or duplicate, see below)
the existing
ClientChainWidgetthere; per-stage bypass continuesto work via the chain tile's existing single-click semantics.
The stage panels themselves do not sprout bypass affordances in
the first iteration — bypass is a chain-tile concern only. (Some
stages already render a "BYPASS" badge in their editor body when
disabled; that visual state is preserved by the duplicate.)
Stages (mapped to render)
ClientTubeEditorStripTubePanelClientGateEditorStripGatePanelClientEqEditor(+ClientEqEditorCanvas)StripEqPanel(+StripEqCanvas)ClientCompEditorStripCompPanelClientDeEssEditorStripDeEssPanelClientPuduEditorStripPuduPanelClientReverbEditorStripReverbPanelLayout (iteration 1)
Footer elements from the render (TX OUTPUT meter, PHASE / DIM /
MASTER / MUTE, S/N decoration, header status LEDs) are iteration
2+ — not part of this plan.
Step-by-step
Step 1 — Launch nub on the AetherialAudio container header
src/gui/AppletPanel.cpp(and possibly theContainerWidgetheader bar code).QPushButton(orQToolButton) with no text, fixedsize ~10×10 px, background color one step lighter than the
panel's
#0f0f1a(or whatever the currenttx_dspheaderbackground resolves to — check at the styling code).
tx_dspcontainer header,not in the row with the existing pop-out / close affordances.
clickedto a new slotMainWindow::toggleAetherialStrip()— initially a no-op stub; will be wired to actual strip
show/hide in step 4.
AetherialStripVisible(defaultFalse).Acceptance: Nub renders; clicking does nothing (stub). No
regression in container drag / pop-out / close behavior. Build clean.
Step 2 — Duplicate the 7 control panels
One commit per stage. Each commit:
ClientFooEditor.{h,cpp}toStripFooPanel.{h,cpp}.CMakeLists.txt.that compiles. No call sites added until step 3.
Order suggestion (smallest/safest first to build confidence):
StripReverbPanelStripPuduPanelStripDeEssPanelStripCompPanelStripGatePanelStripTubePanelStripEqPanel(drag alongStripEqCanvassince the EQ editoruses a paint helper)
Acceptance per commit: Build clean; no symbol collisions; no
behavior change in the live applet/editor (since nothing instantiates
the duplicate yet).
Step 3 — Build the
AetherialAudioStripwindowsrc/gui/AetherialAudioStrip.{h,cpp}.class AetherialAudioStrip : public QWidget(toplevel — no parentfor window-manager status;
Qt::Windowflag).AudioEngine*andRadioModel*(whatever theunderlying editors need — pattern-match on the existing editors'
constructors).
ClientChainWidget(or a strip-localStripChainWidgetduplicate if we want freedom to restyle thechain layout — likely yes).
QGridLayoutwithcolumn-spanned cells for the EQ row.
Strip*Panel."Aetherial Audio — Channel Strip".AetherialStripGeometry,AetherialStripVisible.Acceptance:
AetherialAudioStripexists and can bemanually instantiated and shown; all 7 stage panels render; engine
parameter changes round-trip both ways (strip ↔ engine ↔ live editor)
because both surfaces share the same
AudioEngineatomics.Step 4 — Wire the nub
MainWindow::toggleAetherialStrip()lazy-creates the strip andtoggles visibility.
AetherialStripVisibleon startup.Acceptance: Click nub → strip appears. Click nub again → strip
hides. Quit + relaunch with strip visible → strip reopens at last
position/size. No regression to per-stage applets/editors.
Step 5 — Iterate dimensions, fit, polish
This is the multi-day phase.
Strip*Panelfor the unified layout.setCompactMode(bool)per panel as needed (mirroring theAetherDspWidgetpattern).AetherialAudioStrip.and the live applets/editors still untouched.
Acceptance: Strip looks and behaves like the concept render at
its target window size; all controls are reachable; nothing
overflows; per-stage signal flow is correct.
Step 6 — Cutover (separate plan, not in this issue)
Out of scope for this issue — tracked separately when iteration 5
converges. Sketch:
Client*AppletandClient*Editorfiles.AppletPanel's tx_dsp container.if they aren't already engine-side.
Risks
signal handling may need to be mirrored to both live editor and
strip duplicate if they touch widget code (rare; most engine
fixes are in
AudioEngineitself, which both surfaces share).Mitigation: keep iteration phase short.
ClientEqEditoruses a paint helper(
ClientEqEditorCanvas). The duplicate must clone both. Verifyno static state that would be shared incorrectly.
not collide with existing applet pop-out keys.
cutover plan when promoting it.
Out of scope for iteration 1
stage).
editor).
🤖 Plan drafted by Claude. Local copy at
~/.claude/plans/aetherial-audio-strip-unified-window.md.73, Jeremy KK7GWY & Claude (AI dev partner)