Skip to main content

Echoff

PyPI Python License: MIT Typed Live capture

Echo off. Clean microphone on.

Stops your voice agent from transcribing its own voice.

When your agent speaks through the selected output device, its playback can leak into the microphone and reach speech recognition as if you had said it. Echoff synchronizes that system-audio loopback with microphone capture, then uses WebRTC acoustic echo cancellation (AEC) to reduce the playback before your application receives the microphone stream. It is a Python library and CLI, not a virtual microphone, VAD, ASR, TTS, or conversation system. Applications receive matched reference, raw microphone, and echo-reduced microphone PCM.

Hear the difference

A 20-second comparison presents the same six-second recording three ways: raw microphone, Echoff's echo-reduced output, and the matching computer-audio reference. Listen with headphones.

If the player is unavailable, open the comparison video.

The raw-microphone and Echoff-output tracks use the same +8 dB monitor gain so quiet details remain audible. The computer-audio reference is unmodified.

Note: Echoff 0.3.0 is alpha software. Built-in live capture is hardware-tested on Windows 10/11 and Ubuntu with PipeWire. The macOS capture backend is not implemented. Metrics are diagnostics, not a substitute for listening to the raw and AEC tracks on your own hardware.

Support at a glance

Platform Built-in live capture Processor-only use with aligned PCM
Windows 10/11 Supported: WASAPI loopback reference + WASAPI microphone; optional strict WDM-KS microphone fallback Supported
Linux Supported: PipeWire sink-monitor reference + ordinary source microphone via pactl, pw-dump, and pw-record Supported
macOS No built-in capture backend Processor-only use with application-owned aligned PCM

Python 3.11 or newer is required. See Platform support for the exact boundary between portable processing and platform-specific capture.

Windows quickstart

Create an isolated environment and install the published package:

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install echoff

List the endpoints Echoff can select:

.\.venv\Scripts\python.exe -m echoff devices

Then record a 20-second raw-vs-clean comparison:

.\.venv\Scripts\python.exe -m echoff record --duration 20

While it runs, play continuous speech or music through the selected output device for at least ten seconds. The command reports whether its default echo-path readiness heuristic sees a sufficiently active, paired echo path. Speak during a separate part of the run if you also want to listen for near-end speech preservation.

For a repeatable test, provide a known speech WAV. This requires ffplay:

.\.venv\Scripts\python.exe -m echoff record `
  --play-wav C:\audio\known-speech.wav `
  --repetitions 1

On Windows, a silent output endpoint may produce no WASAPI loopback callbacks. Echoff then stops after its synchronization reserve instead of manufacturing a fake reference. Play system audio or use --play-wav when testing live capture.

The command prints the artifact directory and writes the three tracks to compare:

computer_audio.wav  # captured render-endpoint reference
microphone_raw.wav  # microphone before AEC
microphone_aec.wav  # microphone after AEC

It also writes received-payload tracks, lifecycle events, effective configuration, summary, analysis, and logs. See Capture artifacts for the complete schema.

A technically clean smoke test has status: "completed", equal frame counts in the three primary tracks, and no reported source failure, unsafe overflow, or hard discontinuity. AEC effectiveness still requires listening to microphone_raw.wav and microphone_aec.wav.

Listen first to microphone_raw.wav and microphone_aec.wav. Speaker playback should be lower in the AEC track while your own speech remains intelligible. The whole-run level difference is only descriptive when real microphone speech is present; use a controlled far-end-only window before calling a number echo suppression. The hardware probe guide shows the repeatable path.

Linux quickstart

Install the PipeWire command-line tools. On Ubuntu:

sudo apt update
sudo apt install pipewire-bin pulseaudio-utils

Create an isolated environment and install the published package:

python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install echoff

List the available sink monitors and microphone sources, then record the same raw-vs-clean comparison:

.venv/bin/python -m echoff devices
.venv/bin/python -m echoff record --duration 20

PipeWire's default sink monitor and source are selected automatically. If they are not the physical stereo and microphone you want, pass their displayed indexes with --reference-device and --microphone-device. See the Linux getting-started guide for explicit routing and repeatable playback probes.

Why Echoff exists

System loopback and microphone devices start independently. Matching their first callbacks by arrival order can pair audio from different moments, leaving WebRTC with the wrong echo reference even when both streams have the same block count. Echoff instead:

  1. captures the render reference and microphone on separate streams;
  2. maps each stream's source timing into one local monotonic domain;
  3. establishes one sequence offset at startup and then treats received sample order as authoritative;
  4. retires and counts leading microphone blocks that have no matching reference in the processed timeline (startup_unpaired_microphone_blocks); when artifacts are enabled, their raw payload is preserved in microphone_received.wav;
  5. waits symmetrically for any later missing counterpart; and
  6. submits each reference frame immediately before its matching microphone frame.

This is the capture-and-alignment layer that a bare AEC wrapper does not provide.

Choose the right API

Audio-source situation Use Why
Echoff should open the system-output loopback and microphone AecCapture Echoff captures, timestamp-aligns, processes, and optionally records both streams
exact time-aligned reference/microphone pairs WebRtcAecProcessor One atomic call preserves reference-before-microphone ordering
exact pairs with arbitrary block boundaries BufferedWebRtcAecProcessor Buffers partial 10 ms WebRTC frames and flushes the final tail
deterministic streams on one shared clock StreamingWebRtcAecProcessor Accepts continuous reference and microphone input separately; performs no timestamp alignment

