forked from unom/punktfunk
The Apple client on a loudspeaker was the primary reported echo source: two AVAudioEngines by design (arbitrary mic/speaker pairs, two clocks) meant no unit ever saw render and capture together, which is exactly what setVoiceProcessingEnabled needs. With the mic enabled and the new "Echo cancellation" toggle on (both defaults), playback and capture now share ONE engine with the system voice processor engaged — AEC, noise suppression and AGC — and the input node's other-audio ducking pinned to .min with advanced ducking off, because this is a game stream, not a call: the host's audio must never dip under the outgoing voice. The two-engine path survives where it is the right answer: echo cancel off, macOS sessions with a hand-picked speaker/mic UID or input channel (the voice processor only follows the system default devices, and its capture side is its own mono mix — a per-channel pick can't survive it), and any machine whose voice processor refuses to engage. Every failure falls back to a working configuration rather than silence. The capture chain reads the input format after the processor is enabled, so its mono/lower-rate output flows through the existing converter unchanged; a first-run permission grant swaps the playback-only engine for the combined one in place, reusing the same jitter ring and drain thread; background mic muting mutes the processor's input instead of pausing the shared engine (playback keeps running). echo_cancel rides the full settings plumbing micEnabled has — defaults key, effective settings, profile overlay (serialized as `echo_cancel`; the Rust catalog carries it via unknown-key passthrough until it grows the field), a captioned toggle beside the Microphone row, and a gamepad settings row. tvOS behavior is untouched (playback only, as before). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>