diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md new file mode 100644 index 0000000..0b0d711 --- /dev/null +++ b/docs/WINDOWS.md @@ -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. diff --git a/src/notify.rs b/src/notify.rs index 0e7baf9..58355d9 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -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 { 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