Windows: notification chimes via SoundPlayer + docs/WINDOWS.md (W8)
Implemented by Codex (gpt-5.5); reviewed and committed by Claude. W8 — chimes were played by shelling out to pw-play/paplay/aplay, which don't exist on Windows, so every chime silently no-op'd there. spawn_player is now cfg-split: Linux/unix keeps the existing player list; Windows plays the WAV via PowerShell's System.Media.SoundPlayer (PlaySync on the existing detached thread). Dependency-free, same fire-and-forget / silent-on-failure contract. Custom chime paths are single-quote-escaped for the PowerShell command (helper + unit test). Also adds docs/WINDOWS.md: a build/run/status guide (native MSVC + cross-compile to -gnu, first-run firewall/UDP note, %APPDATA% paths, and the honest known-gaps table — echo-cancel/screenshare/resampling/device-id/buffer-pacing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
//! 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
|
//! 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
|
//! 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
|
//! Linux uses `pw-play` (PipeWire-native; falls back to `paplay`/`aplay`);
|
||||||
//! on a detached thread that waits on the child, so it never blocks the UI and
|
//! Windows uses PowerShell's `System.Media.SoundPlayer`. Playback runs on a
|
||||||
//! never leaves a zombie. Any failure (no player, no audio) is silent by design —
|
//! detached thread that waits on the child, so it never blocks the UI and never
|
||||||
//! a missing chime should never disrupt a call.
|
//! 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::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -202,8 +203,14 @@ fn cached_path(sound: Sound) -> Option<PathBuf> {
|
|||||||
Some(path)
|
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
|
/// 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.
|
/// reaps the child). Runs on a detached thread, so the wait is harmless.
|
||||||
|
#[cfg(not(windows))]
|
||||||
fn spawn_player(path: &Path) {
|
fn spawn_player(path: &Path) {
|
||||||
for player in ["pw-play", "paplay", "aplay"] {
|
for player in ["pw-play", "paplay", "aplay"] {
|
||||||
let started = Command::new(player)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -234,6 +258,18 @@ mod tests {
|
|||||||
assert!(!should_play(false, false));
|
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]
|
#[test]
|
||||||
fn test_sound_indices_unique_and_match_all() {
|
fn test_sound_indices_unique_and_match_all() {
|
||||||
// `index()` must be a 0..COUNT bijection in `ALL` order, or the flag
|
// `index()` must be a 0..COUNT bijection in `ALL` order, or the flag
|
||||||
|
|||||||
Reference in New Issue
Block a user