Por Qué Revertimos AudioWorklets: AEC y Echo en Voice AI

Voice AIAudioWorkletsWebAudioAEC6 min de lectura
Por Qué Revertimos AudioWorklets: AEC y Echo en Voice AI

Por Qué Revertimos AudioWorklets: AEC y Echo en Voice AI

En nuestro post anterior documentamos cómo migramos de AudioBufferSourceNode a un AudioWorklet con ExpandableBuffer para eliminar gaps, reducir latencia y resolver race conditions en nuestro pipeline de Voice AI.

Todo funcionaba perfecto. En nuestras pruebas. Con audífonos.

Cuando el SDK llegó a producción y los usuarios empezaron a usar Voice AI desde el speaker de su laptop o celular, empezamos a recibir un reporte consistente: el agente se respondía a sí mismo en un loop infinito.

El síntoma

Un usuario activaba Voice AI, hacía una pregunta, el agente respondía... y luego seguía hablando solo. Se interrumpía, respondía a lo que acababa de decir, se interrumpía otra vez. Un loop de echo que no paraba hasta que el usuario muteaba el micrófono.

Con audífonos: perfecto. Sin audífonos: inutilizable.

Acoustic Echo Cancellation (AEC)

Los browsers implementan AEC como parte del pipeline de getUserMedia cuando pasas echoCancellation: true. Lo que hace es simple en concepto: el browser sabe qué audio está saliendo por el speaker, y lo resta de la señal del micrófono. Así el audio del agente no se re-captura como input.

typescript
const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,  // AEC activado
    noiseSuppression: true,
    autoGainControl: true,
  },
});

El problema es cómo el browser sabe qué audio está saliendo. AEC opera dentro del pipeline nativo de WebAudio. Cuando el audio fluye por el path estándar (AudioBufferSourceNodedestination), el módulo de AEC tiene visibilidad completa de la señal de referencia.

Cuando usas un AudioWorklet para playback, el audio sale por el speaker pero el módulo de AEC del browser no lo registra como señal de referencia. El worklet vive en su propio thread, procesando audio sample por sample, y el resultado bypasea el punto donde AEC captura su referencia.

El micrófono captura el audio del speaker. AEC no lo cancela porque no sabe que ese audio existe. Nova Sonic lo interpreta como nuevo input del usuario. El agente responde. El speaker reproduce la respuesta. El mic la captura. Loop.

El intento fallido: audio gate

Antes de entender la raíz del problema, intentamos un fix rápido en SDK v0.0.12: mutear el micrófono mientras el agente habla.

typescript
// v0.0.12 — NO hagas esto
if (status === "speaking") {
  micTrack.enabled = false;  // Mutear durante playback
} else {
  micTrack.enabled = true;   // Re-activar cuando termina
}

Esto eliminaba el echo, pero mataba barge-in. Los usuarios no podían interrumpir al agente. Y barge-in es fundamental para una conversación natural — es lo que separa a un voice AI de un contestadora automática.

AWS es explícito en su documentación: el micrófono nunca debe mutearse en un sistema full-duplex. Revertimos en 24 horas.

La solución: volver a createBufferSource

En SDK v0.0.13, revertimos el playback de AudioWorklet a una cola de AudioBufferSourceNode:

typescript
private enqueueChunk(float32: Float32Array) {
  const ctx = this.audioContext;
  const buffer = ctx.createBuffer(1, float32.length, 24000);
  buffer.getChannelData(0).set(float32);

  const source = ctx.createBufferSource();
  source.buffer = buffer;
  source.connect(ctx.destination);

  // Schedule after the last chunk ends
  const startTime = Math.max(ctx.currentTime, this.nextStartTime);
  source.start(startTime);
  this.nextStartTime = startTime + buffer.duration;

  source.onended = () => this.onChunkEnded();
}

La diferencia clave con nuestra implementación original (pre-worklet) es que ahora usamos scheduling preciso con source.start(startTime) en lugar de encadenar callbacks con onended. Esto elimina los gaps entre chunks sin necesitar un worklet.

Y lo más importante: AudioBufferSourceNode pasa por el pipeline nativo del browser. AEC tiene visibilidad completa. El echo desaparece.

¿AWS no tiene este problema?

Revisamos todas las implementaciones oficiales de AWS para Nova Sonic:

RepositorioPlaybackAEC?
sample-voicebot-nova-sonicAudioWorklet + ExpandableBufferNo
amazon-nova-samples (12+ variantes)AudioWorkletNo
sample-amazon-nova-sonic-chat-companionAudioWorkletNo
sample-nova-sonic-websocket-agentcorecreateBufferSourceSí (nativo)

Todas las demos de AWS con AudioWorklet asumen audífonos. Ninguna resuelve AEC para speakers abiertos.

Encontramos un thread en AWS re:Post titulado "Echo Cancellation for open speaker deployments" que retornaba 403 — probablemente porque es un problema abierto sin solución oficial.

El AudioRecorderProcessor que aparece en algunos samples de AWS no es un módulo de AEC. Es un passthrough simple que envía muestras al main thread para visualización (indicadores de volumen, waveforms). No tiene nada que ver con cancelación de echo.

¿Es createBufferSource deprecated?

No realmente. createBufferSource() sigue en la spec de Web Audio API y funciona en todos los browsers. Lo que fue deprecated hace años es webkitAudioContext, que es otra cosa.

AudioBufferSourceNode no va a desaparecer. Es parte fundamental de la API y no hay señales de deprecación en ningún browser.

¿Hay forma de usar AudioWorklet con AEC?

Tres opciones teóricas, ninguna práctica hoy:

  1. Filtro adaptativo manual (LMS/NLMS): Implementar echo cancellation dentro del worklet tomando la señal de playback como referencia y restándola del mic. Es un proyecto de DSP serio — estamos hablando de filtros de cientos de taps con adaptación en tiempo real. No es un weekend project.

  2. Insertable Streams / MediaStreamTrack Processor: APIs más nuevas que eventualmente podrían dar acceso al pipeline de AEC desde código. Pero aún no son estándar para este caso de uso.

  3. Esperar a que los browsers lo resuelvan: Que el módulo de AEC tenga visibilidad de AudioWorklets nativamente. No hay propuestas activas para esto.

Lo que aprendimos

El hardware del usuario define tu arquitectura. Nuestras pruebas con audífonos nos dieron una falsa confianza. Voice AI en producción se usa en laptops, celulares, y tablets — todos con speakers abiertos donde AEC es crítico.

Las demos oficiales no son producción. AWS publica implementaciones que resuelven sus problemas de demostración. El echo en speakers abiertos no es un problema en una demo en vivo con audífonos.

A veces el approach "viejo" es el correcto. createBufferSource con scheduling preciso nos da playback sin gaps, AEC funcional, y cero echo. No es tan elegante como un worklet con ExpandableBuffer, pero funciona en el mundo real.

La lección más importante: testea como tus usuarios usan tu producto, no como tú lo usas.


Prueba Voice AI

Voice AI de Formmy funciona en speakers abiertos sin echo, con barge-in natural y latencia mínima.

Prueba Formmy Gratis — Agentes de voz que funcionan en el mundo real.

¿Construyendo voice AI y lidiando con echo? Escríbenos — compartimos lo que aprendimos.