Steven
Steven6 Min. Lesezeit

Der Earbuds-Bug: Als die andere Person aus unserem Transkript verschwand

Die zweikanalige Transkription funktionierte einwandfrei über Laptop-Lautsprecher und Over-Ear-Kopfhörer. Dann steckte jemand In-Ear-Kopfhörer ein, und die Person, mit der er sprach, verschwand einfach aus dem Transkript — weil Chromium unser Audio in dem Moment, in dem sie sprach, stillschweigend stummschaltete. Elf Fixes scheiterten. Der zwölfte änderte die ganze Architektur.

Engineering
Audio
Transkription
GeekBye-Releases
Der Earbuds-Bug: Als die andere Person aus unserem Transkript verschwand

GeekBye transkribiert beide Seiten eines Gesprächs: dein Mikrofon auf einem Kanal, das Audio der anderen Person (das, was dein Computer abspielt) auf einem anderen. Über Laptop-Lautsprecher lief es fehlerfrei. Über Over-Ear-Kopfhörer lief es. Dann steckte ein Nutzer In-Ear-Kopfhörer ein, und die Person, mit der er sprach, verschwand aus dem Transkript. Nicht verstümmelt. Nicht falsch zugeordnet. Weg.

Das ist die Geschichte von GeekBye v1.6.12, dem Bug, der elf gescheiterte Fixes brauchte, und dem Moment, in dem wir erkannten, dass wir gegen den Browser kämpften, statt mit ihm zu arbeiten.

Der Track, der lebendig und leer war

Das Symptom war unheimlich. Du startetest ein Meeting, alles sah gut aus, du sagtest ein einziges Wort — und von diesem Augenblick an fiel der Audiokanal der anderen Person auf reine digitale Stille. Wir haben es gemessen: Das Systemaudio-Signal lag auf einem normalen Energieniveau (ein RMS um 0.03–0.06), und in dem Moment, in dem das Mikrofon deine Stimme aufnahm, stürzte es auf 0.0000 und blieb dort für den Rest der Sitzung.

Hier ist der Teil, der uns Tage kostete. Alle Browser-APIs bestanden darauf, dass der Audiotrack vollkommen gesund sei. readyState: live. enabled: true. muted: false. Nach jedem Status-Flag, das die Plattform preisgab, war der Stream in Ordnung. Es waren die Samples, die tot waren — ein lebendiges Rohr, das nichts als Nullen transportierte. Wir haben lange gebraucht, um unserem eigenen Energiemesser nicht zu glauben, weil die offizielle Version besagte, dass der Track funktionierte.

Der Auslöser war spezifisch für In-Ear-Kopfhörer, und der Grund ist physikalisch: Ein In-Ear-Kopfhörer ist ein einziges Gerät, das gleichzeitig dein Mikrofoneingang und dein Audioausgang ist. Laptop-Lautsprecher und Over-Ear-Kopfhörer trennen diese beiden — der Ausgang geht an einen Ort, das Mikrofon ist woanders. Aber wenn Eingang und Ausgang dasselbe Gerät sind, hast du per Definition einen Rückkopplungspfad. Unsere starke Schlussfolgerung (das steckt tief im Inneren des Browsers, also können wir nicht Kapitel und Vers zitieren) ist, dass Chromium diese Form von Mikrofon-plus-Lautsprecher-auf-einem-Gerät erkennt, entscheidet, dass es eine Rückkopplungsschleife ist, die gleich zu pfeifen droht, und die Aufnahme schützend stummschaltet — stillschweigend, ohne es der App über irgendeines der Flags mitzuteilen, die eine App lesen kann.

Elf Fixes, mehrere davon schlimmer

Wir gingen es zuerst auf die naheliegenden Weisen an und schrieben jeden Versuch auf. Echounterdrückung, Rauschunterdrückung und automatische Verstärkung auf beiden Streams deaktivieren. Die beiden Streams sequenziell mit Verzögerung starten statt zusammen. Jedem Stream seine eigene Abtastrate geben. Einen stummen Gain-Node hinzufügen. Alles durch einen einzigen gemeinsamen Audiokontext leiten. Auf einen AudioWorklet umsteigen. Ganz hinunter zum rohen Track-Prozessor von WebCodecs gehen.

