
Earbuds-buggen: när den andra personen försvann ur vårt transkript
Tvåkanalig transkribering fungerade felfritt på laptopens högtalare och över-örat-hörlurar. Sedan satte någon in earbuds, och personen de pratade med försvann helt enkelt ur transkriptet — eftersom Chromium tyst tystade vårt ljud i samma stund som de började prata. Elva fixar misslyckades. Den tolfte förändrade hela arkitekturen.
GeekBye transkriberar två sidor av ett samtal: din mikrofon på en kanal, den andra deltagarens ljud (det som din dator spelar upp) på en annan. Det fungerade felfritt på laptopens högtalare. Det fungerade på över-örat-hörlurar. Sedan satte en användare in earbuds, och personen de pratade med försvann ur transkriptet. Inte förvrängd. Inte feltillskriven. Borta.
Det här är historien om GeekBye v1.6.12, buggen som tog elva misslyckade fixar, och ögonblicket då vi insåg att vi slogs mot webbläsaren istället för att arbeta med den.
Spåret som var levande och tomt
Symptomet var kusligt. Du startade ett möte, allt såg bra ut, du sa ett ord — och från den stunden gick den andra personens ljudkanal till ren digital tystnad. Vi mätte det: systemljudssignalen låg på en normal energinivå (ett RMS runt 0.03–0.06), och i det ögonblick mikrofonen fångade din röst föll det till 0.0000 och stannade där resten av sessionen.
Här är delen som kostade oss dagar. Varje webbläsar-API insisterade på att ljudspåret var fullkomligt friskt. readyState: live. enabled: true. muted: false. Enligt varje statusflagga plattformen exponerade var strömmen fin. Det var samplen som var döda — ett levande rör som bar ingenting annat än nollor. Vi tillbringade lång tid med att inte tro på vår egen energimätare, eftersom den officiella historien sa att spåret fungerade.
Utlösaren var specifik för earbuds, och orsaken är fysisk: en earbud är en enda enhet som samtidigt är din mikrofoningång och din ljudutgång. Laptophögtalare och över-örat-hörlurar separerar dessa — utgången går ett ställe, mikrofonen är någon annanstans. Men när ingång och utgång är samma enhet har du per definition en återkopplingsväg. Vår starka slutledning (detta bor i webbläsarens innanmäte, så vi kan inte citera kapitel och vers) är att Chromium upptäcker denna mikrofon-plus-högtalare-på-en-enhet-form, avgör att det är en återkopplingsslinga på väg att tjuta, och skyddande tystar fångsten — tyst, utan att berätta för appen genom någon av de flaggor en app kan läsa.
Elva fixar, flera av dem värre
Vi angrep det på de uppenbara sätten först, och skrev ner varje försök. Stänga av ekosläckning, brusreducering och autoförstärkning på båda strömmarna. Starta de två strömmarna sekventiellt med en fördröjning istället för tillsammans. Ge varje ström sin egen samplingsfrekvens. Lägga till en tyst förstärkningsnod. Dirigera allt genom en enda delad ljudkontext. Gå över till en AudioWorklet. Falla hela vägen ner till den råa WebCodecs-spårprocessorn.
Elva tillvägagångssätt. De misslyckades inte bara — flera gjorde det värre. Ett introducerade en 30-sekunders eftersläpning. Ett annat fick din mikrofons transkript att dyka upp märkt som den andra personen. Vi var djupt inne i snårskogen, behandlade varje symptom, och buggen fortsatte att röra på sig.
Genombrottet var en omformulering: varje fix hittills försökte hålla två separata strömmar friska. Men två separata strömmar var precis det som Chromiums återkopplingsheuristik kunde se och tysta. Tänk om det inte fanns något att jämföra?
Fixen: ge webbläsaren en ström, inte två
v1.6.12 slutade slåss och ändrade arkitekturen. Istället för att skicka mikrofonen och systemljudet som två oberoende strömmar mixar den dem till en enda mono-ström på klienten innan något lämnar appen. När båda källorna flödar genom en pipeline kan spåren inte dö oberoende av varandra — det finns inte längre någon tvåenhetsåterkopplingsform för webbläsaren att upptäcka och tysta. Utlösningsvillkoret slutar helt enkelt att existera.
Det fanns en förtjusande bonus. Två strömmar innebar två klient-till-backend-WebSockets och två backend-till-leverantör-anslutningar; en mixad ström kollapsade det till en och en. Att fixa earbuds-buggen halverade vår transkriberingskostnad som en sidoeffekt.
Men mono-mixning har ett uppenbart problem: om du blandar båda personerna till en kanal, hur vet du fortfarande vem som sa vad? Du har ju kastat bort själva separationen som berättade det för dig.
Återskapa "vem som pratade" från energi
Svaret var att mäta varje källas ljudstyrka innan de mixades, och fästa en ledtråd på varje ljudbit om vilken källa som dominerade — mikrofon, system eller tystnad. Högre källa vinner. Enkelt nog på högtalare.
Earbuds behövde ett lager till, och det är här ekot kommer tillbaka i historien — riktat åt motsatt håll mot vad man skulle gissa. På earbuds läcker den andra personens röst ut ur budden och tillbaka in i mikrofonen. Så utan omsorg tillskrivs deras eko, som anländer på din mikrofonkanal, dig. Fixen är en uppsättning ekomedvetna trösklar: när systemljudet spelas över ett litet golv hoppar ribban din mikrofon måste klara för att räknas som "du pratar" rejält uppåt — från ett normalt 0.015 till 0.1. Med andra ord: medan den andra personen pratar måste din mikrofon vara tydligt, otvetydigt högre än deras eko innan vi tillskriver orden till dig. Tvetydiga ramar hamnar som standard på "system", utifrån antagandet att de är eko. Det är en avsiktlig slagsida som får tillskrivningen rätt i exakt den situation som förr fick den fel.
Det konceptbeviset — mono-mixning plus energibaserad, ekomedveten röstdetektering — är förfadern till tillskrivningssystemet som GeekBye kör idag. Ställningen revs ut i samma release så snart den bevisat sig, och energimatematiken härdades senare till en ordentligt testad modul. Men v1.6.12 är där idén föddes — under press, av en användare med ett par earbuds.
Tre saker earbuds-buggen lärde oss
- Ett friskt handtag kan bära död data. Hela utredningen vände på att lita på vår energimätare framför plattformens statusflaggor —
live,enabled,unmuted, alla ljög medan samplen var nollor. Bevaka innehållet, inte API:ets självrapport. Om en ström påstår att den är fin, mät om den faktiskt är det. - När plattformen slåss mot din arkitektur, ändra arkitekturen — inte plattformen. Vi kunde inte överbevisa Chromiums återkopplingsskydd, och varje försök att stänga av det i kanterna misslyckades eller slog tillbaka. Att ta bort det observerbara villkoret — två strömmar blir en — fick hela problemet att försvinna. Det är nästan alltid billigare att ta bort utlösaren än att vinna en strid med webbläsaren.
- När du slår ihop kanaler för robusthet måste du bygga om det du förstörde. Att mixa till mono dödade kanalseparationen som bar "vem som pratade". Den informationen måste härledas på nytt från energi före mixning och ekomedvetna trösklar. Den kompenserande logiken är ingen eftertanke till fixen — den är halva fixen.
Det här är det fjärde kapitlet i tillförlitlighetshistorien som blir GeekBye v2. För det föregående kapitlet, se en tappad anslutning borde inte krascha hela din app (v1.6.8); för pipeline-omskrivningen som detta ljudarbete byggde på, vi raderade 5 000 rader ljudkod (v1.6.0); och för hela bågen, anatomin av att skeppa mjukvara till perfektion.