● Open sourceFull-Duplex Conversational Audio

Duplex.

Barge-in without muting the microphone

Every iOS voice agent hears itself. The usual fix is muting the mic while the agent talks — which removes self-interruption by removing the one thing that makes a voice agent tolerable.

Swift 6CoreMLZero deps47 tests
Platform iOS 18 · macOS 15Tests 47License MIT
Duplex — swift test
$ swift test
✓ 2,000,000 samples, two threads
✓ none torn, none reordered
✓ echo delay ±1 sample
✓ 47 tests passed
01 The problem

A caller who cannot interrupt hangs up.

If they cannot cut the agent off mid-sentence with "no, I need someone today", they stop talking to it. But leave the microphone open and the agent hears its own voice coming back and interrupts itself. Both symptoms, one bug.

02 The hard part

Where the work actually was.

You already know what your own echo should sound like. You know exactly what you sent to the speaker, and you can measure how much this device attenuates it. So predict it: recent playback level minus measured echo return loss. If the mic reads roughly that, it is your own voice. Meaningfully louder, and something is in the room you did not play.

1

Real-time safe

The render thread allows no allocation, no locks, no ARC. So: one allocation up front and two atomic integers.

2

Lock-free ring buffer

Single producer, single consumer, acquire/release ordering. Verified against two million samples on real threads.

3

Measured echo tail

A chirp and a cross-correlation, per device. An earpiece and a Bluetooth car kit are not the same acoustic device.

4

Double-talk detection

Several consecutive frames above the predicted echo before declaring barge-in. One frame is a cough.

5

Real endpointing

"My number is four one nine…" is not a finished turn. An unfinished digit run buys 900ms.

6

Gate before VAD

Run the VAD first and its noise floor climbs to meet the agent's own echo, going deaf to the caller.

03 Measured

What it actually does.

All of this runs in CI with no audio hardware — the acoustics are pure functions over sample buffers:

echo delay, planted vs recovered 10ms → ±1 sample 120ms → ±1 sample 30ms → ±1 sample 250ms → ±1 sample spectral flatness voiced speech / tones ~1e-9 steady broadband noise 0.37 – 0.46 ceiling 0.30 ring buffer 2,000,000 samples, two real threads 0 torn, 0 reordered, 0 dropped
04 Engineering

How it's built.

LanguageSwift 6, strict concurrency
AtomicsSynchronization.Atomic
SpectrumWelch's method, dependency-free
State@Observable, SwiftUI-bindable
Tests47, no hardware required
Dependenciesnone
05 Honestly

What it doesn't do.

No echo cancellation — it assumes you are running VoiceProcessingIO and solves the gating problem AEC leaves behind. No ASR, no TTS, no networking. And no neural VAD: if you want Silero, run it on the frames that survive the gate, which is the right place for it.

Want a closer look?

The README goes deeper, and the tests are the honest documentation. Happy to walk through the trade-offs.