Steven
Steven7 min de lectura

El bug de los auriculares: cuando la otra persona se esfumó de nuestra transcripción

La transcripción de doble canal funcionaba a la perfección con los altavoces del portátil y con auriculares de diadema. Entonces alguien conectó unos auriculares intraurales, y la persona con la que hablaba simplemente desapareció de la transcripción — porque Chromium silenció nuestro audio sin avisar en el instante en que hablaba. Once arreglos fallaron. El duodécimo cambió toda la arquitectura.

Ingeniería
Audio
Transcripción
Lanzamientos de GeekBye
El bug de los auriculares: cuando la otra persona se esfumó de nuestra transcripción

GeekBye transcribe los dos lados de una conversación: tu micrófono en un canal, el audio de la otra persona (lo que reproduce tu ordenador) en otro. Funcionaba impecablemente con los altavoces del portátil. Funcionaba con auriculares de diadema. Entonces un usuario conectó unos auriculares intraurales, y la persona con la que hablaba se esfumó de la transcripción. Ni distorsionada. Ni mal atribuida. Desaparecida.

Esta es la historia de GeekBye v1.6.12, el bug que necesitó once arreglos fallidos, y el momento en que nos dimos cuenta de que estábamos peleando contra el navegador en vez de trabajar con él.

La pista que estaba viva y vacía

El síntoma era inquietante. Empezabas una reunión, todo parecía correcto, decías una sola palabra — y desde ese instante, el canal de audio de la otra persona caía a puro silencio digital. Lo medimos: la señal de audio del sistema estaba en un nivel de energía normal (un RMS en torno a 0.03–0.06), y en el momento en que el micrófono captaba tu voz, se desplomaba a 0.0000 y ahí se quedaba el resto de la sesión.

Aquí está la parte que nos costó días. Todas las APIs del navegador insistían en que la pista de audio estaba perfectamente sana. readyState: live. enabled: true. muted: false. Según cada indicador de estado que la plataforma exponía, el flujo estaba bien. Eran las muestras las que estaban muertas — una tubería viva que no transportaba más que ceros. Pasamos mucho tiempo sin creernos nuestro propio medidor de energía, porque la versión oficial decía que la pista funcionaba.

El detonante era específico de los auriculares intraurales, y la razón es física: un auricular intraural es un solo dispositivo que es a la vez tu entrada de micrófono y tu salida de audio. Los altavoces del portátil y los auriculares de diadema separan ambas cosas — la salida va a un sitio, el micrófono está en otro. Pero cuando la entrada y la salida son el mismo dispositivo, tienes, por definición, un camino de retroalimentación. Nuestra inferencia firme (esto vive en las tripas del navegador, así que no podemos citar capítulo y versículo) es que Chromium detecta esa forma de micrófono-más-altavoz-en-un-solo-dispositivo, decide que es un bucle de retroalimentación a punto de chillar, y silencia la captura de forma protectora — en silencio, sin decírselo a la app a través de ninguno de los indicadores que una app puede leer.

Once arreglos, varios de ellos peores

Lo atacamos primero por las vías obvias, y anotamos cada intento. Desactivar la cancelación de eco, la supresión de ruido y el control automático de ganancia en ambos flujos. Iniciar los dos flujos de forma secuencial con un retardo en vez de juntos. Dar a cada flujo su propia frecuencia de muestreo. Añadir un nodo de ganancia silencioso. Enrutar todo a través de un único contexto de audio compartido. Pasar a un AudioWorklet. Bajar hasta el propio procesador de pista en crudo de WebCodecs.

Once enfoques. No solo fallaron — varios lo empeoraron. Uno introdujo un retardo de 30 segundos. Otro hizo que la transcripción de tu micrófono apareciera etiquetada como la de la otra persona. Estábamos metidos hasta el fondo, tratando cada síntoma, y el bug seguía moviéndose.

El punto de inflexión fue un replanteamiento: cada arreglo hasta entonces intentaba mantener sanos dos flujos separados. Pero dos flujos separados eran exactamente lo que la heurística de retroalimentación de Chromium podía ver y silenciar. ¿Y si no hubiera nada que comparar?

El arreglo: dale al navegador un flujo, no dos

