
De oordopjes-bug: toen de andere persoon uit ons transcript verdween
Transcriptie op twee kanalen werkte perfect op laptopspeakers en over-ear-koptelefoons. Toen stopte iemand oordopjes in, en de persoon met wie ze praatten verdween simpelweg uit het transcript — omdat Chromium onze audio stil dempte op het moment dat zij spraken. Elf fixes faalden. De twaalfde veranderde de hele architectuur.
GeekBye transcribeert twee kanten van een gesprek: jouw microfoon op het ene kanaal, de audio van de andere deelnemer (wat je computer afspeelt) op het andere. Het werkte vlekkeloos op laptopspeakers. Het werkte op over-ear-koptelefoons. Toen stopte een gebruiker oordopjes in, en de persoon met wie ze praatten verdween uit het transcript. Niet verhaspeld. Niet verkeerd toegeschreven. Weg.
Dit is het verhaal van GeekBye v1.6.12, de bug die elf mislukte fixes kostte, en het moment waarop we beseften dat we tegen de browser vochten in plaats van ermee samen te werken.
De track die leefde en leeg was
Het symptoom was griezelig. Je startte een meeting, alles zag er goed uit, je zei één woord — en vanaf dat moment ging het audiokanaal van de andere persoon naar pure digitale stilte. We hebben het gemeten: het systeemaudio-signaal zat op een normaal energieniveau (een RMS rond de 0.03–0.06), en op het moment dat de microfoon je stem oppikte, viel het naar 0.0000 en bleef daar voor de rest van de sessie.
Hier is het deel dat ons dagen kostte. Elke browser-API hield vol dat de audiotrack volkomen gezond was. readyState: live. enabled: true. muted: false. Volgens elke statusvlag die het platform blootlegde, was de stream in orde. Het waren de samples die dood waren — een levende pijp die niets dan nullen vervoerde. We hebben lang onze eigen energiemeter niet geloofd, omdat het officiële verhaal zei dat de track werkte.
De trigger was specifiek voor oordopjes, en de reden is fysiek: een oordopje is één apparaat dat tegelijkertijd je microfooningang en je audio-uitgang is. Laptopspeakers en over-ear-koptelefoons scheiden die — output gaat de ene kant op, de microfoon zit ergens anders. Maar wanneer input en output hetzelfde apparaat zijn, heb je per definitie een feedbackpad. Onze sterke gevolgtrekking (dit leeft in de ingewanden van de browser, dus we kunnen geen precieze bron aanhalen) is dat Chromium die microfoon-plus-speaker-op-één-apparaat-vorm detecteert, besluit dat het een feedbacklus is die op het punt staat te gaan piepen, en beschermend de opname dempt — stil, zonder het de app te vertellen via een van de vlaggen die een app kan lezen.
Elf fixes, meerdere ervan erger
We vielen het eerst op de voor de hand liggende manieren aan, en schreven elke poging op. Echo-onderdrukking, ruisonderdrukking en auto gain op beide streams uitschakelen. De twee streams sequentieel starten met een vertraging in plaats van samen. Elke stream zijn eigen sample rate geven. Een stille gain node toevoegen. Alles door één gedeelde audiocontext routeren. Overstappen op een AudioWorklet. Helemaal terug naar de rauwe WebCodecs track processor.
Elf benaderingen. Ze faalden niet alleen — meerdere maakten het erger. Eén introduceerde een vertraging van 30 seconden. Een andere zorgde ervoor dat het transcript van je microfoon werd gelabeld als de andere persoon. We zaten diep in het onkruid, behandelden elk symptoom, en de bug bleef verschuiven.
De doorbraak was een herkadering: elke fix tot dan toe probeerde twee aparte streams gezond te houden. Maar twee aparte streams waren precies het ding dat Chromiums feedbackheuristiek kon zien en dempen. Wat als er niets was om te vergelijken?
De fix: geef de browser één stream, niet twee
v1.6.12 stopte met vechten en veranderde de architectuur. In plaats van de microfoon en systeemaudio als twee onafhankelijke streams te sturen, mixt het ze tot één mono-stream op de client voordat er iets de app verlaat. Wanneer beide bronnen door één pijplijn stromen, kunnen de tracks niet onafhankelijk sterven — er is niet langer een twee-apparaten-feedbackvorm die de browser kan detecteren en dempen. De triggervoorwaarde houdt gewoon op te bestaan.
Er was een heerlijke bonus. Twee streams betekenden twee client-naar-backend-WebSockets en twee backend-naar-provider-verbindingen; één gemixte stream bracht dat terug tot één en één. Het oplossen van de oordopjes-bug halveerde onze transcriptiekosten als bijeffect.
Maar mono mixen heeft een voor de hand liggend probleem: als je beide personen tot één kanaal mengt, hoe weet je dan nog steeds wie wat zei? Je hebt juist de scheiding weggegooid die je dat vertelde.
"Wie sprak" heropbouwen uit energie
Het antwoord was om de luidheid van elke bron te meten voordat ze werden gemixt, en aan elk stuk audio een hint te hangen die zei welke bron domineerde — microfoon, systeem of stilte. Luidere bron wint. Simpel genoeg op speakers.
Oordopjes hadden nog een laag nodig, en dit is waar de echo terugkomt in het verhaal — de tegenovergestelde kant op van wat je zou raden. Op oordopjes lekt de stem van de andere persoon uit het dopje en terug in de microfoon. Dus zonder zorg wordt hun echo, die op jouw microfoonkanaal aankomt, toegeschreven aan jou. De fix is een set echo-bewuste drempels: wanneer de systeemaudio boven een kleine ondergrens speelt, springt de lat die je microfoon moet halen om te tellen als "jij die spreekt" flink omhoog — van een normale 0.015 naar 0.1. Met andere woorden: terwijl de andere persoon praat, moet je microfoon duidelijk, ondubbelzinnig luider zijn dan hun echo voordat we de woorden aan jou toeschrijven. Dubbelzinnige frames vallen standaard terug op "systeem", in de veronderstelling dat het echo is. Het is een bewuste bias die de attributie goed krijgt in precies de situatie die het vroeger fout deed.
Die proof-of-concept — mono mixen plus energie-gebaseerde, echo-bewuste stemdetectie — is de voorouder van het attributiesysteem dat GeekBye vandaag draait. De steigers werden in dezelfde release afgebroken zodra het zich bewezen had, en de energie-wiskunde verhardde later tot een goed geteste module. Maar v1.6.12 is waar het idee werd geboren, onder druk, door een gebruiker met een paar oordopjes.
Drie dingen die de oordopjes-bug leerde
- Een gezonde handle kan dode data dragen. Het hele onderzoek draaide om het vertrouwen op onze energiemeter boven de statusvlaggen van het platform —
live,enabled,unmuted, allemaal liegend terwijl de samples nullen waren. Monitor de payload, niet de zelfrapportage van de API. Als een stream beweert in orde te zijn, meet dan of dat werkelijk zo is. - Wanneer het platform tegen je architectuur vecht, verander de architectuur — niet het platform. We konden Chromiums feedbackbescherming niet overtroeven, en elke poging om het langs de randen uit te schakelen faalde of sloeg averechts uit. Het verwijderen van de waarneembare voorwaarde — twee streams die er één worden — liet het hele probleem verdwijnen. Het is bijna altijd goedkoper om de trigger te verwijderen dan een gevecht met de browser te winnen.
- Wanneer je kanalen samenvouwt voor robuustheid, moet je heropbouwen wat je vernietigde. Mixen tot mono doodde de kanaalscheiding die "wie sprak" droeg. Die informatie moest opnieuw worden afgeleid uit pre-mix-energie en echo-bewuste drempels. De compenserende logica is geen bijzaak van de fix — het is de helft van de fix.
Dit is het vierde hoofdstuk van het betrouwbaarheidsverhaal dat GeekBye v2 wordt. Voor het vorige hoofdstuk, zie een verbroken verbinding hoort niet je hele app te laten crashen (v1.6.8); voor de pijplijnherschrijving waarop dit audiowerk voortbouwde, we schrapten 5.000 regels audiocode (v1.6.0); en voor de hele boog, de anatomie van software tot in de perfectie uitbrengen.