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.

Artículos Relacionados

El silencio era estructural
Steven
Steven8 min de lectura

El silencio era estructural

Las dos últimas releases de GeekBye v1 giran en torno a la misma verdad incómoda: la transcripción en tiempo real sobre una red real no es sin pérdidas, y lo honesto es dejar de fingir que lo es. La v1.8.20 guardaba en disco una copia de cada chunk de audio antes de descartarlo durante una reconexión, y empezó a marcar en voz alta los huecos en la transcripción. La v1.9.0 dejó de enviar silencio para ahorrar ancho de banda — y descubrió que el silencio era justo la señal que el transcriptor usaba para saber que una frase había terminado. Dos releases sobre el coste de tirar cosas.

Ingeniería
Audio
Fiabilidad
Los tres verbos que mantienen vivo el Web Audio
Steven
Steven10 min de lectura

Los tres verbos que mantienen vivo el Web Audio

Dos releases puntuales de GeekBye, con dos meses de diferencia y en dos archivos distintos, le enseñaron a nuestro código de audio la misma lección desde extremos opuestos: dejar de tratar el AudioContext del navegador como desechable. Una release aprendió a hacer resume() de un contexto que macOS había suspendido silenciosamente a mitad de grabación; la otra aprendió a hacer suspend() en vez de close() para que las sesiones consecutivas dejen de estrellarse contra el techo de aproximadamente seis contextos de Chromium. Resume, suspend, close — ese es todo el argumento.

Ingeniería
Audio
Desktop
Borramos 5,000 líneas de código de audio — y las transcripciones empezaron a aparecer dos veces
Steven
Steven7 min de lectura

Borramos 5,000 líneas de código de audio — y las transcripciones empezaron a aparecer dos veces

GeekBye v1.6 arrancó dos transcriptores Swift en local y los reemplazó con un único pipeline unificado — una eliminación neta de más de 5,000 líneas, hecha mientras los usuarios estaban en mitad de una reunión. Entonces las transcripciones empezaron a aparecer dos veces, porque el bug se trasladó a la única capa que seguía creyendo que había dos motores.

Ingeniería
Transcripción
Arquitectura