Windows port Phase 2: cpal device enumeration #4
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user