
Khi xây dựng voice assistant, bạn sẽ gặp ngay vấn đề: micro thu lại giọng của chính assistant đang phát qua loa, tạo vòng lặp echo vô hạn. Đây là bài toán Acoustic Echo Cancellation (AEC) — một lĩnh vực xử lý tín hiệu số đã tồn tại hơn 50 năm nhưng vẫn cực kỳ quan trọng trong thời đại AI voice.
Bài viết này giải thích AEC hoạt động như thế nào, cách browser và thiết bị phần cứng xử lý, và những bài học từ việc tích hợp AEC vào Assistant Core trên cả web lẫn ESP32.
Vấn đề: Vòng lặp Echo
Hình dung: bạn hỏi assistant một câu. Assistant phát câu trả lời qua loa. Micro đang mở thu lại giọng assistant. Server nhận audio này, nghĩ đó là user nói, xử lý và phát tiếp... Kết quả là assistant nói chuyện với chính mình.
Với các model realtime (OpenAI Realtime, Gemini Live, xAI Grok), vấn đề càng nghiêm trọng hơn vì micro luôn mở trong chế độ full-duplex. Không như auto mode — nơi mic tắt khi TTS đang phát — realtime mode truyền audio liên tục cả hai chiều.
AEC hoạt động như thế nào?
AEC dựa trên một ý tưởng đơn giản: nếu biết âm thanh nào đang phát qua loa, ta có thể trừ nó ra khỏi tín hiệu micro. Quy trình gồm 3 bước:
- Reference signal — Lấy audio đang gửi đến loa làm tín hiệu tham chiếu
- Adaptive filter — Mô hình hóa cách âm thanh truyền từ loa → phòng → micro (echo path)
- Subtraction — Trừ echo ước tính ra khỏi tín hiệu micro, chỉ giữ lại giọng user
# Simplified AEC pipeline
def aec_process(mic_signal, reference_signal, filter_state):
# 1. Adaptive filter dự đoán echo
estimated_echo = adaptive_filter(reference_signal, filter_state)
# 2. Trừ echo khỏi mic
clean_signal = mic_signal - estimated_echo
# 3. Cập nhật filter (NLMS algorithm)
error = mic_signal - estimated_echo
filter_state = update_nlms(filter_state, reference_signal, error)
return clean_signal, filter_state
Thuật toán NLMS (Normalized Least Mean Squares) được dùng phổ biến nhất. So với LMS gốc, NLMS chuẩn hóa step size theo năng lượng tín hiệu — giúp hội tụ nhanh hơn và ổn định hơn khi âm lượng thay đổi liên tục (đặc thù của giọng nói).
Ngoài ra, sau khi trừ echo tuyến tính, vẫn còn residual echo do biến dạng phi tuyến (loa rẻ, clipping). Bước Non-Linear Processing (NLP) sẽ phát hiện và suppress phần echo còn sót lại.
AEC trên Browser — Miễn phí và đủ tốt
Tin tốt: trên web, bạn không cần tự implement AEC. Browser (Chrome, Firefox, Safari) đã tích hợp AEC thông qua Web Audio API:
// Bật AEC khi capture micro
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true, // AEC
noiseSuppression: true, // Noise reduction
autoGainControl: true, // Normalize volume
}
})
// Chrome sử dụng AEC3 — adaptive filter trong frequency domain
// Reference signal = audio đang phát qua speaker
// Tất cả xử lý ở kernel level, không cần code thêm
Chrome cụ thể sử dụng AEC3 — module xử lý trong frequency domain (FFT) thay vì time domain, cho phép mô hình hóa phòng phức tạp hiệu quả hơn. Quy trình:
- Capture reference signal từ audio output device
- Adaptive filter (PBFDAF) ước tính room impulse response
- Trừ echo và áp dụng Non-Linear Processor cho residual
Bạn có thể debug AEC trên Chrome tại chrome://webrtc-internals — xem trạng thái AEC real-time và tải raw audio dumps để phân tích.
AEC trên ESP32 — Phức tạp hơn nhiều
ESP32 không có browser, không có AEC sẵn. Bạn phải tự xử lý, và có 3 hướng:
| Giải pháp | RAM | Chất lượng | Thiết bị |
|---|---|---|---|
| ESP-ADF aec_stream | ~50KB | Tốt | ESP32-S3 + dual mic |
| Codec hardware AEC | ~0KB | Rất tốt | Board có ES8311/ES7210 |
| Mute mic khi phát | ~0KB | Cơ bản | Mọi board (half-duplex) |
ESP-ADF (Espressif Audio Development Framework) cung cấp algorithm_stream với AEC tích hợp. Điểm quan trọng là reference signal phải khớp chính xác với audio đang phát qua loa — nếu bị delay, compress, hay biến dạng, adaptive filter sẽ không hội tụ.
// ESP-ADF: cấu hình AEC cho ESP32-S3
algorithm_stream_cfg_t algo_cfg = {
.input_type = ALGORITHM_STREAM_INPUT_TYPE2,
.algo_mask = ALGORITHM_STREAM_USE_AEC
| ALGORITHM_STREAM_USE_NS // Noise suppression
| ALGORITHM_STREAM_USE_AGC, // Auto gain
.sample_rate = 16000,
.mic_ch = 1,
.ref_ch = 1, // Reference = speaker output
.debug_input = true, // Bật để calibrate delay
};
// Delay calibration: recording nên trễ 0-10ms so với reference
algo_stream_set_delay(algo_stream, 5); // 5ms
Board ESP32-S3-BOX và Korvo-2 có hardware AEC sẵn qua codec chip — hoạt động tốt nhất. Board tự thiết kế với INMP441 + MAX98357A thì cần dùng software AEC hoặc chấp nhận mute mic khi phát (half-duplex).
Realtime AI Provider — Không ai hỗ trợ server-side AEC
Một sự thật quan trọng: cả ba realtime provider lớn đều không xử lý AEC trên server.
| Provider | Server AEC | Server VAD | Ghi chú |
|---|---|---|---|
| OpenAI Realtime | ❌ | ✅ | Có WebRTC endpoint (AEC trong browser) |
| Gemini Live | ❌ | ✅ | VAD rất nhạy — dễ interrupt loop |
| xAI Grok | ❌ | ✅ | Full-duplex, phụ thuộc client AEC |
Hệ quả: nếu echo lọt qua client AEC, server VAD sẽ nhầm echo là giọng user — dẫn đến interrupt loop (model tự ngắt liên tục, đặc biệt với Gemini Live vì VAD rất nhạy).
Bài học thực tế từ Assistant Core
- Web client: 0 dòng code AEC —
echoCancellation: truetronggetUserMedialà đủ. Laptop/Desktop đều có AEC tốt ở hardware + browser level. - ESP32: chọn board cẩn thận — Board có codec chip với reference channel (ES8311) cho AEC tốt nhất. Board đơn giản (INMP441 + MAX98357A) sẽ phải trade-off: software AEC tốn RAM hoặc half-duplex tốn UX.
- Headphone = AEC hoàn hảo — Khi dùng tai nghe, echo bị loại bỏ vật lý. Đây là khuyến nghị tốt nhất cho user gặp vấn đề echo, đặc biệt trên mobile.
- Gemini cần đặc biệt chú ý — VAD sensitivity cao hơn OpenAI/xAI. Nếu gặp interrupt loop, tăng
prefixPaddingMstrong session config giúp buffer thêm thời gian trước khi VAD trigger.
Khi nào cần lo về AEC?
Quy tắc đơn giản:
- Web app trên laptop/desktop → Không cần lo, browser xử lý
- Web app trên mobile (loa ngoài) → AEC yếu hơn, khuyên dùng tai nghe
- ESP32 với loa gần micro → Cần hardware AEC hoặc software AEC
- Bluetooth headset → Có thể có latency cao, AEC đôi khi không compensate đúng
AEC là một bài toán đã có lời giải ở hầu hết platform hiện đại. Việc của developer là hiểu nó hoạt động như thế nào để chọn đúng giải pháp cho từng loại client — không phải tự implement từ đầu.