Controller audio: mic + speaker over the Satellite protocol - #48
Conversation
…lamp The client half of the protocol-2 controller-audio extension (MSG_MIC_AUDIO 0x0012 / MSG_SPEAKER_AUDIO 0x0013 / MSG_MIC_LED 0x0014), ported from dish-windows feat/controller-audio (389134b wave 1 + 4218a0a wave 2) in one pass; dish-android shipped the reference client. The shared audio cores land byte-identical with the windows copies: AudioJitter.h is another mirror of the satellite's reorder window (same names on purpose - edit them together), the Opus wrapper pins the same fixed formats (mic mono VOIP 32 kbps FEC+DTX, speaker stereo AUDIO 96 kbps FEC), and the matcher, engine policy, host verdict, stores, engines and their tests diff clean against windows except where this platform genuinely differs. What differs, and why: - The matcher's pad string is the USB `product` attribute (iProduct) read from sysfs, not HID_NAME: the kernel builds HID_NAME as "<manufacturer> <product>", and that longer string is contained in no pipewire/pulse endpoint name, while iProduct is exactly the string the audio stack derives its device names from - and what Windows and android enumerate, so a pad also shows one name across clients now. HID_NAME stays as the fallback for a device with no product string. - The Moonlight strip needs no new chokepoint: buttons fold through moonmap::toMoonlightButtons, an explicit per-bit map with no row for 0x0800, so the mic-mute state bit structurally cannot leak to a GameStream host. A pin test keeps that from eroding into an accidental row, and the map's comment now says the explicitness is load-bearing. - libopus arrives through pkg_check_modules like sodium and SDL2, and dpkg-shlibdeps / rpm's soname scanner pick the runtime dependency up from DT_NEEDED, so the packaging Depends lists need nothing. The Flatpak gains --socket=pulseaudio (pipewire-pulse serves it on PipeWire desktops) and flips its bundled SDL to -DSDL_AUDIO=ON; the AppImage job does the same and adds libpulse-dev so SDL's pulse backend configures (SDL dlopen's the host libpulse at run time, so nothing new is bundled). The KDE runtime already ships libopus, so the Flatpak needs no opus module - and the REQUIRED opus.pc check fails the build loudly if a runtime bump ever drops it. - SatelliteClient's audio arms ride the POSIX socket shape it already has: sendEncrypted was already bool with the MSG_DONTWAIT/EAGAIN soft-drop, so the port adds only the 1472-byte oversize refusal (logged once), the full-datagram receive buffer (recvfrom truncates, and a truncated SPEAKER_AUDIO ciphertext fails the AEAD silently), and the two parsers; the loopback test speaks sockaddr_in/close instead of Winsock. The capture engine enforces the privacy invariant unchanged: muted, toggled off, unrouted, unstreaming or unwelcome at the host means the capture device is CLOSED and zero MSG_MIC_AUDIO packets leave - never silent ones - and seq still advances on failed encodes. Mute stays session-scoped (it clears when the pad leaves, like the hardware's own); the durable off-switch is the per-binding Microphone toggle, still OFF by default. The DualSense mute button lives as a decoder- owned latch folded into wButtons on the read thread, the lamp answers locally at once, and a later host MSG_MIC_LED repaints it - last writer wins on the PAD while the UI shows the local truth.
This repo's clang-tidy runs performance-enum-size, which the Windows client's config does not; the enum is shared code, so the base type lands in both repos to keep the files byte-identical.
Qt 6.9 rebuilt QWaitCondition over std::condition_variable, so a Qt thread handoff now reaches the pthread interceptor with libstdc++ as the immediate caller and the existing libQt6* called_from_lib lines no longer match. TSan then flags an unlock-of-unlocked mutex inside Qt-allocated memory it never saw locked; QNetworkAccessManager's worker startup under test_moonlight_binding_refcount trips it depending on scheduling, which is why the same code passed and failed across identical reruns. No first-party code uses std::condition_variable (the file says to re-check before keeping the line).
|
All lanes green: both compiler legs, ASan+UBSan, TSan, coverage, the .deb package-and-launch gate, and CodeQL. Two follow-ups landed after the initial push: the shared Stream enum gained a byte-sized base (this repo's clang-tidy runs performance-enum-size; the file stays byte-identical with dish-windows), and tests/tsan.suppressions learned that Qt 6.9 rebuilt QWaitCondition over std::condition_variable, which routed Qt's thread handoffs past the existing libQt6* suppressions and produced a scheduling-dependent unlock-of-unlocked report from QNetworkAccessManager's worker startup; the new line is constrained by a grep-verified fact recorded in the file (no first-party std::condition_variable exists). Marking ready. Live-hardware items (pipewire endpoint naming on a real DualSense, lamp/amp bytes against firmware) are listed in the PR body. |
Completes the fleet-wide build unification (satellite's landed inside TinkerNorth/satellite#88, dish-windows in TinkerNorth/dish-windows#67): local builds and CI share the same entry points so they cannot drift. This repo already had the fleet's best piece (ci_local.sh, build-appimage.sh); this converges the rest. CMakePresets.json (new) holds debug/release/package configures, with QT_QPA_PLATFORM=offscreen moving into the test presets; scripts/build.sh rides the presets; ci_local.sh becomes scripts/ci-local.sh (old name forwards) and closes its known gaps against CI: the missing DISH_REQUIRE_TRANSLATIONS=ON, the missing qmllint -I $QT_ROOT_DIR/qml include, and single-compiler blindness (new --compiler gcc|clang). scripts/install-deps.sh lands with a --ci-qt flag for CI's aqtinstall 6.9.3, since distro lupdate under 6.9 breaks the translation gate. The three near-identical inline cmake+cpack blocks (PR package job, release deb, release rpm) become scripts/build-deb.sh + build-rpm.sh over a shared package preset. The pre-commit hook and docs pointed clang-tidy at build-debug while CI used build; everything now agrees on CI's tree names. Compiler matrix and ccache stay workflow-side by design (a preset naming a compiler would break the other matrix leg); sanitizer/coverage instrumentation stays lane-specific, mirrored by ci-local's --with-sanitizers. No Linux toolchain exists on the dev box, so beyond bash -n, YAML/preset validation, the pin-lint awk, and a real check-format.sh run, the preset plumbing proves out on this PR's CI. Stacked on #48 (both edit linux-ci.yml); will be rebased onto main once that merges.
Ports controller audio (protocol 2 extension; satellite TinkerNorth/satellite#88, dish-android TinkerNorth/dish-android#178, dish-windows TinkerNorth/dish-windows#66) to the Linux client. Physical Direct-claimed pads only; no virtual controller exists here.
This is the file-for-file port of both Windows waves in one commit: wire (MIC_AUDIO 0x0012 up / SPEAKER_AUDIO 0x0013 down / MIC_LED 0x0014 down, caps mic 0x0040 / speaker 0x0080, receive buffer 256 to 1500, 1472 send guard), the capability fold with first-time consumption of GET /api/server/capabilities (probed per session PUT, conservative-false), the Opus codec and shared 2-frame jitter mirror, SDL audio capture/playout engines with the zero-packets-while-muted invariant, product-string pad-to-endpoint matching (ambiguity publishes nothing), the DualSense mute button and wButtons 0x0800 mute state, MIC_LED actuation with the FeedbackState lamp shadow, and slot-card mute controls showing local truth.
Linux-specific deltas: the pad string comes from the USB product attribute (iProduct) with HID_NAME as fallback, since pipewire/alsa names derive from iProduct while HID_NAME prepends the manufacturer; opus arrives via pkg-config; Moonlight cannot see 0x0800 structurally (the explicit button map has no such row, pinned by test); packaging grows libopus across CI, deb/rpm (via shlibdeps), AppImage (SDL built with audio + libpulse backend), and both Flatpak manifests gain --socket=pulseaudio.
Of the 33 new source files, 29 are byte-identical to dish-windows; the four that differ are three cross-platform comment variants and the POSIX loopback test. 12 new test suites plus 10 extended ones. This box has no Linux toolchain, so beyond syntax/format/translation gates the proof is this PR's CI: the compile+ctest lane, the stricter clang-tidy, TSan over the engines, and the package job's shlibdeps.