Windows port Phase 2: cpal device enumeration #4

Open
mollusk wants to merge 15 commits from windows-port-phase2 into windows-port-phase1
2 changed files with 118 additions and 5 deletions
Showing only changes of commit 46809153d8 - Show all commits
+77
View File
@@ -0,0 +1,77 @@
# PeerSpeak on Windows
Current status: the Windows port cross-compiles to `x86_64-pc-windows-gnu` and the `.exe`
launches under Wine. A real Windows/WASAPI host is still needed for the final audio-device
checks listed below.
## What works today
| Area | Status |
|---|---|
| GUI | Iced/wgpu builds and renders under Wine. |
| Networking | Iroh QUIC transport and gossip compile on Windows. |
| Audio backend | `cpal` drives WASAPI capture/playback behind `AudioBackend`. |
| Codec | Opus remains 48 kHz mono, 20 ms frames. |
| Identity | `ring` identity generation/load is platform-neutral. |
| Chimes | Windows uses PowerShell `System.Media.SoundPlayer` for WAV playback. |
Windows paths are resolved through `dirs`:
- Config: `%APPDATA%\peerspeak\config.json`
- Identity: `%APPDATA%\peerspeak\identity.key`
- Log: `%LOCALAPPDATA%\peerspeak\peerspeak.log`
## Building
### Native Windows
Install MSVC Build Tools and CMake, then build normally:
```powershell
cargo build --release
```
If CMake is 4.x or newer, the vendored `opus`/`libopus` build may need:
```powershell
$env:CMAKE_POLICY_VERSION_MINIMUM = "3.5"
cargo build --release
```
### Cross-compile from Linux
The current dev path cross-compiles from an Arch environment to the GNU Windows target:
```sh
rustup target add x86_64-pc-windows-gnu
sudo pacman -S mingw-w64-gcc cmake
CMAKE_POLICY_VERSION_MINIMUM=3.5 cargo build --release --target x86_64-pc-windows-gnu --bin peerspeak
```
Wine is useful for launch/render smoke tests, but it is not a substitute for a real
Windows audio-device pass. The deeper migration plan (phases, decisions, the opus build
spike) lives in the maintainer's handoff docs, outside the repo.
## First run and networking
Expect a Windows Firewall prompt the first time the app opens network sockets. Allow it:
PeerSpeak uses UDP for QUIC, plus relay traffic when direct NAT traversal is not available.
The default network mode keeps the n0 relay available for NAT traversal without publishing
presence to n0 DNS. Direct peer-to-peer paths may work when both networks allow them; relayed
connections are expected and valid.
## Known gaps
| Item | Status |
|---|---|
| Echo cancellation | Linux-only PipeWire feature. The Windows UI shows it disabled as unavailable. |
| Screen share | Requires a Windows `pixelpass.exe` on `PATH` or a configured override. |
| Chimes | Now routed through Windows `SoundPlayer`; needs a real Windows host to audibly verify. |
| Resampling/device format | Open. Devices must support 48 kHz, and output must support stereo; a 44.1 kHz-only/default device currently errors instead of playing. |
| Device persistence | Open. WASAPI friendly names may duplicate or change across driver/profile changes. |
| Playback pacing | Open. The fixed playback target under WASAPI shared mode still needs real-hardware verification. |
Before calling Windows support done, verify a real Windows machine can create/join a room,
capture mic audio, hear remote audio, select devices, restart with selections preserved, and
play notification chimes.
+41 -5
View File
@@ -3,11 +3,12 @@
//!
//! The WAVs are embedded in the binary (`include_bytes!`) so a deployed single
//! binary is self-contained — no asset directory to ship alongside it. On first
//! use each sound is written once to a temp file, then played fire-and-forget
//! via `pw-play` (PipeWire-native; falls back to `paplay`/`aplay`). Playback runs
//! on a detached thread that waits on the child, so it never blocks the UI and
//! never leaves a zombie. Any failure (no player, no audio) is silent by design —
//! a missing chime should never disrupt a call.
//! use each sound is written once to a temp file, then played fire-and-forget.
//! Linux uses `pw-play` (PipeWire-native; falls back to `paplay`/`aplay`);
//! Windows uses PowerShell's `System.Media.SoundPlayer`. Playback runs on a
//! detached thread that waits on the child, so it never blocks the UI and never
//! leaves a zombie. Any failure (no player, no audio) is silent by design — a
//! missing chime should never disrupt a call.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -202,8 +203,14 @@ fn cached_path(sound: Sound) -> Option<PathBuf> {
Some(path)
}
#[cfg(any(windows, test))]
fn escape_powershell_single_quoted(s: &str) -> String {
s.replace('\'', "''")
}
/// Try each available player in turn, waiting on the first that starts (which
/// reaps the child). Runs on a detached thread, so the wait is harmless.
#[cfg(not(windows))]
fn spawn_player(path: &Path) {
for player in ["pw-play", "paplay", "aplay"] {
let started = Command::new(player)
@@ -221,6 +228,23 @@ fn spawn_player(path: &Path) {
}
}
/// Play through Windows' built-in WAV player. Runs on a detached thread, so
/// `PlaySync()` blocking for the sound duration is fine.
#[cfg(windows)]
fn spawn_player(path: &Path) {
let path = escape_powershell_single_quoted(&path.display().to_string());
let command = format!("(New-Object System.Media.SoundPlayer '{path}').PlaySync()");
let _ = Command::new("powershell")
.arg("-NoProfile")
.arg("-NonInteractive")
.arg("-Command")
.arg(command)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(test)]
mod tests {
use super::*;
@@ -234,6 +258,18 @@ mod tests {
assert!(!should_play(false, false));
}
#[test]
fn test_powershell_single_quote_escape() {
assert_eq!(
escape_powershell_single_quoted(r"C:\Users\O'Brien\chime.wav"),
r"C:\Users\O''Brien\chime.wav"
);
assert_eq!(
escape_powershell_single_quoted("a'b'c"),
"a''b''c"
);
}
#[test]
fn test_sound_indices_unique_and_match_all() {
// `index()` must be a 0..COUNT bijection in `ALL` order, or the flag