Skip to content

fix(dax): quote pipe source/sink properties so node descriptions survive pipewire-pulse#3438

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
Ozy311:fix/dax-pipewire-node-names
Jun 7, 2026
Merged

fix(dax): quote pipe source/sink properties so node descriptions survive pipewire-pulse#3438
ten9876 merged 1 commit into
aethersdr:mainfrom
Ozy311:fix/dax-pipewire-node-names

Conversation

@Ozy311

@Ozy311 Ozy311 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

fix(dax): quote pipe source/sink properties so node descriptions survive pipewire-pulse

Summary

On builds without libpipewire-0.3 (native PipeWire off), DAX audio nodes are created
through the legacy pactl module-pipe-source / module-pipe-sink path. That path passes the
per-channel name as source_properties=device.description="AetherSDR DAX N", but
pipewire-pulse's pulse-module argument parser splits the property value on unescaped
whitespace and does not honor the embedded quotes for grouping.
The value is taken only up
to the first space, so device.description collapses to "AetherSDR" for every DAX RX node
and the TX node. node.description is derived from device.description, so qpwgraph, JACK
patchbays, WSJT-X and FLDIGI all show an indistinguishable "AetherSDR" and operators must
hand-route by guesswork.

The same truncation also orphans the trailing node.latency=256/48000 quantum hint (#1008),
which never reaches the node.

Fix

Wrap the property value in single quotes so the spaced device.description (and the
node.latency hint) survive the split. node.description is then derived correctly. 2 lines:

- QStringLiteral("source_properties=device.description=\"%1\" node.latency=256/48000").arg(sourceDesc),
+ QStringLiteral("source_properties='device.description=\"%1\" node.latency=256/48000'").arg(sourceDesc),
- QStringLiteral("sink_properties=device.description=\"%1\"").arg(sinkDesc),
+ QStringLiteral("sink_properties='device.description=\"%1\"'").arg(sinkDesc),

The native pw_stream path (PipeWireNativeRxSource) already sets PW_KEY_NODE_DESCRIPTION
via the API and is unaffected — this aligns the legacy fallback (and TX, which always uses the
legacy sink) with it.

Why (quoting A/B)

Exact argv the code emits, read back with pw-cli on pipewire-pulse:

source_properties value emitted node.description node.latency
device.description="AetherSDR DAX 1" node.latency=256/48000 (before) AetherSDR dropped
device.description=AetherSDR\ DAX\ 1 (backslash) AetherSDR n/a
'device.description="AetherSDR DAX 1" node.latency=256/48000' (fix) AetherSDR DAX 1 256/48000

Test plan

  • Incremental ninja -C build clean (Nobara/FC43, Qt 6.10.3, gcc 15.2.1; native PipeWire off)
  • Live against a FLEX-6700, DAX enabled — pw-cli ls Node:
    aethersdr-dax-1..4AetherSDR DAX 1..4, aethersdr-txAetherSDR TX
  • qpwgraph shows the named pipe nodes (screenshot)
  • FLDIGI Soundcard/Devices (PortAudio): Capture lists AetherSDR DAX 1..4, Playback AetherSDR TX (screenshots)
  • WSJT-X device list (maintainer/secondary — PulseAudio names follow the same device.description)

Screenshots (patched build, live on a FLEX-6700, Linux/pipewire-pulse)

FLDIGI → Soundcard/Devices — Capture lists each DAX channel by name:

FLDIGI Capture list

FLDIGI PlaybackAetherSDR TX selectable by name:

FLDIGI Playback list

qpwgraph — named pipe nodes (AetherSDR DAX 1/2/4, AetherSDR TX; pre-fix all read AetherSDR):

qpwgraph

Related

Notes / out of scope

  • node.nick is intentionally not set (the native path doesn't set it either; node.description
    is what qpwgraph/JACK display).
  • Known related (separate follow-up): the app's own RX/TX audio streams (media.class
    Stream/Output/Audio / Stream/Input/Audio) still carry the generic node.name="AetherSDR"
    with no node.description, so they appear as AetherSDR / AetherSDR-<id> in device lists.
    Different code path (main audio engine, not the DAX bridge) — tracked in App's own RX/TX audio streams show as generic "AetherSDR" (no node.description) in device lists #3440.
  • DaxIqModel.cpp builds its pactl command with the same backslash-escaped-space form (via
    bash -c), so DAX-IQ descriptions are likely truncated the same way — separate follow-up.

Closes #3437


73, Ozy K6OZY
AI compute partnership: cloaked.agency — drafted with Don, our VP of Engineering agent, on the Linux dev stack (nobara-dell: Nobara/FC43, KDE Plasma 6.6.4, Qt 6.10.3, kernel 7.0.9, Wayland; Dell Precision 5570).

