Files
wz-phone/desktop/src-tauri/Cargo.toml
Siavash Sameni 4ba77c8c0e
Some checks failed
Mirror to GitHub / mirror (push) Failing after 34s
Build Release Binaries / build-amd64 (push) Has been cancelled
feat(linux): WebRTC AEC3 capture/playback backend with render-side tee
Adds gold-standard Linux echo cancellation: in-app WebRTC AEC3 (Audio
Processing Module) via the webrtc-audio-processing crate, using the
same algorithm as Chrome WebRTC, Zoom, Teams, and Jitsi. Runs entirely
in-process, so it works identically on ALSA / PulseAudio / PipeWire
systems — no dependency on user-configured echo-cancel modules.

Architecture:
- New crates/wzp-client/src/audio_linux_aec.rs module (~470 lines).
  Contains LinuxAecCapture and LinuxAecPlayback, both using CPAL
  under the hood but routing samples through a shared
  Arc<webrtc_audio_processing::Processor>. The playback path tees
  each 20 ms frame into APM.process_render_frame as the echo
  reference BEFORE handing the samples to CPAL's output callback.
  The capture path runs APM.process_capture_frame on each mic frame
  in place before pushing to the audio ring buffer. This is the
  "tee the playback ring" approach that Zoom/Teams/Jitsi use.
- New `linux-aec` feature in wzp-client pulling in the
  webrtc-audio-processing crate at v2.x with the `bundled`
  sub-feature. Bundled means the vendored PulseAudio WebRTC C++
  sources are statically compiled via meson+ninja at cargo build
  time — no runtime .so dependency, avoids Debian Bookworm's stale
  libwebrtc-audio-processing-dev 0.3 package (which predates AEC3).
  Dep is target-gated to Linux, so enabling the feature on non-Linux
  is a no-op.
- lib.rs re-exports LinuxAecCapture/LinuxAecPlayback as
  AudioCapture/AudioPlayback when `linux-aec` is on, otherwise
  falls back to the CPAL audio_io path. Shared public API
  (start/ring/stop/Drop) means downstream code is unchanged.
- New `linux-aec` feature in wzp-desktop forwards to
  wzp-client/linux-aec so `cargo tauri build -- --features
  wzp-desktop/linux-aec` builds the AEC variant.

APM configuration:
- EchoCancellation: High suppression, delay-agnostic mode on,
  extended filter on, stream_delay_ms=60 initial hint
- NoiseSuppression: High
- HighPassFilter: on
- AGC: off (can fight Opus encoder's own gain staging + adaptive
  quality controller; add later if users report low mic level)

Frame size handling:
- Pipeline uses 20 ms frames (960 samples @ 48 kHz mono)
- APM requires strict 10 ms (480 samples) per call
- Each 20 ms frame is split into two 480-sample halves, APM called
  twice, halves stitched back
- Same pattern for render and capture sides
- Carry-buffer logic handles the case where CPAL delivers samples in
  arbitrary chunk sizes that don't divide 960

Build infrastructure:
- scripts/Dockerfile.linux-desktop-builder adds meson, ninja-build,
  python3, clang for the webrtc-audio-processing bundled build
- scripts/build-linux-desktop-docker.sh takes a new --aec flag that
  enables the linux-aec feature and renames the output artifacts
  with an `-aec` suffix so noAEC and AEC variants can coexist on disk

Task #30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:53:23 +04:00

108 lines
4.9 KiB
TOML

