Steven
Steven7 min de leitura

O bug dos fones: quando a outra pessoa sumiu da nossa transcrição

A transcrição de canal duplo funcionava perfeitamente nas caixas de som do notebook e em fones de ouvido over-ear. Então alguém plugou fones intra-auriculares, e a pessoa com quem falava simplesmente sumiu da transcrição — porque o Chromium silenciou nosso áudio sem avisar no instante em que ela falava. Onze correções falharam. A décima segunda mudou toda a arquitetura.

Engenharia
Áudio
Transcrição
Lançamentos do GeekBye
O bug dos fones: quando a outra pessoa sumiu da nossa transcrição

O GeekBye transcreve os dois lados de uma conversa: seu microfone em um canal, o áudio da outra pessoa (o que seu computador está reproduzindo) em outro. Funcionava impecavelmente nas caixas de som do notebook. Funcionava em fones over-ear. Então um usuário plugou fones intra-auriculares, e a pessoa com quem falava sumiu da transcrição. Nem embolada. Nem mal atribuída. Sumiu.

Esta é a história do GeekBye v1.6.12, do bug que exigiu onze correções fracassadas, e do momento em que percebemos que estávamos brigando com o navegador em vez de trabalhar com ele.

A faixa que estava viva e vazia

O sintoma era perturbador. Você iniciava uma reunião, tudo parecia bem, dizia uma única palavra — e a partir daquele instante, o canal de áudio da outra pessoa caía para puro silêncio digital. Medimos: o sinal de áudio do sistema ficava em um nível de energia normal (um RMS em torno de 0.03–0.06), e no momento em que o microfone captava sua voz, despencava para 0.0000 e ali permanecia pelo resto da sessão.

Aqui está a parte que nos custou dias. Todas as APIs do navegador insistiam que a faixa de áudio estava perfeitamente saudável. readyState: live. enabled: true. muted: false. Por cada flag de status que a plataforma expunha, o fluxo estava bem. Eram as amostras que estavam mortas — um cano vivo carregando nada além de zeros. Passamos muito tempo sem acreditar em nosso próprio medidor de energia, porque a versão oficial dizia que a faixa estava funcionando.

O gatilho era específico dos fones intra-auriculares, e a razão é física: um fone intra-auricular é um único dispositivo que é ao mesmo tempo sua entrada de microfone e sua saída de áudio. As caixas de som do notebook e os fones over-ear separam essas duas funções — a saída vai para um lugar, o microfone fica em outro. Mas quando a entrada e a saída são o mesmo dispositivo, você tem, por definição, um caminho de realimentação. Nossa forte inferência (isso vive nas entranhas do navegador, então não podemos citar capítulo e versículo) é que o Chromium detecta esse formato de microfone-mais-alto-falante-em-um-dispositivo, decide que é um loop de realimentação prestes a chiar, e silencia a captura de forma protetora — em silêncio, sem avisar a aplicação por nenhuma das flags que uma aplicação pode ler.

Onze correções, várias delas piores

Atacamos pelas vias óbvias primeiro, e anotamos cada tentativa. Desativar o cancelamento de eco, a supressão de ruído e o ganho automático em ambos os fluxos. Iniciar os dois fluxos sequencialmente com um atraso em vez de juntos. Dar a cada fluxo sua própria taxa de amostragem. Adicionar um nó de ganho silencioso. Rotear tudo através de um único contexto de áudio compartilhado. Migrar para um AudioWorklet. Descer até o processador de faixa bruto do WebCodecs.

Onze abordagens. Não só falharam — várias pioraram as coisas. Uma introduziu um atraso de 30 segundos. Outra fez a transcrição do seu microfone aparecer rotulada como a da outra pessoa. Estávamos afundados no mato, tratando cada sintoma, e o bug seguia se deslocando.

A virada foi uma nova formulação: cada correção até então tentava manter saudáveis dois fluxos separados. Mas dois fluxos separados eram exatamente o que a heurística de realimentação do Chromium conseguia ver e silenciar. E se não houvesse nada para comparar?

A correção: dê ao navegador um fluxo, não dois

O v1.6.12 parou de brigar e mudou a arquitetura. Em vez de enviar o microfone e o áudio do sistema como dois fluxos independentes, ele os mixa em um único fluxo mono no cliente antes que qualquer coisa saia da aplicação. Quando ambas as fontes fluem por um único pipeline, as faixas não podem morrer de forma independente — não existe mais um formato de realimentação de dois dispositivos para o navegador detectar e silenciar. A condição do gatilho simplesmente deixa de existir.

Houve um bônus encantador. Dois fluxos significavam dois WebSockets de cliente para backend e duas conexões de backend para provedor; um único fluxo mixado colapsou isso para um e um. Corrigir o bug dos fones cortou pela metade nosso custo de transcrição como efeito colateral.