Do not use the streaming adapter for two independently clocked physical devices. Use AecCapture or align the streams before calling a processor.

Minimal application skeleton

on_frame and on_reference run on Echoff's pairing thread, so move application work to a queue and poll capture health from the application loop:

import time
from queue import Empty, Queue

from echoff import AecCapture, AecConfig, AecFrame

frames: Queue[AecFrame] = Queue()


def handle_clean_audio(samples: tuple[float, ...]) -> None:
    """Replace this with your VAD, recorder, stream, or ASR handoff."""
    pass


capture = AecCapture(AecConfig(), on_frame=frames.put)
capture.start()
try:
    deadline = time.monotonic() + 20
    while time.monotonic() < deadline:
        capture.raise_if_failed()
        try:
            frame = frames.get(timeout=0.1)
        except Empty:
            continue
        handle_clean_audio(frame.microphone_clean)
finally:
    capture.stop()

# Surface a device or processing failure that happened near shutdown.
capture.raise_if_failed()

This bounded-duration example uses an unbounded queue for clarity. Production applications need a bounded, non-blocking handoff and an explicit overload policy; blocking the callback stalls Echoff's pairing thread.

AecFrame contains 48 kHz mono floating-point samples in [-1.0, 1.0], the matched reference and raw microphone, canonical processed-timeline end times, nullable per-source observed end times, pair skew, and an AEC state snapshot. The canonical reference_ended_monotonic and microphone_ended_monotonic are equal for the processed pair; pair_skew_s uses observed timing when valid. An AecCapture instance is single-use.

For an application that already owns aligned PCM:

from echoff import AecConfig, WebRtcAecProcessor

reference_10ms = (0.0,) * 480
microphone_10ms = (0.0,) * 480
processor = WebRtcAecProcessor(AecConfig())
clean_microphone = processor.process_pair(reference_10ms, microphone_10ms)

Both inputs must be equal-length, 48 kHz mono floats and contain a whole number of 480-sample (10 ms) frames. Read Integration before wiring physical devices or separate capture clocks.

How the live path works

System output -> platform loopback -> fixed-block queue --+
                                                         +-> align -> WebRTC APM -> AecFrame
Physical mic -> platform capture -> fixed-block queue ----+

At startup, Echoff uses source timing to establish the sequence mapping. Leading microphone blocks without a matching reference are retired from the processed timeline and counted in startup_unpaired_microphone_blocks; they never create a synthetic reference slot or an AecFrame. When artifacts are enabled, the raw payload remains in microphone_received.wav. Once real paired capture has begun, received sample order is authoritative. If either expected head is missing, Echoff waits symmetrically for its counterpart instead of creating synthetic audio. The configured stall reserve defaults to 3 seconds and adds no normal-path latency.

If the reserve expires or a source fails, Echoff marks alignment degraded and suspends unsafe paired AEC output. A proven sequence discontinuity opens one new epoch and resets WebRTC at most once. See Architecture for recovery behavior, timing, and clock boundaries.

With default settings, echo_path_ready requires 7.5 seconds of active paired reference audio, sufficient microphone exposure, and at least 10 dB raw-to-clean reduction sustained for 250 ms over a rolling one-second window of active far-end frames. This is a readiness heuristic, not proof that all echo has been removed. The application decides whether it should gate VAD or barge-in.

Validate on your hardware

For repeatable far-end-only evidence, confirm ffplay -version, choose a speech WAV, remain silent, and run:

.\.venv\Scripts\python.exe -m echoff record `
  --play-wav C:\audio\known-speech.wav `
  --repetitions 3

Echoff preserves all three tracks plus the process-timed stimulus windows. Compare runs only after fixing endpoint, microphone, speaker position, volume, input WAV, and stream-delay setting. Do not tune acceptance thresholds after seeing the result.

Documentation

Privacy

Diagnostic captures may contain private microphone speech and application audio. The default captures\ tree and general WAV/JSONL/log patterns are ignored by this repository; only the three curated demo WAVs under assets/ are explicitly exempt. Custom locations and JSON metadata are not guaranteed to be ignored. They are not encrypted; check git status and review every artifact before sharing.

Development

Clone the repository only when contributing. See Contributing and Development for the editable install, deterministic tests, and hardware-evidence contract.

License

Echoff is open source under the MIT License.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

echoff-0.3.0.tar.gz (95.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

echoff-0.3.0-py3-none-any.whl (65.8 kB view details)

Uploaded Python 3

File details

Details for the file echoff-0.3.0.tar.gz.

File metadata

  • Download URL: echoff-0.3.0.tar.gz
  • Upload date:
  • Size: 95.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for echoff-0.3.0.tar.gz
Algorithm Hash digest
SHA256 dabef27b76084f8d8a134c9c794ae3fa121874948dfc7fc488c8c697ce9372b3
MD5 a3b9118204776106927ccff5ce563103
BLAKE2b-256 37f65c2a943fe0c751ce8d8f10bcaa123e1639c56d238e954981be88ecaab1c8

See more details on using hashes here.

File details

Details for the file echoff-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: echoff-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 65.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for echoff-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a019e8e53cab4d419acf485688bfc3e0812e4f960714a92d7ceb03c5ca41932
MD5 4593c55271bc38ba5ca0ea5f9cbeacd2
BLAKE2b-256 939ccda89fd50ef240c144864657568215e5863931ef61ef83e84e9fe20ee79d

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.1

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page