[package]
name = "wzp-desktop"
version = "0.1.0"
edition = "2024"
description = "WarzonePhone Desktop — encrypted VoIP client"
default-run = "wzp-desktop"
# Library target — required for Tauri mobile (Android/iOS link the app as a cdylib)
# and also used by the desktop binary below.
#
# `staticlib` was DROPPED from crate-type because rust-lang/rust#104707
# documents that having staticlib alongside cdylib leaks non-exported
# symbols from staticlibs into the cdylib. Bionic's private `__init_tcb`
# / `pthread_create` symbols end up bound LOCALLY inside our .so instead
# of resolved dynamically against libc.so at dlopen time — which crashes
# at launch as soon as tao tries to std::thread::spawn() from the JNI
# onCreate callback. The legacy wzp-android crate uses ["cdylib", "rlib"]
# and runs fine on the same phone with the same NDK + Rust toolchain.
#
# iOS Tauri builds that actually need staticlib can re-add it behind a
# target cfg if we ever ship on iOS.
[lib]
name = "wzp_desktop_lib"
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "wzp-desktop"
path = "src/main.rs"
[build-dependencies]
tauri-build = { version = "2", features = [] }
# cc is no longer needed — all C++ moved to crates/wzp-native (built with
# cargo-ndk and loaded via libloading at runtime). wzp-desktop's .so on
# Android is now pure Rust.
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
# WarzonePhone crates — protocol layer is platform-independent
wzp-proto = { path = "../../crates/wzp-proto" }
wzp-codec = { path = "../../crates/wzp-codec" }
wzp-fec = { path = "../../crates/wzp-fec" }
wzp-crypto = { path = "../../crates/wzp-crypto" }
wzp-transport = { path = "../../crates/wzp-transport" }
# wzp-client pulls in CPAL on every desktop target and, additionally on
# macOS, VoiceProcessingIO (coreaudio-rs behind the "vpio" feature). The
# vpio feature MUST NOT be enabled on Windows / Linux because coreaudio-rs
# is Apple-framework-only and will fail to build. Task #24 will add a
# matching Windows Voice Capture DSP path behind its own feature; until
# then, Windows desktops use plain CPAL with AEC disabled.
# macOS: CPAL + VoiceProcessingIO (hardware AEC via Core Audio).
[target.'cfg(target_os = "macos")'.dependencies]
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
# Windows: CPAL for playback + direct WASAPI for capture with OS-level
# AEC (AudioCategory_Communications). The wzp-client `windows-aec`
# feature swaps the default CPAL AudioCapture for a WASAPI one that
# opens the mic under AudioCategory_Communications, turning on Windows's
# communications audio processing chain (AEC, NS, AGC). The reference
# signal for AEC is the system render mix, so echo from our CPAL
# playback is cancelled automatically without extra plumbing.
[target.'cfg(target_os = "windows")'.dependencies]
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "windows-aec"] }
# Linux: CPAL playback+capture baseline. AEC is enabled via the top-level
# `linux-aec` feature in wzp-desktop, which forwards to wzp-client/linux-aec.
# Keeping it opt-in at the wzp-desktop level (rather than forcing it always
# on here) lets `cargo tauri build` produce two variants from the same
# source tree — a noAEC baseline and an AEC build — by toggling the feature
# at build time: `cargo tauri build -- --features wzp-desktop/linux-aec`.
[target.'cfg(target_os = "linux")'.dependencies]
wzp-client = { path = "../../crates/wzp-client", features = ["audio"] }
# Android: no CPAL, no vpio — audio goes through the standalone wzp-native
# cdylib that we dlopen via libloading at runtime. See the wzp_native
# module in src/.
[target.'cfg(target_os = "android")'.dependencies]
wzp-client = { path = "../../crates/wzp-client", default-features = false }
# libloading: runtime dlopen of libwzp_native.so — the standalone cdylib
# crate that owns all C++ (Oboe bridge). Keeps wzp-desktop's .so free of
# any C/C++ static archives that would otherwise leak bionic's internal
# pthread_create into our cdylib and trigger the __init_tcb crash.
libloading = "0.8"
# jni + ndk-context: called from android_audio.rs to invoke
# AudioManager.setSpeakerphoneOn on the JVM side at runtime, so the
# Oboe playout stream (opened with Usage::VoiceCommunication) can route
# between earpiece and loud speaker without restarting.
jni = "0.21"
ndk-context = "0.1"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
# linux-aec: forwards to wzp-client/linux-aec so `cargo tauri build -- --features
# wzp-desktop/linux-aec` enables the WebRTC AEC3 backend on Linux. No-op on
# other targets because wzp-client/linux-aec is itself cfg(target_os = "linux").
linux-aec = ["wzp-client/linux-aec"]