Mas a mixagem mono tem um problema óbvio: se você funde as duas pessoas em um único canal, como ainda sabe quem disse o quê? Você jogou fora a própria separação que lhe dizia isso.

Reconstruindo o "quem falou" a partir da energia

A resposta foi medir o volume de cada fonte antes de mixá-las, e anexar uma dica a cada fragmento de áudio dizendo qual fonte dominava — microfone, sistema ou silêncio. A fonte mais alta vence. Simples o bastante nas caixas de som.

Os fones intra-auriculares precisaram de mais uma camada, e é aqui que o eco volta à história — apontando na direção oposta à que você imaginaria. Com fones intra-auriculares, a voz da outra pessoa vaza do fone e volta a entrar no microfone. Então, sem cuidado, o eco dela, chegando ao seu canal de microfone, é atribuído a você. A correção é um conjunto de limiares cientes do eco: quando o áudio do sistema toca acima de um pequeno piso, a barra que seu microfone tem que superar para contar como "você falando" dispara para cima — de um 0.015 normal para 0.1. Em outras palavras: enquanto a outra pessoa fala, seu microfone tem que ser clara e inequivocamente mais alto que o eco dela antes de creditarmos as palavras a você. Quadros ambíguos vão por padrão para "sistema", presumindo que sejam eco. É um viés deliberado que acerta a atribuição exatamente na situação que antes errava.

Essa prova de conceito — mixagem mono mais detecção de voz baseada em energia e ciente do eco — é a ancestral do sistema de atribuição que o GeekBye roda hoje. O andaime foi arrancado no mesmo lançamento assim que provou seu valor, e a matemática da energia depois se solidificou em um módulo devidamente testado. Mas o v1.6.12 é onde a ideia nasceu, sob pressão, por um usuário com um par de fones intra-auriculares.

Três coisas que o bug dos fones ensinou

  1. Um handle saudável pode carregar dados mortos. Toda a investigação girou em torno de confiar em nosso medidor de energia acima das flags de status da plataforma — live, enabled, unmuted, todas mentindo enquanto as amostras eram zeros. Monitore a carga útil, não o autorrelato da API. Se um fluxo afirma que está bem, meça se ele realmente está.
  2. Quando a plataforma briga com sua arquitetura, mude a arquitetura — não a plataforma. Não conseguíamos vencer a discussão contra a proteção de realimentação do Chromium, e cada tentativa de desativá-la pelas bordas falhou ou saiu pela culatra. Remover a condição observável — dois fluxos virando um — fez todo o problema desaparecer. É quase sempre mais barato remover o gatilho do que ganhar uma briga com o navegador.
  3. Quando você colapsa canais em prol da robustez, precisa reconstruir o que destruiu. Mixar para mono matou a separação de canais que carregava o "quem falou". Essa informação teve que ser re-derivada da energia pré-mixagem e de limiares cientes do eco. A lógica compensatória não é um pensamento tardio da correção — ela é metade da correção.

Este é o quarto capítulo da história de confiabilidade que se torna o GeekBye v2. Para o capítulo anterior, veja uma conexão caída não deveria derrubar toda a sua app (v1.6.8); para a reescrita do pipeline sobre a qual este trabalho de áudio foi construído, apagamos 5.000 linhas de código de áudio (v1.6.0); e para todo o arco, a anatomia de entregar software até a perfeição.

Artigos Relacionados

Apagámos 5,000 Linhas de Código de Áudio — e as Transcrições Começaram a Aparecer em Duplicado
Steven
Steven6 min de leitura

Apagámos 5,000 Linhas de Código de Áudio — e as Transcrições Começaram a Aparecer em Duplicado

O GeekBye v1.6 removeu dois transcritores Swift on-device e substituiu-os por um único pipeline unificado — uma eliminação líquida de mais de 5,000 linhas, feita enquanto as pessoas estavam a meio de reuniões na app. Depois as transcrições começaram a aparecer em duplicado, porque o bug migrou para a única camada que ainda pensava existirem dois motores.

Engenharia
Transcrição
Arquitetura
Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava
Steven
Steven7 min de leitura

Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava

Quando nosso backend ficou offline no meio de uma reunião, ele não só pausou a transcrição — derrubou o app inteiro. A causa era um único evento não tratado, e a correção tinha dez linhas. Este é o grupo de lançamentos que fez o GeekBye continuar logado e continuar conectado através das coisas que antes o matavam.

Engenharia
Confiabilidade
Electron
Como transmitir um relatório ao vivo sem a cintilação
Steven
Steven6 min de leitura

Como transmitir um relatório ao vivo sem a cintilação

Quando a sua reunião termina, o resumo do GeekBye agora se preenche ao vivo em vez de o obrigar a olhar para um spinner. Fazer com que a interface em streaming parecesse calma em vez de trêmula exigiu resolver a cintilação duas vezes — uma para os campos estruturados, outra para o markdown — e a segunda correção foi um renderizador que já tínhamos.

Engenharia
Frontend
Streaming