diff --git a/docs/ADMINISTRATION.md b/docs/ADMINISTRATION.md index 04dd1b6..043ebd6 100644 --- a/docs/ADMINISTRATION.md +++ b/docs/ADMINISTRATION.md @@ -625,3 +625,123 @@ curl -s http://relay-host:9090/metrics | grep wzp_relay_active_sessions # Check federation probe health curl -s http://relay-host:9090/metrics | grep wzp_probe_up ``` + +## Build Pipelines + +All production artifacts (Android APK, Linux x86_64 binaries, Windows `.exe`) are built on **SepehrHomeserverdk** using Docker, not on developer workstations. The pipelines are fire-and-forget: a local script invokes a `tmux` session on the remote, the build runs in a Docker container, and the artifact is uploaded to `paste.dk.manko.yoga` (rustypaste) with a notification sent to `ntfy.sh/wzp` on start and completion. + +### Docker images + +Two long-lived images live on the remote: + +| Image | Used by | Base | Key contents | +|---|---|---|---| +| `wzp-android-builder` | Android APK (Tauri mobile + legacy Kotlin), Linux x86_64 relay/CLI | Debian bookworm | Rust stable with Android targets, cargo-ndk, NDK 26.1, Android SDK (API 34 + 35 + 36), JDK 17, Gradle 8.5, Node.js 20, cmake, ninja, tauri-cli 2.x | +| `wzp-windows-builder` | Windows x86_64 `.exe` | Debian bookworm | Rust stable with `x86_64-pc-windows-msvc` target, cargo-xwin (with pre-warmed MSVC CRT + Windows SDK cache), Node.js 20, cmake, ninja, clang, lld, nasm | + +Both images are rebuilt rarely — once the base toolchain is stable, rebuilds are only needed to pick up new dependencies or security patches. + +**Rebuilding an image** (fire-and-forget, ~10 min on a warm base): + +```bash +# Windows +./scripts/build-windows-docker.sh --image-build + +# Android (upload and rebuild handled by the Android build script itself — see +# its --image-build flag or equivalent) +``` + +The `--image-build` flag uploads the local Dockerfile to the remote, kicks off `docker build` under `nohup`, and returns immediately. Monitor with: + +```bash +ssh SepehrHomeserverdk 'tail -f /tmp/wzp-windows-image-build.log' +``` + +### Pipeline: Android APK (Tauri Mobile) + +```bash +./scripts/build-tauri-android.sh # Full: pull + build + upload + notify +./scripts/build-tauri-android.sh --no-pull # Skip git fetch +./scripts/build-tauri-android.sh --clean # Force-clean Rust target +``` + +- **Branch**: `android-rewrite` +- **Image**: `wzp-android-builder` +- **Build command**: `cargo tauri android build --release` +- **Output**: `wzp-release.apk` → uploaded to rustypaste +- **Notifications**: start + completion to `ntfy.sh/wzp` +- **Remote artifact path**: `/mnt/storage/manBuilder/data/cache-android/target/…/release/app-release.apk` + +### Pipeline: Linux x86_64 (relay + CLI + bench + web) + +```bash +./scripts/build-linux-docker.sh # Fire-and-forget +./scripts/build-linux-docker.sh --no-pull # Skip git fetch +./scripts/build-linux-docker.sh --clean # Force-clean target +./scripts/build-linux-docker.sh --install # Wait for completion and download locally +``` + +- **Branch**: `feat/android-voip-client` (script default — override by editing the script or passing an env var) +- **Image**: `wzp-android-builder` (shared, not a separate Linux-only image) +- **Targets built**: `wzp-relay`, `wzp-client`, `wzp-client-audio` (with `--features audio`), `wzp-web`, `wzp-bench` +- **Output**: `wzp-linux-x86_64.tar.gz` with all five binaries → uploaded to rustypaste +- **Local landing dir** (with `--install`): `target/linux-x86_64/` + +### Pipeline: Windows x86_64 (`wzp-desktop.exe`) + +```bash +./scripts/build-windows-docker.sh # Full: pull + build + download locally +./scripts/build-windows-docker.sh --no-pull # Skip git fetch +./scripts/build-windows-docker.sh --rust # Force-clean target-windows cache +./scripts/build-windows-docker.sh --image-build # Rebuild the Docker image (fire-and-forget) +``` + +- **Branch**: `feat/desktop-audio-rewrite` +- **Image**: `wzp-windows-builder` +- **Build command**: `cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop` +- **Output**: `wzp-desktop.exe` (~16 MB) → downloaded to `target/windows-exe/wzp-desktop.exe`, also uploaded to rustypaste +- **Target cache volume**: `target-windows` (separate from the Android target cache to avoid triple cross-contamination) +- **Shared cache volumes**: `cargo-registry`, `cargo-git` (shared with Android — both pipelines pull the same crates) + +**A/B-preserving workflow** for testing audio backends: rename the prior `.exe` before re-running the build, so both coexist: + +```bash +# Preserve prior build as the noAEC baseline +mv target/windows-exe/wzp-desktop.exe target/windows-exe/wzp-desktop-noAEC.exe +./scripts/build-windows-docker.sh +ls -la target/windows-exe/ +# wzp-desktop-noAEC.exe (previous build) +# wzp-desktop.exe (new build) +``` + +### Alternative pipeline: Windows via Hetzner Cloud VPS + +For situations where Docker image rebuilds would be disruptive, or for one-shot debug builds on a clean machine: + +```bash +./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy +./scripts/build-windows-cloud.sh --prepare # Create VM + install deps, don't build +./scripts/build-windows-cloud.sh --build # Build on existing VM +./scripts/build-windows-cloud.sh --transfer # Download .exe from existing VM +./scripts/build-windows-cloud.sh --destroy # Delete the VM +WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Don't auto-destroy after successful build +``` + +- **Provider**: Hetzner Cloud +- **Default server type**: `cx33` (8 GB RAM, 8 vCPU — `cx23` with 4 GB OOMs on the tauri+rustls cross-compile) +- **Image**: `ubuntu-24.04` +- **SSH key**: must be named `wz` in Hetzner and loaded in the local ssh-agent +- **Reminder**: set `WZP_KEEP_VM=1` for multi-build sessions, then **remember to `--destroy` at end of day** so the VM isn't left running overnight. This is tracked in the auto-memory as `feedback_keep_windows_builder_vm.md`. + +### Notifications + +All pipelines post to `https://ntfy.sh/wzp`. Subscribe from your phone via the [ntfy.sh app](https://ntfy.sh/) to get push notifications on build start/success/failure. Messages include the short git hash and the rustypaste URL on success: + +``` +WZP Windows build OK [03a80a3] (16M) +https://paste.dk.manko.yoga//wzp-desktop.exe +``` + +### Rustypaste credentials + +Build pipelines read `rusty_address` and `rusty_auth_token` from the `.env` file at `/mnt/storage/manBuilder/.env` on SepehrHomeserverdk. Local scripts that upload directly (`build-windows-cloud.sh` when run in `--transfer` mode) read from `~/.wzp/rustypaste.env` with the same variable names. Both files must be kept in sync manually if rotated. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2024aa1..bff44ae 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -872,3 +872,71 @@ warzonePhone/ | wzp-relay | 40 + 4 integration | Room ACL, session mgmt, metrics, probes, mesh, trunking | | wzp-client | 30 + 2 integration | Encoder/decoder, quality adapter, silence, drift, sweep | | wzp-web | 2 | Metrics | + +## Audio Backend Architecture (Platform Matrix) + +WarzonePhone's audio I/O goes through one of four backends depending on the target platform and feature flags. All backends expose the same public API (`AudioCapture::start() → AudioCapture { ring(), stop() }`) via conditional re-exports in `crates/wzp-client/src/lib.rs`, so the `CallEngine` above the audio layer doesn't know or care which backend is running. + +``` + ┌─────────────────────────────────────────────┐ + │ CallEngine (platform-agnostic) │ + │ reads PCM from AudioCapture::ring() │ + │ writes PCM to AudioPlayback::ring() │ + └────────────────────┬────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌────────────────┐ ┌───────────────┐ + │ audio_io │ │ audio_vpio │ │ audio_wasapi │ + │ (CPAL) │ │ (Core Audio │ │ (Windows │ + │ │ │ VoiceProc IO) │ │ IAudioClient2│ + │ All platforms │ │ macOS only │ │ Windows │ + │ (baseline) │ │ feature=vpio │ │ feature= │ + │ │ │ │ │ windows-aec │ + └───────────────┘ └────────────────┘ └───────────────┘ + │ + ▼ on Android only + ┌───────────────┐ + │ wzp-native │ + │ (Oboe bridge │ + │ via dlopen) │ + │ │ + │ Android only │ + │ libloading │ + └───────────────┘ +``` + +### Backend selection matrix + +| Platform | Capture | Playback | OS AEC | Feature flags | +|---|---|---|---|---| +| macOS | VoiceProcessingIO (native Core Audio) | CPAL | **Yes** — Apple's hardware-accelerated AEC (same AEC as FaceTime, iMessage audio, Voice Memos) | `audio`, `vpio` | +| Windows (AEC build) | Direct WASAPI with `AudioCategory_Communications` | CPAL | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + NS + AGC), driver-dependent quality | `audio`, `windows-aec` | +| Windows (baseline) | CPAL (WASAPI shared mode) | CPAL | No | `audio` | +| Linux | CPAL (ALSA / PulseAudio) | CPAL | No | `audio` | +| Android (Tauri Mobile) | Oboe via `wzp-native` cdylib, `Usage::VoiceCommunication` + `MODE_IN_COMMUNICATION` | Same Oboe stream | Depends on device (some Android devices apply AEC to the voice-communication stream, most do not) | none (`wzp-client` compiled with `default-features = false`) | + +### Why `wzp-native` is a standalone cdylib + +On Android, the audio backend lives in a separate cdylib crate (`crates/wzp-native`) that `wzp-desktop`'s lib crate loads at runtime via `libloading`. It is **not** linked as a regular Rust dep. + +This is deliberate. rust-lang/rust#104707 documents that a crate with `crate-type = ["cdylib", "staticlib"]` leaks non-exported symbols from the staticlib into the cdylib. On Android, that caused Bionic's private `__init_tcb` / `pthread_create` symbols to be bound LOCALLY inside our `.so` instead of resolved dynamically against `libc.so` at `dlopen` time — which crashed the app at launch as soon as `tao` tried to `std::thread::spawn()` from the JNI `onCreate` callback. + +Keeping `wzp-native` in its own cdylib and loading it via `libloading` means: + +1. The app's own `.so` has `crate-type = ["cdylib", "rlib"]` only — no `staticlib`, no symbol leak. +2. `libwzp_native.so` is loaded via `System.loadLibrary` from the JVM side (or `dlopen` from Rust), which triggers the normal Bionic resolver and binds all private symbols against `libc.so` at load time. +3. The C/C++ Oboe bridge is fully isolated inside `libwzp_native.so`'s symbol space — no chance of its archives leaking into `wzp-desktop`'s `.so`. + +See `docs/BRANCH-android-rewrite.md` for the full incident postmortem and `docs/incident-tauri-android-init-tcb.md` for the debug log. + +### Vendored `audiopus_sys` for libopus / clang-cl cross-compile + +The workspace root carries a vendored copy of `audiopus_sys` at `vendor/audiopus_sys/` with a patched `opus/CMakeLists.txt`. This is needed because libopus 1.3.1 gates its per-file `-msse4.1` / `-mssse3` `COMPILE_FLAGS` behind `if(NOT MSVC)`, and under `clang-cl` (used by `cargo-xwin` for Windows cross-compiles) CMake sets `MSVC=1` unconditionally — so the SIMD source files compile without the required target feature and fail to link the intrinsic `always_inline` functions. + +The patch introduces an `MSVC_CL` variable that is true only for real `cl.exe` (distinguished via `CMAKE_C_COMPILER_ID STREQUAL "MSVC"`), and flips the eight `if(NOT MSVC)` SIMD guards to `if(NOT MSVC_CL)` so clang-cl gets the GCC-style per-file flags. Wired in via `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` at the workspace root. + +This does not affect macOS or Linux builds — on those platforms `MSVC=0` everywhere so the patched logic behaves identically to upstream. + +Upstream tracking: xiph/opus#256, xiph/opus PR #257 (both stale). diff --git a/docs/BRANCH-desktop-audio-rewrite.md b/docs/BRANCH-desktop-audio-rewrite.md new file mode 100644 index 0000000..9d1ed72 --- /dev/null +++ b/docs/BRANCH-desktop-audio-rewrite.md @@ -0,0 +1,164 @@ +# Branch: `feat/desktop-audio-rewrite` + +Home of the Tauri desktop client for macOS, Windows, and Linux. Named "audio-rewrite" because the original driver was replacing a CPAL-only audio pipeline with platform-native backends that support OS-level echo cancellation (VoiceProcessingIO on macOS, WASAPI Communications on Windows), but the branch has grown into the full desktop story — Windows cross-compilation, vendored dependencies, history UI, direct calling, the whole thing. + +## Purpose + +The desktop client shares 100% of its frontend (`desktop/src/`) and Tauri command layer (`desktop/src-tauri/src/lib.rs`, `engine.rs`, `history.rs`) with the Android build on `android-rewrite`. Differences are limited to: + +- **Audio backends**, which are platform-gated via Cargo target-dep sections in `desktop/src-tauri/Cargo.toml` and feature flags in `crates/wzp-client/Cargo.toml`. +- **Identity storage paths**, which resolve via Tauri's `app_data_dir()` (`~/Library/Application Support/…` on macOS, `%APPDATA%\…` on Windows, `~/.local/share/…` on Linux). +- **Build toolchains**: native `cargo build` on macOS/Linux, `cargo xwin` cross-compile from Linux for Windows via Docker on SepehrHomeserverdk. + +## Audio backend matrix + +| Target | Capture | Playback | AEC | +|---|---|---|---| +| macOS | CPAL (WASAPI/CoreAudio via cpal crate) OR VoiceProcessingIO (native Core Audio) | CPAL | VoiceProcessingIO native AEC (when `vpio` feature enabled) | +| Windows (default) | CPAL → WASAPI shared mode | CPAL → WASAPI shared mode | None | +| Windows (AEC build) | Direct WASAPI with `IAudioClient2::SetClientProperties(AudioCategory_Communications)` | CPAL → WASAPI shared mode | **OS-level**: Windows routes the capture stream through the driver's communications APO chain (AEC + NS + AGC) | +| Linux | CPAL → ALSA/PulseAudio | CPAL → ALSA/PulseAudio | None | + +The macOS VPIO path is gated behind the `vpio` feature in `wzp-client` and the `coreaudio-rs` dep is itself `cfg(target_os = "macos")`, so enabling the feature on Windows or Linux is a no-op. + +The Windows AEC path is gated behind the `windows-aec` feature, also target-gated (the `windows` crate dep is only pulled in on Windows), and re-exports `WasapiAudioCapture as AudioCapture` when enabled so downstream code doesn't need to know which backend is active. The current Windows build at `target/windows-exe/wzp-desktop.exe` has `windows-aec` on; a baseline noAEC build is preserved at `target/windows-exe/wzp-desktop-noAEC.exe` for A/B comparison on real hardware. + +See [`BRANCH-android-rewrite.md`](BRANCH-android-rewrite.md) for Oboe audio on Android, which is its own story. + +## Recent major work + +### 1. Desktop direct calling feature (commit `2fd9465` and neighbors) + +Brought direct 1:1 calls to macOS with full parity to the Android client: + +- **Identity path fix**: the desktop `CallEngine::start` was loading seed from `$HOME/.wzp/identity` while `register_signal` used Tauri's `app_data_dir()`, producing two different fingerprints per run. Both now route through `load_or_create_seed()` which uses `app_data_dir()` everywhere. +- **Call history with dedup**: `history.rs` stores a `Vec` with a `CallDirection` enum (`Placed | Received | Missed`). The `log` function dedupes by `call_id` so an outgoing call isn't logged twice as "missed" (when the signal loop's `DirectCallOffer` handler fires) and then again as "placed" (when `place_call` returns). Instead the entry is updated in place. +- **Recent contacts row**: a horizontal chip UI in the direct-call panel showing the last N peers with friendly aliases, clickable to re-dial. +- **Deregister button**: lets a user drop their signal registration without quitting the app, useful when switching identities. +- **Random alias derivation**: a new client sees a human-friendly alias like "silent-forest-41" derived deterministically from its seed, so it's identifiable in the UI before manual naming. +- **Default room "general"** instead of "android", since the desktop client is not Android. + +### 2. macOS VoiceProcessingIO integration + +`crates/wzp-client/src/audio_vpio.rs` — a native Core Audio implementation using `AUGraph` + `AudioComponentInstance` with the VPIO audio unit. Gives you hardware-accelerated AEC (same AEC Apple ships in FaceTime / iMessage audio / voice memos) at the cost of tight coupling to Apple frameworks. Lock-free ring pattern matches the CPAL path so the upper layers don't notice the difference. + +Enabled by `features = ["audio", "vpio"]` in the macOS target section of `desktop/src-tauri/Cargo.toml`. + +### 3. Windows cross-compilation via cargo-xwin + +Cross-compiling Rust + Tauri to `x86_64-pc-windows-msvc` from Linux using `cargo-xwin`, which downloads the Microsoft CRT + Windows SDK on demand and drives `clang-cl` as the compiler. No Windows machine is needed for the build itself — only for runtime testing. + +**Build infrastructure**: + +- `scripts/Dockerfile.windows-builder` — Debian bookworm + Rust + cargo-xwin + Node 20 + cmake + ninja + llvm + clang + lld + nasm. Pre-warms the xwin MSVC CRT cache at image build time (saves ~4 minutes per cold build). +- `scripts/build-windows-docker.sh` — fire-and-forget remote build via Docker on SepehrHomeserverdk. Same pattern as `build-tauri-android.sh`. Uploads the `.exe` to rustypaste and fires an `ntfy.sh/wzp` notification on start and on completion. +- `scripts/build-windows-cloud.sh` — alternative pipeline using a temporary Hetzner Cloud VPS. Slower (full VM spin-up), more expensive, but useful when Docker image rebuilds would be disruptive. + +**Two critical blockers resolved** on the way to a working `.exe`: + +1. **libopus SSE4.1 / SSSE3 intrinsic compile failure**. `audiopus_sys` vendors libopus 1.3.1, whose `CMakeLists.txt` gates the per-file `-msse4.1` `COMPILE_FLAGS` behind `if(NOT MSVC)`. Under `clang-cl`, CMake sets `MSVC=1` (because `CMAKE_C_COMPILER_FRONTEND_VARIANT=MSVC` triggers `Platform/Windows-MSVC.cmake` which unconditionally sets the variable), so the per-file flag is never set and the SSE4.1 source files compile without the target feature — then fail with 20+ "always_inline function '_mm_cvtepi16_epi32' requires target feature 'sse4.1'" errors. + + Fixed by **vendoring audiopus_sys into `vendor/audiopus_sys/`** and patching its bundled libopus to introduce an `MSVC_CL` variable that is true only for real `cl.exe` (distinguished via `CMAKE_C_COMPILER_ID STREQUAL "MSVC"`). The eight `if(NOT MSVC)` SIMD guards are flipped to `if(NOT MSVC_CL)` and the global `/arch` block at line 445 becomes `if(MSVC_CL)`, so clang-cl gets the GCC-style per-file flags while real cl.exe keeps the `/arch:AVX` / `/arch:SSE2` globals. + + Wired in via `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` at the workspace root. + + Upstream tracking: [xiph/opus#256](https://github.com/xiph/opus/issues/256), [xiph/opus PR #257](https://github.com/xiph/opus/pull/257) (both stale). + +2. **tauri-build needs `icons/icon.ico` for the Windows PE resource**. The desktop only had `icon.png`. Generated a multi-size ICO (16/24/32/48/64/128/256) from the existing placeholder via Pillow and committed it. Placeholder quality — real branded icons can replace it later. + +### 4. Windows `AudioCategory_Communications` capture path (task #24) + +`crates/wzp-client/src/audio_wasapi.rs` — direct WASAPI capture via `IMMDeviceEnumerator → IAudioClient2 → SetClientProperties` with `AudioCategory_Communications`. This tells Windows "this is a VoIP call" and Windows routes the capture stream through the driver's registered communications APO chain, which on most Win10/11 consumer hardware includes AEC, NS, and AGC. + +**Caveat**: quality is driver-dependent. On a machine with a good communications APO (Intel Smart Sound, Dolby, modern Realtek on Win11 24H2+, anything with Voice Clarity enabled) it's excellent. On generic class-compliant drivers with no communications APO registered, it's a no-op. For a guaranteed AEC regardless of driver, see task #26 which tracks implementing the classic Voice Capture DSP (`CLSID_CWMAudioAEC`) as a fallback. + +Gated behind the `windows-aec` feature in `wzp-client`. Enabled by default in the Windows target section of `desktop/src-tauri/Cargo.toml`. + +## Build pipelines + +### Native macOS / Linux + +```bash +cd desktop +npm install +npm run build +cd src-tauri +cargo build --release --bin wzp-desktop +``` + +### Windows x86_64 via Docker on SepehrHomeserverdk + +```bash +./scripts/build-windows-docker.sh # Full: pull + build + download +./scripts/build-windows-docker.sh --no-pull # Skip git fetch +./scripts/build-windows-docker.sh --rust # Force-clean Rust target +./scripts/build-windows-docker.sh --image-build # (Re)build the Docker image (fire-and-forget) +``` + +Output lands at `target/windows-exe/wzp-desktop.exe`. Both `wzp-desktop.exe` and `wzp-desktop-noAEC.exe` can coexist in that directory; the script writes `wzp-desktop.exe` so renaming the prior build to `-noAEC.exe` (or any other name) before rebuilding preserves it. + +### Windows x86_64 via Hetzner Cloud (alternative) + +```bash +./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy +./scripts/build-windows-cloud.sh --prepare # Create VM and install deps only +./scripts/build-windows-cloud.sh --build # Build on existing VM +./scripts/build-windows-cloud.sh --destroy # Delete the VM +WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Keep VM alive after build for debug +``` + +Remember to destroy the VM at end of day with `--destroy`. + +### Linux x86_64 (relay + CLI + bench) + +```bash +./scripts/build-linux-docker.sh # Fire-and-forget remote Docker build +./scripts/build-linux-docker.sh --install # Wait for completion and download +``` + +Uses the same `wzp-android-builder` Docker image as Android (not a separate image), since the deps (Rust + cmake + ring prereqs) are the same. + +## Testing + +### Direct calling parity + +1. Build on two machines (macOS + Windows, or two macOS, or any combination). +2. Both machines register on the same relay. +3. Copy one machine's fingerprint into the other's direct-call panel. +4. Place the call. Confirm ringing UI on the callee and "calling…" UI on the caller. +5. Answer. Confirm audio flows both ways. +6. Hang up from either side. Confirm call-history entries are labeled correctly (`Outgoing` on caller, `Incoming` on callee, never `Missed` on a successful call). + +### Windows AEC A/B + +1. Install `wzp-desktop-noAEC.exe` and `wzp-desktop.exe` on the same Windows box. +2. Join a call from each (separately) while a second machine plays known audio through the first machine's speakers. +3. On the remote (listening) side: the `noAEC` call should have clear audible echo; the AEC call should have minimal or no echo after a 1–2 s convergence period. +4. If both builds sound identical (with echo) → the `AudioCategory_Communications` switch isn't triggering the driver's APO chain. Investigate via task #26 (Voice Capture DSP fallback). + +## Known quirks + +1. **libopus vendor path is workspace-relative**. `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` works from any crate in the workspace because Cargo resolves it against the root `Cargo.toml`'s directory. If the workspace is moved or vendored into another workspace, update the path. + +2. **`cargo xwin` overwrites `override.cmake` on every invocation**. Any attempt to patch `~/.cache/cargo-xwin/cmake/clang-cl/override.cmake` at Docker image build time is inert because `src/compiler/clang_cl.rs` line ~444 writes the bundled file fresh on every run. All real fixes must land in the source tree (via the vendored audiopus_sys, as done here), not in the cargo-xwin cache. + +3. **WebView2 runtime is a prerequisite on Windows 10**. Windows 11 ships with it. If the `.exe` launches and immediately exits with no error on a Win10 machine, that's the missing runtime — install it from [Microsoft's Evergreen bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). + +4. **Rust 2024 edition `unsafe_op_in_unsafe_fn` lint**. The WASAPI backend in `audio_wasapi.rs` emits ~18 of these warnings because Rust 2024 requires explicit `unsafe { ... }` blocks inside `unsafe fn` bodies. The warnings don't block the build and don't affect runtime behavior; cleaning them up is tracked informally as tech debt. + +## Files of interest + +| Path | Purpose | +|---|---| +| `desktop/src/` | Shared frontend (TypeScript + HTML + CSS) | +| `desktop/src-tauri/src/lib.rs` | Tauri commands shared with Android | +| `desktop/src-tauri/src/engine.rs` | `CallEngine` wrapper | +| `desktop/src-tauri/src/history.rs` | Persistent call history store with dedup | +| `crates/wzp-client/src/audio_io.rs` | CPAL capture + playback (baseline) | +| `crates/wzp-client/src/audio_vpio.rs` | macOS VoiceProcessingIO capture (AEC) | +| `crates/wzp-client/src/audio_wasapi.rs` | Windows WASAPI communications capture (AEC) | +| `vendor/audiopus_sys/opus/CMakeLists.txt` | Patched libopus for clang-cl SIMD | +| `scripts/Dockerfile.windows-builder` | Windows cross-compile Docker image | +| `scripts/build-windows-docker.sh` | Remote Docker build pipeline | +| `scripts/build-windows-cloud.sh` | Hetzner VPS alternative pipeline | +| `scripts/build-linux-docker.sh` | Linux x86_64 relay/CLI build pipeline | diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3c2adfc..390652e 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -457,3 +457,52 @@ Auto mode uses three tiers (Good, Degraded, Catastrophic). It does not use the S When you select a specific profile (not Auto), adaptive switching is disabled. The encoder stays at the selected profile regardless of network conditions. This is useful when you know your network quality and want consistent encoding, or when you want to force a specific bitrate. Note: The decoder always accepts all codecs. A manual quality selection only affects what you send, not what you receive. + +## Direct 1:1 Calling (Desktop + Android) + +In addition to room-mode group calls, you can place direct calls to a specific peer by fingerprint. Direct calls bypass room state entirely — the relay is used purely as a signaling gateway and for media relay. There is no need for the callee to join a room beforehand; they just need to be registered with the same signal hub. + +### UI elements in the direct-call panel + +- **Place call field** — paste a fingerprint (the long hex string you see under your own identity) and click Call. The callee sees a ringing UI. +- **Recent contacts row** — a horizontal strip of chips showing your most recently called/receiving peers. Click a chip to re-dial. Aliases are shown if the peer has one, otherwise a short fingerprint prefix. +- **Call history list** — every direct call you've placed, received, or missed, with direction indicator (↗ Outgoing, ↙ Incoming, ✗ Missed), the peer's alias (if known) or fingerprint prefix, and a timestamp. Click an entry to re-dial. +- **Deregister button** — drops your signal-hub registration without quitting the app. Useful when switching identities (e.g. testing with two accounts on one machine) or when you want to explicitly appear offline to peers. +- **Clear history button** — wipes the call history store. Does not affect current calls. + +### Live updates + +The call history updates in real time across all views via Tauri events (`history-changed`). Placing, answering, or missing a call immediately refreshes the history list and the recent contacts row — no manual refresh needed. + +### Default room + +On first launch, the room name in the room-mode panel defaults to `general` (changed from the prior `android` default so the desktop and Android clients don't silently talk past each other). You can still change it to any room name, and the last-used room is remembered across launches. + +### Random alias + +New installations derive a human-friendly alias from your identity seed — something like `silent-forest-41` or `bold-river-07`. It's deterministic, so reinstalling without changing your seed gives you the same alias. The alias is shown alongside your fingerprint in the header and is what peers see in their call history when they receive your call. + +You can override the alias in Settings → Identity if you want a specific name. + +## Windows AEC Variants + +The Windows desktop build ships in two variants for echo cancellation, depending on which backend you want to exercise. Both are `wzp-desktop.exe` binaries — only the internal audio backend differs. + +| Build | File | Capture backend | AEC | When to use | +|---|---|---|---|---| +| **noAEC baseline** | `wzp-desktop-noAEC.exe` | CPAL (WASAPI shared mode) | None | Headphone-only use, or for A/B comparison against the AEC build | +| **Communications AEC** | `wzp-desktop.exe` | Direct WASAPI with `AudioCategory_Communications` | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + noise suppression + automatic gain control) | Any speaker-mode call, laptop built-in speakers, anywhere echo is audible | + +**Quality caveat**: the communications AEC operates at the OS level and its algorithm depends on the audio driver's installed APO chain. On modern consumer laptops with Intel Smart Sound, Dolby, recent Realtek, or Windows 11 Voice Clarity, the quality is excellent (effectively matching what Teams/Zoom deliver). On generic class-compliant USB microphones or older drivers, the communications APO may not be present at all — in that case the build behaves identically to the noAEC baseline. + +If you hear echo on the AEC build, try these in order before escalating: + +1. **Check which capture device is selected as "Default Device - Communications"** in Windows Sound Settings → Recording tab. Right-click any device to set it. The AEC build opens the device marked as `eCommunications`, not `eConsole`, so changing the default-communications device changes what we capture from. +2. **Verify the driver exposes a communications APO**. Sound Settings → Recording → your mic → Properties → Advanced → look for an "Enhancements" or "Signal Enhancements" tab. If it's absent, the driver has no APOs and the AEC build effectively has no AEC. +3. **Try the classic Voice Capture DSP build** when it ships (tracked as task #26). That uses Microsoft's bundled software AEC (`CLSID_CWMAudioAEC`) which works on every Windows machine regardless of driver. + +### Installing the Windows builds + +1. Windows 10: install the [WebView2 Runtime Evergreen Bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) first. Windows 11 has it pre-installed. +2. Copy `wzp-desktop.exe` (or `wzp-desktop-noAEC.exe`) to any directory and double-click. No installer needed. +3. First launch creates the config + identity store at `%APPDATA%\com.wzp.phone\`.