Elf Ansätze. Sie scheiterten nicht nur — mehrere machten es schlimmer. Einer führte eine Verzögerung von 30 Sekunden ein. Ein anderer sorgte dafür, dass das Transkript deines Mikrofons als das der anderen Person beschriftet auftauchte. Wir steckten tief im Dickicht, behandelten jedes Symptom, und der Bug bewegte sich immer weiter.

Der Durchbruch war eine Umformulierung: Jeder Fix bis dahin versuchte, zwei getrennte Streams gesund zu halten. Aber zwei getrennte Streams waren genau das, was Chromiums Rückkopplungs-Heuristik sehen und stummschalten konnte. Was, wenn es nichts zu vergleichen gäbe?

Der Fix: Gib dem Browser einen Stream, nicht zwei

v1.6.12 hörte auf zu kämpfen und änderte die Architektur. Statt Mikrofon und Systemaudio als zwei unabhängige Streams zu senden, mischt sie sie zu einem einzigen Mono-Stream auf dem Client, bevor irgendetwas die App verlässt. Wenn beide Quellen durch eine einzige Pipeline fließen, können die Tracks nicht unabhängig voneinander sterben — es gibt keine Zwei-Geräte-Rückkopplungsform mehr, die der Browser erkennen und stummschalten kann. Die Auslösebedingung hört einfach auf zu existieren.

Es gab einen entzückenden Bonus. Zwei Streams bedeuteten zwei WebSockets von Client zu Backend und zwei Verbindungen von Backend zu Anbieter; ein einziger gemischter Stream reduzierte das auf eins und eins. Den Earbuds-Bug zu beheben halbierte unsere Transkriptionskosten als Nebeneffekt.

Aber Mono-Mischen hat ein offensichtliches Problem: Wenn du beide Personen in einen Kanal verschmilzt, wie weißt du dann noch, wer was gesagt hat? Du hast genau die Trennung weggeworfen, die es dir verriet.

Das "Wer hat gesprochen" aus der Energie rekonstruieren

Die Antwort war, die Lautstärke jeder Quelle vor dem Mischen zu messen und an jedes Audiofragment einen Hinweis anzuhängen, welche Quelle dominierte — Mikrofon, System oder Stille. Die lautere Quelle gewinnt. Einfach genug über Lautsprecher.

In-Ear-Kopfhörer brauchten eine Schicht mehr, und hier kommt das Echo zurück in die Geschichte — in die entgegengesetzte Richtung zeigend, als man vermuten würde. Bei In-Ear-Kopfhörern dringt die Stimme der anderen Person aus dem Ohrstöpsel heraus und wieder ins Mikrofon. Ohne Sorgfalt wird also ihr Echo, das auf deinem Mikrofonkanal ankommt, dir zugeordnet. Der Fix ist eine Reihe echo-bewusster Schwellenwerte: Wenn das Systemaudio über einer kleinen Untergrenze spielt, springt die Hürde, die dein Mikrofon überwinden muss, um als "du sprichst" zu zählen, weit nach oben — von einem normalen 0.015 auf 0.1. Mit anderen Worten: Während die andere Person spricht, muss dein Mikrofon klar und unmissverständlich lauter sein als ihr Echo, bevor wir dir die Worte zuschreiben. Mehrdeutige Frames gehen standardmäßig auf "System", in der Annahme, dass sie Echo sind. Es ist eine bewusste Verzerrung, die die Zuordnung genau in der Situation richtig macht, die sie früher falsch machte.

Dieser Proof of Concept — Mono-Mischen plus energiebasierte, echo-bewusste Sprecherkennung — ist der Vorfahre des Zuordnungssystems, das GeekBye heute betreibt. Das Gerüst wurde im selben Release herausgerissen, sobald es sich bewiesen hatte, und die Energiemathematik verhärtete sich später zu einem ordentlich getesteten Modul. Aber v1.6.12 ist der Ort, an dem die Idee geboren wurde, unter Druck, von einem Nutzer mit einem Paar In-Ear-Kopfhörern.