v1.6.12 dejó de pelear y cambió la arquitectura. En lugar de enviar el micrófono y el audio del sistema como dos flujos independientes, los mezcla en un único flujo mono en el cliente antes de que nada salga de la app. Cuando ambas fuentes fluyen por una sola tubería, las pistas no pueden morir de forma independiente — ya no hay una forma de retroalimentación de dos dispositivos que el navegador pueda detectar y silenciar. La condición del detonante simplemente deja de existir.

Hubo una recompensa encantadora. Dos flujos significaban dos WebSockets de cliente a backend y dos conexiones de backend a proveedor; un único flujo mezclado colapsó eso a uno y uno. Arreglar el bug de los auriculares redujo a la mitad nuestro coste de transcripción como efecto colateral.

Pero la mezcla mono tiene un problema obvio: si fusionas a las dos personas en un solo canal, ¿cómo sigues sabiendo quién dijo qué? Has tirado a la basura la propia separación que te lo decía.

Reconstruir el "quién habló" a partir de la energía

La respuesta fue medir el volumen de cada fuente antes de mezclarlas, y adjuntar una pista a cada fragmento de audio indicando qué fuente dominaba — micrófono, sistema o silencio. Gana la fuente más fuerte. Bastante sencillo con altavoces.

Los auriculares intraurales necesitaban una capa más, y aquí es donde el eco vuelve a la historia — apuntando en sentido contrario al que imaginarías. Con auriculares intraurales, la voz de la otra persona se escapa del auricular y vuelve a entrar en el micrófono. Así que sin cuidado, su eco, llegando a tu canal de micrófono, se atribuye a ti. El arreglo es un conjunto de umbrales conscientes del eco: cuando el audio del sistema suena por encima de un pequeño suelo, el listón que tu micrófono tiene que superar para contar como "tú hablando" se dispara hacia arriba — de un normal 0.015 a 0.1. Dicho de otro modo: mientras la otra persona habla, tu micrófono tiene que ser clara e inequívocamente más fuerte que su eco antes de que atribuyamos las palabras a ti. Los fotogramas ambiguos van por defecto a "sistema", suponiendo que son eco. Es un sesgo deliberado que acierta la atribución en la situación exacta que antes la erraba.

Esa prueba de concepto — mezcla mono más detección de voz basada en energía y consciente del eco — es el antepasado del sistema de atribución que GeekBye ejecuta hoy. El andamiaje se arrancó en el mismo lanzamiento una vez que demostró su valía, y la matemática de la energía después se endureció en un módulo debidamente testeado. Pero v1.6.12 es donde nació la idea, bajo presión, gracias a un usuario con un par de auriculares intraurales.

Tres cosas que enseñó el bug de los auriculares

  1. Un manejador sano puede transportar datos muertos. Toda la investigación giró en torno a confiar en nuestro medidor de energía por encima de los indicadores de estado de la plataforma — live, enabled, unmuted, todos mintiendo mientras las muestras eran ceros. Monitoriza la carga útil, no el autoinforme de la API. Si un flujo dice que está bien, mide si realmente lo está.
  2. Cuando la plataforma pelea contra tu arquitectura, cambia la arquitectura — no la plataforma. No podíamos ganarle la discusión a la protección de retroalimentación de Chromium, y cada intento de desactivarla por los bordes falló o salió por la culata. Eliminar la condición observable — dos flujos convirtiéndose en uno — hizo que todo el problema desapareciera. Casi siempre es más barato eliminar el detonante que ganar una pelea con el navegador.
  3. Cuando colapsas canales por robustez, debes reconstruir lo que destruiste. Mezclar a mono mató la separación de canales que transportaba el "quién habló". Esa información tuvo que rederivarse de la energía previa a la mezcla y de umbrales conscientes del eco. La lógica compensatoria no es una ocurrencia tardía del arreglo — es la mitad del arreglo.

Este es el cuarto capítulo de la historia de fiabilidad que se convierte en GeekBye v2. Para el capítulo anterior, mira una conexión caída no debería tumbar toda tu app (v1.6.8); para la reescritura de la tubería sobre la que se construyó este trabajo de audio, borramos 5.000 líneas de código de audio (v1.6.0); y para todo el arco, la anatomía de publicar software hasta la perfección.