Steven
Steven7 min di lettura

Il bug degli auricolari: quando l'altra persona è sparita dalla nostra trascrizione

La trascrizione a doppio canale funzionava perfettamente con gli altoparlanti del portatile e con le cuffie over-ear. Poi qualcuno ha collegato degli auricolari in-ear, e la persona con cui parlava è semplicemente sparita dalla trascrizione — perché Chromium ha silenziato il nostro audio senza avvisare nell'istante in cui parlava. Undici correzioni sono fallite. La dodicesima ha cambiato tutta l'architettura.

Ingegneria
Audio
Trascrizione
Rilasci di GeekBye
Il bug degli auricolari: quando l'altra persona è sparita dalla nostra trascrizione

GeekBye trascrive i due lati di una conversazione: il tuo microfono su un canale, l'audio dell'altra persona (ciò che il tuo computer sta riproducendo) su un altro. Funzionava alla perfezione con gli altoparlanti del portatile. Funzionava con le cuffie over-ear. Poi un utente ha collegato degli auricolari in-ear, e la persona con cui parlava è sparita dalla trascrizione. Non distorta. Non attribuita male. Sparita.

Questa è la storia di GeekBye v1.6.12, del bug che ha richiesto undici correzioni fallite, e del momento in cui abbiamo capito che stavamo combattendo contro il browser invece di lavorare con lui.

La traccia che era viva e vuota

Il sintomo era inquietante. Avviavi una riunione, tutto sembrava a posto, dicevi una sola parola — e da quell'istante, il canale audio dell'altra persona cadeva nel puro silenzio digitale. L'abbiamo misurato: il segnale dell'audio di sistema stava a un livello di energia normale (un RMS intorno a 0.03–0.06), e nel momento in cui il microfono captava la tua voce, crollava a 0.0000 e ci restava per il resto della sessione.

Ecco la parte che ci è costata giorni. Tutte le API del browser insistevano che la traccia audio fosse perfettamente sana. readyState: live. enabled: true. muted: false. Secondo ogni flag di stato che la piattaforma esponeva, il flusso stava bene. Erano i campioni a essere morti — un tubo vivo che non trasportava altro che zeri. Abbiamo passato molto tempo a non credere al nostro stesso misuratore di energia, perché la versione ufficiale diceva che la traccia funzionava.

L'innesco era specifico degli auricolari in-ear, e la ragione è fisica: un auricolare in-ear è un unico dispositivo che è al tempo stesso il tuo ingresso microfono e la tua uscita audio. Gli altoparlanti del portatile e le cuffie over-ear separano queste due cose — l'uscita va in un posto, il microfono è altrove. Ma quando ingresso e uscita sono lo stesso dispositivo, hai, per definizione, un percorso di feedback. La nostra forte deduzione (questo vive nelle viscere del browser, quindi non possiamo citare capitolo e versetto) è che Chromium rilevi quella forma di microfono-più-altoparlante-su-un-solo-dispositivo, decida che è un loop di feedback sul punto di fischiare, e silenzi la cattura in modo protettivo — in silenzio, senza dirlo all'app tramite nessuno dei flag che un'app può leggere.

Undici correzioni, diverse delle quali peggiori

L'abbiamo attaccato prima nei modi ovvi, e abbiamo annotato ogni tentativo. Disattivare la cancellazione dell'eco, la soppressione del rumore e il guadagno automatico su entrambi i flussi. Avviare i due flussi in sequenza con un ritardo invece che insieme. Dare a ogni flusso la propria frequenza di campionamento. Aggiungere un nodo di guadagno silenzioso. Instradare tutto attraverso un unico contesto audio condiviso. Passare a un AudioWorklet. Scendere fino al processore di traccia grezzo di WebCodecs.

Undici approcci. Non solo sono falliti — diversi hanno peggiorato le cose. Uno ha introdotto un ritardo di 30 secondi. Un altro ha fatto sì che la trascrizione del tuo microfono comparisse etichettata come quella dell'altra persona. Eravamo immersi fino al collo, curando ogni sintomo, e il bug continuava a spostarsi.

La svolta è stata una riformulazione: ogni correzione fino a quel punto cercava di mantenere sani due flussi separati. Ma due flussi separati erano esattamente ciò che l'euristica di feedback di Chromium poteva vedere e silenziare. E se non ci fosse stato nulla da confrontare?

La correzione: dai al browser un flusso, non due