…ive pipewire-pulse

The legacy module-pipe-source/sink path passes the channel name via
source_properties=device.description="AetherSDR DAX N", but pipewire-pulse's
pulse-module argument parser splits the property value on unescaped whitespace
and does not honor the embedded quotes for grouping. The value is taken only up
to the first space, so device.description collapses to "AetherSDR" for every DAX
RX node and the TX node, and the trailing node.latency=256/48000 hint (aethersdr#1008) is
orphaned. node.description is derived from device.description, so qpwgraph, JACK
patchbays, WSJT-X and FLDIGI all show an indistinguishable "AetherSDR".

Wrap the property value in single quotes so the spaced device.description (and
node.latency) survive the split. node.description is then derived correctly: DAX
RX nodes show "AetherSDR DAX 1".."DAX 4" and TX shows "AetherSDR TX". Verified
live against a FLEX-6700 (pw-cli + qpwgraph + FLDIGI PortAudio device list).

The native pw_stream path (PipeWireNativeRxSource) already sets node.description
via the API and is unaffected; this fixes the legacy fallback (and TX, which
always uses the legacy sink).

Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
@Ozy311 Ozy311 marked this pull request as ready for review June 6, 2026 19:44
@Ozy311 Ozy311 requested a review from a team as a code owner June 6, 2026 19:44

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

Looks good — clean, minimal fix with thorough evidence.

A few things I verified:

  • runPactl calls QProcess::start("pactl", args), so each QStringList entry is its own argv element with no shell expansion. The single quotes therefore reach pipewire-pulse's module-arg parser as literal characters, where they group the spaced value — exactly matching your A/B table. ✅
  • sourceDesc / sinkDesc are built from a fixed literal plus an integer index, so there's no quote-injection surface to worry about from the description string itself.
  • The native pw_stream path (PipeWireNativeRxSource) uses PW_KEY_NODE_DESCRIPTION directly and is unaffected, as noted — this only repairs the legacy module-pipe-source/sink fallback, which is the path TX always takes and RX takes when libpipewire-0.3 is off.
  • The inline comments are the right amount of context — future readers will know why those single quotes are load-bearing instead of "stylistic."

The node.latency=256/48000 un-orphaning is a nice incidental win on top of the description fix (#1008).

The DaxIqModel.cpp follow-up note and the separate node.name=AetherSDR issue (#3440) are correctly scoped out of this PR.

Thanks for the careful write-up, the pw-cli readback table, and the screenshots — this was easy to review. 73!

— AetherClaude


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

@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 for the careful writeup — the empirical A/B table with pw-cli readback makes this very easy to verify. LGTM.

Confirmed locally:

  • runPactl() invokes pactl directly via QProcess::start("pactl", args) (no shell), so the single quotes are passed literally to pactl/pulse-module parser — which is the intended behavior here. PulseAudio's pa_modargs parser is the layer that respects the outer '…' for grouping, exactly as the patch description claims.
  • sourceDesc / sinkDesc are constructed from a hardcoded prefix plus an integer ("AetherSDR DAX %1" / "AetherSDR TX"), so there's no injection / quote-escape concern in the formatted value.
  • Change is scoped strictly to the two pactl argv lines in PipeWireAudioBridge.cpp; no behavior change for the native pw_stream path or for build configurations with libpipewire-0.3.
  • Comments anchor the why (parser splits on whitespace) at both call sites, which is exactly where a future reader would otherwise re-introduce the bug.

Nice catch on the orphaned node.latency=256/48000 token too — that's a quiet regression from #2312 that this same edit un-breaks.

73 and thanks for the contribution!


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

@ten9876 ten9876 self-assigned this Jun 7, 2026
@ten9876 ten9876 merged commit b245b1d into aethersdr:main Jun 7, 2026
6 checks passed
NF0T pushed a commit that referenced this pull request Jun 13, 2026
## Summary
The CW sidetone uses the PortAudio backend, which on Linux runs over
PipeWire's JACK API and registers a JACK client under PortAudio's
hardcoded default name **`PortAudio`**. The sidetone sink opens
alongside RX and stays open for the whole RX session, so qpwgraph / JACK
patchbays / WSJT-X / FLDIGI show a persistent node literally named
`PortAudio` with no indication it belongs to AetherSDR — the most opaque
label of any AetherSDR audio node.

## Fix
Call `PaJack_SetClientName("AetherSDR CW Sidetone")` immediately before
`Pa_Initialize()`. The `pa_jack` contract requires the call precede
`Pa_Initialize` and references (does not copy) the string, so it uses a
static-lifetime `const char[]`. Guarded with `#if defined(Q_OS_LINUX) &&
__has_include(<pa_jack.h>)` so macOS CoreAudio / Windows WASAPI builds
(no `pa_jack.h`, no JACK node) compile unaffected. CW sidetone is
AetherSDR's only PortAudio user, so naming the process-global JACK
client is unambiguous.

## Why
- Completes the "every AetherSDR audio node is identifiable in
patchbays" line of work alongside the merged DAX/TX pipe-naming fix
(#3438) and the still-open main RX/TX stream naming (#3440).
- A node named `PortAudio` is indistinguishable from any other PortAudio
app and confuses routing in WSJT-X / FLDIGI device pickers.

## Scope
- 13 lines added in one file (`src/core/CwSidetonePortAudioSink.cpp`).
- Linux/PipeWire-only effect (guarded); no protocol, persistence, or UX
change.

## Test plan
- [x] Local incremental `ninja -C build` clean on Linux (Nobara 43, Qt
6.10.3, gcc 15.2.1).
- [x] **Live A/B on a FLEX-6700 (RX-only), `pw-dump` ground truth:**
- Baseline (upstream/main `753eb689`): sidetone node `client.name` /
`node.name` / `node.description` all read **`PortAudio`**.
  - This branch: all three read **`AetherSDR CW Sidetone`**.
- [x] macOS CoreAudio / Windows WASAPI: compiles unaffected (guard
excludes the call; no JACK node on those backends) —
maintainer/secondary.

Related: #3438, #3440

---

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`).
@Ozy311 Ozy311 deleted the fix/dax-pipewire-node-names branch June 13, 2026 23:27
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…ive pipewire-pulse (aethersdr#3438)

fix(dax): quote pipe source/sink properties so node descriptions survive
pipewire-pulse

## Summary
On builds without `libpipewire-0.3` (native PipeWire off), DAX audio
nodes are created
through the legacy `pactl module-pipe-source` / `module-pipe-sink` path.
That path passes the
per-channel name as `source_properties=device.description="AetherSDR DAX
N"`, but
**pipewire-pulse's pulse-module argument parser splits the property
value on unescaped
whitespace and does not honor the embedded quotes for grouping.** The
value is taken only up
to the first space, so `device.description` collapses to `"AetherSDR"`
for every DAX RX node
and the TX node. `node.description` is derived from
`device.description`, so qpwgraph, JACK
patchbays, WSJT-X and FLDIGI all show an indistinguishable `"AetherSDR"`
and operators must
hand-route by guesswork.

The same truncation also orphans the trailing `node.latency=256/48000`
quantum hint (aethersdr#1008),
which never reaches the node.

## Fix
Wrap the property value in single quotes so the spaced
`device.description` (and the
`node.latency` hint) survive the split. `node.description` is then
derived correctly. 2 lines:

```diff
- QStringLiteral("source_properties=device.description=\"%1\" node.latency=256/48000").arg(sourceDesc),
+ QStringLiteral("source_properties='device.description=\"%1\" node.latency=256/48000'").arg(sourceDesc),
- QStringLiteral("sink_properties=device.description=\"%1\"").arg(sinkDesc),
+ QStringLiteral("sink_properties='device.description=\"%1\"'").arg(sinkDesc),
```

The native `pw_stream` path (`PipeWireNativeRxSource`) already sets
`PW_KEY_NODE_DESCRIPTION`
via the API and is unaffected — this aligns the legacy fallback (and TX,
which always uses the
legacy sink) with it.

## Why (quoting A/B)
Exact argv the code emits, read back with `pw-cli` on pipewire-pulse:

| `source_properties` value emitted | `node.description` |
`node.latency` |

|-------------------------------------------------------------------------|--------------------|----------------|
| `device.description="AetherSDR DAX 1" node.latency=256/48000` (before)
| `AetherSDR` | dropped |
| `device.description=AetherSDR\ DAX\ 1` (backslash) | `AetherSDR` | n/a
|
| `'device.description="AetherSDR DAX 1" node.latency=256/48000'` (fix)
| `AetherSDR DAX 1` | `256/48000` |

## Test plan
- [x] Incremental `ninja -C build` clean (Nobara/FC43, Qt 6.10.3, gcc
15.2.1; native PipeWire off)
- [x] Live against a FLEX-6700, DAX enabled — `pw-cli ls Node`:
`aethersdr-dax-1..4` → `AetherSDR DAX 1..4`, `aethersdr-tx` → `AetherSDR
TX`
- [x] qpwgraph shows the named pipe nodes (screenshot)
- [x] FLDIGI Soundcard/Devices (PortAudio): Capture lists `AetherSDR DAX
1..4`, Playback `AetherSDR TX` (screenshots)
- [x] WSJT-X device list (maintainer/secondary — PulseAudio names follow
the same `device.description`)

## Screenshots (patched build, live on a FLEX-6700,
Linux/pipewire-pulse)
FLDIGI → Soundcard/Devices — **Capture** lists each DAX channel by name:

![FLDIGI Capture
list](https://raw.githubusercontent.com/Ozy311/AetherSDR/pr3438-assets/pr-capture-list.png)

FLDIGI **Playback** — `AetherSDR TX` selectable by name:

![FLDIGI Playback
list](https://raw.githubusercontent.com/Ozy311/AetherSDR/pr3438-assets/pr-playback-list.png)

qpwgraph — named pipe nodes (`AetherSDR DAX 1/2/4`, `AetherSDR TX`;
pre-fix all read `AetherSDR`):


![qpwgraph](https://raw.githubusercontent.com/Ozy311/AetherSDR/pr3438-assets/qpwgraph-crop.png)

## Related
- aethersdr#1008 — DAX RX latency; the `node.latency=256/48000` quantum hint this
fix un-orphans.
- aethersdr#2312 — added the `node.latency=256/48000` token to this exact RX line
(latency work) but left
the value unquoted; this fix makes both that hint and the spaced
`device.description` survive.
It deliberately kept the legacy `module-pipe-source/sink` fallback that
this patch repairs.

## Notes / out of scope
- `node.nick` is intentionally not set (the native path doesn't set it
either; `node.description`
  is what qpwgraph/JACK display).
- Known related (separate follow-up): the app's own RX/TX audio streams
(`media.class`
`Stream/Output/Audio` / `Stream/Input/Audio`) still carry the generic
`node.name="AetherSDR"`
with no `node.description`, so they appear as `AetherSDR` /
`AetherSDR-<id>` in device lists.
Different code path (main audio engine, not the DAX bridge) — tracked in
aethersdr#3440.
- `DaxIqModel.cpp` builds its `pactl` command with the same
backslash-escaped-space form (via
`bash -c`), so DAX-IQ descriptions are likely truncated the same way —
separate follow-up.

Closes aethersdr#3437

---

73, Ozy **K6OZY**
AI compute partnership: [cloaked.agency](https://cloaked.agency) —
drafted with Don, our VP of Engineering agent, on the Linux dev stack
(`nobara-dell`: Nobara/FC43, KDE Plasma 6.6.4, Qt 6.10.3, kernel 7.0.9,
Wayland; Dell Precision 5570).
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…hersdr#3517)

## Summary
The CW sidetone uses the PortAudio backend, which on Linux runs over
PipeWire's JACK API and registers a JACK client under PortAudio's
hardcoded default name **`PortAudio`**. The sidetone sink opens
alongside RX and stays open for the whole RX session, so qpwgraph / JACK
patchbays / WSJT-X / FLDIGI show a persistent node literally named
`PortAudio` with no indication it belongs to AetherSDR — the most opaque
label of any AetherSDR audio node.

## Fix
Call `PaJack_SetClientName("AetherSDR CW Sidetone")` immediately before
`Pa_Initialize()`. The `pa_jack` contract requires the call precede
`Pa_Initialize` and references (does not copy) the string, so it uses a
static-lifetime `const char[]`. Guarded with `#if defined(Q_OS_LINUX) &&
__has_include(<pa_jack.h>)` so macOS CoreAudio / Windows WASAPI builds
(no `pa_jack.h`, no JACK node) compile unaffected. CW sidetone is
AetherSDR's only PortAudio user, so naming the process-global JACK
client is unambiguous.

## Why
- Completes the "every AetherSDR audio node is identifiable in
patchbays" line of work alongside the merged DAX/TX pipe-naming fix
(aethersdr#3438) and the still-open main RX/TX stream naming (aethersdr#3440).
- A node named `PortAudio` is indistinguishable from any other PortAudio
app and confuses routing in WSJT-X / FLDIGI device pickers.

## Scope
- 13 lines added in one file (`src/core/CwSidetonePortAudioSink.cpp`).
- Linux/PipeWire-only effect (guarded); no protocol, persistence, or UX
change.

## Test plan
- [x] Local incremental `ninja -C build` clean on Linux (Nobara 43, Qt
6.10.3, gcc 15.2.1).
- [x] **Live A/B on a FLEX-6700 (RX-only), `pw-dump` ground truth:**
- Baseline (upstream/main `753eb689`): sidetone node `client.name` /
`node.name` / `node.description` all read **`PortAudio`**.
  - This branch: all three read **`AetherSDR CW Sidetone`**.
- [x] macOS CoreAudio / Windows WASAPI: compiles unaffected (guard
excludes the call; no JACK node on those backends) —
maintainer/secondary.

Related: aethersdr#3438, aethersdr#3440

---

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.

DAX/TX PipeWire nodes all show node.description "AetherSDR" — spaced device.description is truncated on the legacy pipe path

2 participants