| 229 | } |
| 230 | |
| 231 | bool PipeWireAudioBridge::loadPipeSource(int index) |
| 232 | { |
| 233 | // FIFO lives in the per-user XDG runtime directory with mode 0600. |
| 234 | // Pre-fix code used /tmp/aethersdr-dax-%1.pipe with mode 0666, which |
| 235 | // any local user could read (operator RX audio) or write (operator |
| 236 | // TX audio with the licensed callsign — an FCC liability). |
| 237 | // See GHSA-x8xf-4g5v-ppf9. |
| 238 | const QString fifoDir = daxFifoDir(); |
| 239 | if (fifoDir.isEmpty()) return false; |
| 240 | auto pipePath = QStringLiteral("%1/dax-%2.pipe").arg(fifoDir).arg(index + 1); |
| 241 | auto sourceName = QStringLiteral("aethersdr-dax-%1").arg(index + 1); |
| 242 | auto sourceDesc = QStringLiteral("AetherSDR DAX %1").arg(index + 1); |
| 243 | |
| 244 | if (!makeOwnedFifo(pipePath)) return false; |
| 245 | |
| 246 | // Load PulseAudio pipe-source module. |
| 247 | // Format/rate match the PipeWire graph (48 kHz float32) so PipeWire does |
| 248 | // not insert a resampler on this node — that resampler is the dominant |
| 249 | // remaining latency source after pipe_size was reduced (see issue #1008). |
| 250 | // node.latency=256/48000 (~5.3 ms quantum) tells PipeWire to negotiate a |
| 251 | // small quantum for this node instead of falling back to clock.quantum-limit. |
| 252 | uint32_t modIdx = runPactl({ |
| 253 | "load-module", "module-pipe-source", |
| 254 | QStringLiteral("file=%1").arg(pipePath), |
| 255 | QStringLiteral("source_name=%1").arg(sourceName), |
| 256 | // Single-quoted: the pipewire-pulse pulse-module argument parser splits |
| 257 | // the property value on unescaped whitespace, so unquoted the space in |
| 258 | // "AetherSDR DAX N" truncates device.description to "AetherSDR" and |
| 259 | // orphans node.latency. node.description is derived from |
| 260 | // device.description, so this also labels the node in qpwgraph / JACK. |
| 261 | QStringLiteral("source_properties='device.description=\"%1\" node.latency=256/48000'").arg(sourceDesc), |
| 262 | QStringLiteral("format=float32le"), |
| 263 | QStringLiteral("rate=%1").arg(PIPE_RATE), |
| 264 | QStringLiteral("channels=%1").arg(PIPE_CHANNELS), |
| 265 | }); |
| 266 | |
| 267 | if (modIdx == 0) { |
| 268 | ::unlink(pipePath.toUtf8().constData()); |
| 269 | return false; |
| 270 | } |
| 271 | |
| 272 | // Open the pipe for writing (non-blocking to avoid hanging if no reader) |
| 273 | int fd = ::open(pipePath.toUtf8().constData(), O_WRONLY | O_NONBLOCK); |
| 274 | if (fd < 0) { |
| 275 | qCWarning(lcDax) << "PipeWireAudioBridge: open pipe failed:" << strerror(errno); |
| 276 | runPactl({"unload-module", QString::number(modIdx)}); |
| 277 | ::unlink(pipePath.toUtf8().constData()); |
| 278 | return false; |
| 279 | } |
| 280 | |
| 281 | // Cap kernel FIFO capacity so back-pressure surfaces quickly instead of |
| 282 | // accumulating ~1.3 s of audio in the default 64 KB pipe buffer. |
| 283 | if (::fcntl(fd, F_SETPIPE_SZ, PIPE_KERNEL_BUF) < 0) { |
| 284 | qCDebug(lcDax) << "PipeWireAudioBridge: F_SETPIPE_SZ failed (non-fatal):" << strerror(errno); |
| 285 | } |
| 286 | |
| 287 | m_rx[index].fd = fd; |
| 288 | m_rx[index].moduleIndex = modIdx; |
nothing calls this directly
no test coverage detected