v1.6.12 ha smesso di combattere e ha cambiato l'architettura. Invece di inviare il microfono e l'audio di sistema come due flussi indipendenti, li mixa in un unico flusso mono sul client prima che qualcosa esca dall'app. Quando entrambe le sorgenti scorrono attraverso un'unica pipeline, le tracce non possono morire in modo indipendente — non esiste più una forma di feedback a due dispositivi che il browser possa rilevare e silenziare. La condizione dell'innesco semplicemente smette di esistere.

C'è stato un bonus delizioso. Due flussi significavano due WebSocket da client a backend e due connessioni da backend a fornitore; un unico flusso mixato ha ridotto tutto a uno e uno. Correggere il bug degli auricolari ha dimezzato il nostro costo di trascrizione come effetto collaterale.

Ma il mixaggio mono ha un problema ovvio: se fondi entrambe le persone in un unico canale, come fai a sapere ancora chi ha detto cosa? Hai buttato via proprio quella separazione che te lo diceva.

Ricostruire il "chi ha parlato" a partire dall'energia

La risposta è stata misurare il volume di ogni sorgente prima di mixarle, e allegare a ogni frammento di audio un indizio che dicesse quale sorgente dominava — microfono, sistema o silenzio. Vince la sorgente più forte. Abbastanza semplice con gli altoparlanti.

Gli auricolari in-ear hanno avuto bisogno di uno strato in più, ed è qui che l'eco torna nella storia — puntando nella direzione opposta a quella che immagineresti. Con gli auricolari in-ear, la voce dell'altra persona fuoriesce dall'auricolare e rientra nel microfono. Quindi senza attenzione, la sua eco, che arriva sul tuo canale microfono, viene attribuita a te. La correzione è un insieme di soglie consapevoli dell'eco: quando l'audio di sistema è in riproduzione sopra un piccolo pavimento, l'asticella che il tuo microfono deve superare per contare come "tu che parli" schizza in alto — da un normale 0.015 a 0.1. In altre parole: mentre l'altra persona parla, il tuo microfono deve essere chiaramente e inequivocabilmente più forte della sua eco prima che attribuiamo le parole a te. I frame ambigui vanno per impostazione predefinita a "sistema", nell'ipotesi che siano eco. È un bias deliberato che azzecca l'attribuzione proprio nella situazione che prima la sbagliava.

Quel proof of concept — mixaggio mono più rilevamento vocale basato sull'energia e consapevole dell'eco — è l'antenato del sistema di attribuzione che GeekBye esegue oggi. L'impalcatura è stata smantellata nello stesso rilascio una volta che ha dimostrato il proprio valore, e la matematica dell'energia si è poi consolidata in un modulo adeguatamente testato. Ma v1.6.12 è dove l'idea è nata, sotto pressione, grazie a un utente con un paio di auricolari in-ear.

Tre cose che il bug degli auricolari ha insegnato

  1. Un handle sano può trasportare dati morti. Tutta l'indagine è ruotata attorno al fidarci del nostro misuratore di energia più che dei flag di stato della piattaforma — live, enabled, unmuted, tutti a mentire mentre i campioni erano zeri. Monitora il payload, non l'autodichiarazione dell'API. Se un flusso afferma di stare bene, misura se lo è davvero.
  2. Quando la piattaforma combatte contro la tua architettura, cambia l'architettura — non la piattaforma. Non potevamo vincere la discussione contro la protezione dal feedback di Chromium, e ogni tentativo di disattivarla dai bordi è fallito o si è ritorto contro. Rimuovere la condizione osservabile — due flussi che diventano uno — ha fatto sparire tutto il problema. È quasi sempre più economico rimuovere l'innesco che vincere una battaglia con il browser.
  3. Quando comprimi i canali per la robustezza, devi ricostruire ciò che hai distrutto. Mixare in mono ha ucciso la separazione dei canali che trasportava il "chi ha parlato". Quell'informazione ha dovuto essere ri-derivata dall'energia pre-mixaggio e da soglie consapevoli dell'eco. La logica compensativa non è un ripensamento rispetto alla correzione — è metà della correzione.

Questo è il quarto capitolo della storia di affidabilità che diventa GeekBye v2. Per il capitolo precedente, vedi una connessione caduta non dovrebbe far crashare tutta la tua app (v1.6.8); per la riscrittura della pipeline su cui questo lavoro sull'audio si è basato, abbiamo cancellato 5.000 righe di codice audio (v1.6.0); e per l'intero arco, l'anatomia del rilasciare software fino alla perfezione.

Articoli Correlati