Drei Dinge, die der Earbuds-Bug lehrte

  1. Ein gesunder Handle kann tote Daten transportieren. Die gesamte Untersuchung drehte sich darum, unserem Energiemesser mehr zu vertrauen als den Status-Flags der Plattform — live, enabled, unmuted, alle logen, während die Samples Nullen waren. Überwache die Nutzlast, nicht den Selbstbericht der API. Wenn ein Stream behauptet, in Ordnung zu sein, miss, ob er es tatsächlich ist.
  2. Wenn die Plattform gegen deine Architektur kämpft, ändere die Architektur — nicht die Plattform. Wir konnten Chromiums Rückkopplungsschutz nicht wegdiskutieren, und jeder Versuch, ihn an den Rändern zu deaktivieren, scheiterte oder ging nach hinten los. Die beobachtbare Bedingung zu entfernen — zwei Streams werden einer — ließ das ganze Problem verschwinden. Es ist fast immer billiger, den Auslöser zu entfernen, als einen Kampf mit dem Browser zu gewinnen.
  3. Wenn du Kanäle für die Robustheit verschmilzt, musst du wiederaufbauen, was du zerstört hast. Das Mischen zu Mono tötete die Kanaltrennung, die das "Wer hat gesprochen" transportierte. Diese Information musste aus der Energie vor dem Mischen und aus echo-bewussten Schwellenwerten neu abgeleitet werden. Die kompensierende Logik ist kein nachträglicher Einfall zum Fix — sie ist die Hälfte des Fixes.

Das ist das vierte Kapitel der Zuverlässigkeitsgeschichte, die zu GeekBye v2 wird. Für das vorherige Kapitel siehe eine abgebrochene Verbindung sollte nicht deine ganze App zum Absturz bringen (v1.6.8); für die Pipeline-Neuschreibung, auf der diese Audioarbeit aufbaute, wir haben 5.000 Zeilen Audiocode gelöscht (v1.6.0); und für den ganzen Bogen die Anatomie, Software bis zur Perfektion auszuliefern.

Ähnliche Artikel

Wir löschten 5,000 Zeilen Audio-Code — und Transkripte tauchten plötzlich doppelt auf
Steven
Steven6 Min. Lesezeit

Wir löschten 5,000 Zeilen Audio-Code — und Transkripte tauchten plötzlich doppelt auf

GeekBye v1.6 riss zwei on-device-Swift-Transkriber heraus und ersetzte sie durch eine einheitliche Pipeline — ein Netto-Löschen von über 5,000 Zeilen, während Nutzer gerade mitten in Meetings waren. Dann tauchten Transkripte doppelt auf, weil der Fehler in die eine Schicht wanderte, die noch glaubte, es gäbe zwei Engines.

Engineering
Transkription
Architektur
Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es
Steven
Steven6 Min. Lesezeit

Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es

Als unser Backend mitten in einem Meeting offline ging, hat es nicht nur die Transkription pausiert — es hat die ganze App zum Absturz gebracht. Die Ursache war ein einziges nicht behandeltes Event, und der Fix waren zehn Zeilen. Das ist das Release-Cluster, das GeekBye angemeldet bleiben und verbunden bleiben ließ, durch genau die Dinge, die es früher umgebracht haben.

Engineering
Zuverlässigkeit
Electron
Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log
Steven
Steven7 Min. Lesezeit

Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log

GeekBye v2.0.7 machte das gesamte Overlay verstellbar durchscheinend — was nach einer einzeiligen CSS-Änderung klingt und definitiv keine war. Die eigentliche Geschichte ist, was das Code-Review aufdeckte: zwei Flächen, die sich weigerten zu verblassen, eine Aufnahmeleiste, die bei gleicher Deckkraft falsch aussah, und ein kompiliertes Binary, das um 672 Bytes hin und her sprang, weil unsere eigene Dokumentation einem Reviewer die falsche Swift-Version nannte.

Engineering
Design
Build