feat(host/windows): GameStream (Moonlight) audio on Windows — stereo
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 30s
android / android (push) Failing after 53s
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 47s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 1m48s
decky / build-publish (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 3m12s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 1m17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m18s
docker / deploy-docs (push) Failing after 17s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 30s
android / android (push) Failing after 53s
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 47s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 1m48s
decky / build-publish (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 3m12s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 1m17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m18s
docker / deploy-docs (push) Failing after 17s
`serve` gave Moonlight clients no audio on Windows: the GameStream audio stream thread was Linux-only (a non-Linux stub errored). Widen the stereo path to Windows — the encode/RTP/AES-CBC/hand-rolled-RS(4,2)-FEC logic is platform-neutral and already live-validated byte-identical on Linux, and it now runs over the WASAPI capturer + the (already cross-platform) `opus` crate. The cfg gates go from `linux` to `any(linux, windows)`; only the surround path stays Linux-only because its libopus *multistream* encoder needs `audiopus_sys` (a Linux dep) — on Windows a surround request fails cleanly with a "use stereo" error. Linux stays byte-identical (the `SessionEncoder::Surround` variant and its match arm keep `#[cfg(linux)]`, so Linux compiles exactly as before). Verified: clippy -D warnings + host test suite green on both x86_64-pc-windows-msvc (73/73) and Linux (78/78). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,9 @@
|
||||
//! data packets are consumed immediately and missing parity only costs loss recovery — so
|
||||
//! the validated stereo path stays byte-identical (data packets only, exactly as before).
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", test))]
|
||||
use crate::audio::SAMPLE_RATE;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use {
|
||||
super::AUDIO_PORT,
|
||||
crate::audio::{self, AudioCapturer},
|
||||
@@ -31,7 +31,7 @@ use {
|
||||
std::time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
|
||||
|
||||
/// RTP payload types (moonlight-common-c `RtpAudioQueue.c`: `RTP_PAYLOAD_TYPE_AUDIO 97`,
|
||||
@@ -259,7 +259,7 @@ pub type AudioCapSlot =
|
||||
/// Spawn the audio stream thread (idempotent via `running`). Stops when `running` clears.
|
||||
/// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption;
|
||||
/// `params` is the negotiated [`AudioParams`] from the RTSP ANNOUNCE.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub fn start(
|
||||
running: Arc<AtomicBool>,
|
||||
gcm_key: [u8; 16],
|
||||
@@ -279,10 +279,10 @@ pub fn start(
|
||||
});
|
||||
}
|
||||
|
||||
/// Stub — the audio plane needs Linux (PipeWire capture + libopus); this keeps non-Linux
|
||||
/// dev builds compiling (crate doc: "the crate compiles everywhere"). Reports failure the
|
||||
/// same way the real stream thread does: by clearing `running`.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
/// Stub — the audio plane needs an audio-capture backend (PipeWire on Linux, WASAPI on Windows)
|
||||
/// + libopus; this keeps the remaining targets (e.g. macOS) compiling (crate doc: "the crate
|
||||
/// compiles everywhere"). Reports failure the same way the real stream thread does: clears `running`.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
pub fn start(
|
||||
running: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_gcm_key: [u8; 16],
|
||||
@@ -290,11 +290,11 @@ pub fn start(
|
||||
_params: AudioParams,
|
||||
_audio_cap: AudioCapSlot,
|
||||
) {
|
||||
tracing::error!("GameStream audio requires Linux (PipeWire + libopus)");
|
||||
tracing::error!("GameStream audio requires Linux (PipeWire) or Windows (WASAPI) + libopus");
|
||||
running.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
fn run(
|
||||
running: &AtomicBool,
|
||||
gcm_key: &[u8; 16],
|
||||
@@ -340,13 +340,15 @@ fn run(
|
||||
|
||||
/// Opus encoder for one session: the plain stereo encoder (the live-validated path, byte
|
||||
/// identical) or a libopus multistream encoder for 5.1/7.1.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
enum SessionEncoder {
|
||||
Stereo(opus::Encoder),
|
||||
// Surround needs the libopus *multistream* encoder via `audiopus_sys` (Linux-only dep).
|
||||
#[cfg(target_os = "linux")]
|
||||
Surround(MsEncoder),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
impl SessionEncoder {
|
||||
fn new(layout: &'static OpusLayout) -> Result<SessionEncoder> {
|
||||
if layout.channels == 2 {
|
||||
@@ -362,8 +364,19 @@ impl SessionEncoder {
|
||||
enc.set_vbr(false).ok();
|
||||
Ok(SessionEncoder::Stereo(enc))
|
||||
} else {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(SessionEncoder::Surround(MsEncoder::new(layout)?))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"surround audio ({} ch) needs the libopus multistream encoder (Linux only) — \
|
||||
use a stereo session",
|
||||
layout.channels
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode one interleaved frame (`samples_per_channel * channels` f32s) into `out`,
|
||||
@@ -374,8 +387,12 @@ impl SessionEncoder {
|
||||
samples_per_channel: usize,
|
||||
out: &mut [u8],
|
||||
) -> Result<usize> {
|
||||
// `samples_per_channel` only feeds the multistream (surround) encoder; stereo infers it.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let _ = samples_per_channel;
|
||||
match self {
|
||||
SessionEncoder::Stereo(enc) => enc.encode_float(frame, out).context("opus encode"),
|
||||
#[cfg(target_os = "linux")]
|
||||
SessionEncoder::Surround(enc) => enc.encode_float(frame, samples_per_channel, out),
|
||||
}
|
||||
}
|
||||
@@ -454,7 +471,7 @@ impl Drop for MsEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
fn audio_body(
|
||||
cap: &mut dyn AudioCapturer,
|
||||
sock: &UdpSocket,
|
||||
|
||||
@@ -29,6 +29,7 @@ Every OS-touching backend is implemented behind the existing traits and **builds
|
||||
| Run host (serve/m3-host) | ✅ live | m3-host starts + listens; `c_abi_connection_roundtrip` passes |
|
||||
| Gamepad (ViGEm) | ✅ done | compiles incl. rumble back-channel; live needs ViGEmBus + a physical pad |
|
||||
| Host→client audio wiring | ✅ done | builds on MSVC; `m3` `audio_thread` active on Windows (silent VM → no samples to send) |
|
||||
| GameStream (Moonlight) audio | ✅ done | stereo path active on Windows (WASAPI→Opus→RTP/FEC); surround stays Linux-only (libopus multistream / `audiopus_sys`) |
|
||||
| Rumble back-channel (ViGEm) | ✅ done | `request_notification` → background thread → 0xCA; live needs a physical pad |
|
||||
|
||||
**Remaining for full parity:**
|
||||
|
||||
Reference in New Issue
Block a user