fix(dax): quote pipe source/sink properties so node descriptions survive pipewire-pulse#3438
Conversation
…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
There was a problem hiding this comment.
Looks good — clean, minimal fix with thorough evidence.
A few things I verified:
runPactlcallsQProcess::start("pactl", args), so eachQStringListentry 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/sinkDescare 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_streampath (PipeWireNativeRxSource) usesPW_KEY_NODE_DESCRIPTIONdirectly and is unaffected, as noted — this only repairs the legacymodule-pipe-source/sinkfallback, which is the path TX always takes and RX takes whenlibpipewire-0.3is 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
There was a problem hiding this comment.
Thanks for the careful writeup — the empirical A/B table with pw-cli readback makes this very easy to verify. LGTM.
Confirmed locally:
runPactl()invokespactldirectly viaQProcess::start("pactl", args)(no shell), so the single quotes are passed literally topactl/pulse-module parser — which is the intended behavior here. PulseAudio'spa_modargsparser is the layer that respects the outer'…'for grouping, exactly as the patch description claims.sourceDesc/sinkDescare 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
pactlargv lines inPipeWireAudioBridge.cpp; no behavior change for the nativepw_streampath or for build configurations withlibpipewire-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
## 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`).
…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 **Playback** — `AetherSDR TX` selectable by name:  qpwgraph — named pipe nodes (`AetherSDR DAX 1/2/4`, `AetherSDR TX`; pre-fix all read `AetherSDR`):  ## 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).
…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`).
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 createdthrough the legacy
pactl module-pipe-source/module-pipe-sinkpath. That path passes theper-channel name as
source_properties=device.description="AetherSDR DAX N", butpipewire-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.descriptioncollapses to"AetherSDR"for every DAX RX nodeand the TX node.
node.descriptionis derived fromdevice.description, so qpwgraph, JACKpatchbays, WSJT-X and FLDIGI all show an indistinguishable
"AetherSDR"and operators musthand-route by guesswork.
The same truncation also orphans the trailing
node.latency=256/48000quantum hint (#1008),which never reaches the node.
Fix
Wrap the property value in single quotes so the spaced
device.description(and thenode.latencyhint) survive the split.node.descriptionis then derived correctly. 2 lines:The native
pw_streampath (PipeWireNativeRxSource) already setsPW_KEY_NODE_DESCRIPTIONvia 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-clion pipewire-pulse:source_propertiesvalue emittednode.descriptionnode.latencydevice.description="AetherSDR DAX 1" node.latency=256/48000(before)AetherSDRdevice.description=AetherSDR\ DAX\ 1(backslash)AetherSDR'device.description="AetherSDR DAX 1" node.latency=256/48000'(fix)AetherSDR DAX 1256/48000Test plan
ninja -C buildclean (Nobara/FC43, Qt 6.10.3, gcc 15.2.1; native PipeWire off)pw-cli ls Node:aethersdr-dax-1..4→AetherSDR DAX 1..4,aethersdr-tx→AetherSDR TXAetherSDR DAX 1..4, PlaybackAetherSDR TX(screenshots)device.description)Screenshots (patched build, live on a FLEX-6700, Linux/pipewire-pulse)
FLDIGI → Soundcard/Devices — Capture lists each DAX channel by name:
FLDIGI Playback —
AetherSDR TXselectable by name:qpwgraph — named pipe nodes (
AetherSDR DAX 1/2/4,AetherSDR TX; pre-fix all readAetherSDR):Related
node.latency=256/48000quantum hint this fix un-orphans.node.latency=256/48000token to this exact RX line (latency work) but leftthe value unquoted; this fix makes both that hint and the spaced
device.descriptionsurvive.It deliberately kept the legacy
module-pipe-source/sinkfallback that this patch repairs.Notes / out of scope
node.nickis intentionally not set (the native path doesn't set it either;node.descriptionis what qpwgraph/JACK display).
media.classStream/Output/Audio/Stream/Input/Audio) still carry the genericnode.name="AetherSDR"with no
node.description, so they appear asAetherSDR/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.cppbuilds itspactlcommand with the same backslash-escaped-space form (viabash -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).