Switch the webrtc-audio-processing dep from the 2.x git source (bundled
mode) back to crates.io 0.3, and link against Debian's apt package
libwebrtc-audio-processing-dev (0.3-1+b1 on Bookworm). The 2.x path
fails because both the crates.io tarball and the upstream git main
branch of webrtc-audio-processing-sys 2.0.3 have a build.rs bug where
\`meson setup --reconfigure\` is passed unconditionally, panicking on
first-run empty build dirs with "Directory does not contain a valid
build tree". The 0.x line sidesteps bundled mode entirely by linking
the apt-provided library.
Trade-off: we get AEC2 (the older generation) instead of AEC3, but
it's the same algorithm family and is what PulseAudio's
module-echo-cancel and PipeWire's filter-chain use on current
Debian-family distros. Fine for shipping — we can revisit AEC3 once
the 2.x bundled build is fixed upstream.
API changes:
- 0.3's Processor::process_capture_frame and process_render_frame
take &mut self, so wrap the module-level processor in a Mutex.
Capture and playback threads each lock briefly (sub-ms per 10 ms
frame); contention is minimal.
- Import NUM_SAMPLES_PER_FRAME from the crate directly instead of
hardcoding 480, so the code tracks whatever sample rate the
upstream C++ lib exposes (currently 48 kHz hardcoded -> 480).
- Helper fns drain_frames_through_apm / tee_render_samples / etc.
take &Mutex<Processor> instead of &Processor.
- Use explicit EchoCancellationSuppressionLevel and
NoiseSuppressionLevel imports rather than fully-qualified paths.
Dockerfile:
- Drop meson / ninja-build / python3 (only needed for bundled build).
- Add libwebrtc-audio-processing-dev for the system link path.
- Keep clang (may be needed by the bindgen step in some versions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
New Dockerfile and build script for producing wzp-desktop as a Linux
x86_64 binary (plus .deb and .AppImage bundles via tauri-cli).
- scripts/Dockerfile.linux-desktop-builder: thin extension of
wzp-android-builder that adds the Tauri Linux runtime deps
(libwebkit2gtk-4.1-dev, libsoup-3.0-dev, libgtk-3-dev,
libayatana-appindicator3-dev, librsvg2-dev, libglib2.0-dev, patchelf).
Everything else (Rust, Node, cmake, pkg-config, libasound2-dev,
tauri-cli) is inherited from the base image.
- scripts/build-linux-desktop-docker.sh: mirrors the pattern of
build-windows-docker.sh and build-linux-docker.sh. Ships
\`cargo tauri build\` which produces target/release/wzp-desktop
plus bundles under target/release/bundle/{deb,appimage}/. Uploads
the .deb (or raw binary if bundling fails) to rustypaste and
notifies ntfy.sh/wzp on start + completion. Downloads all three
artifact types (raw binary, .deb, .AppImage) to target/linux-desktop/
when they exist.
Image cache volumes are shared with the Android pipeline for cargo
registry + git, but the target dir is in its own cache-linux-desktop/
path to avoid stomping on the Android / Linux-CLI / Windows target
caches.
Branch default is feat/desktop-audio-rewrite (where the actual
wzp-desktop source lives), not feat/android-voip-client.
Task #29.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>