Steven
Steven6 min read

The Earbuds Bug: When the Other Person Vanished From Our Transcript

Dual-channel transcription worked perfectly on laptop speakers and over-ear headphones. Then someone plugged in earbuds, and the person they were talking to simply disappeared from the transcript — because Chromium silently muted our audio the instant they spoke. Eleven fixes failed. The twelfth changed the whole architecture.

Engineering
Audio
Transcription
GeekBye Releases
The Earbuds Bug: When the Other Person Vanished From Our Transcript

GeekBye transcribes two sides of a conversation: your microphone on one channel, the other participant's audio (what your computer is playing) on another. It worked flawlessly on laptop speakers. It worked on over-ear headphones. Then a user plugged in earbuds, and the person they were talking to vanished from the transcript. Not garbled. Not misattributed. Gone.

This is the story of GeekBye v1.6.12, the bug that took eleven failed fixes, and the moment we realized we were fighting the browser instead of working with it.

The track that was alive and empty

The symptom was eerie. You'd start a meeting, everything looked fine, you'd say one word — and from that instant, the other person's audio channel went to pure digital silence. We measured it: the system-audio signal sat at a normal energy level (an RMS around 0.03–0.06), and the moment the mic picked up your voice, it dropped to 0.0000 and stayed there for the rest of the session.

Here's the part that cost us days. Every browser API insisted the audio track was perfectly healthy. readyState: live. enabled: true. muted: false. By every status flag the platform exposed, the stream was fine. It was the samples that were dead — a live pipe carrying nothing but zeros. We spent a long time not believing our own energy meter, because the official story said the track was working.

The trigger was specific to earbuds, and the reason is physical: an earbud is one device that is simultaneously your microphone input and your audio output. Laptop speakers and over-ear headphones separate those — output goes one place, the mic is elsewhere. But when input and output are the same device, you have, by definition, a feedback path. Our strong inference (this lives in the browser's guts, so we can't cite chapter and verse) is that Chromium detects that mic-plus-speaker-on-one-device shape, decides it's a feedback loop about to squeal, and protectively mutes the capture — silently, without telling the app through any of the flags an app can read.

Eleven fixes, several of them worse

We attacked it the obvious ways first, and wrote every attempt down. Disable echo cancellation, noise suppression, and auto gain on both streams. Start the two streams sequentially with a delay instead of together. Give each stream its own sample rate. Add a silent gain node. Route everything through a single shared audio context. Move to an AudioWorklet. Drop all the way down to the raw WebCodecs track processor.

Eleven approaches. Not only did they fail — several made it worse. One introduced a 30-second lag. Another caused your microphone's transcript to show up labeled as the other person. We were deep in the weeds, treating each symptom, and the bug kept moving.

The breakthrough was a reframe: every fix so far tried to keep two separate streams healthy. But two separate streams were exactly the thing Chromium's feedback heuristic could see and mute. What if there was nothing to compare?

The fix: give the browser one stream, not two

v1.6.12 stopped fighting and changed the architecture. Instead of sending the mic and system audio as two independent streams, it mixes them into a single mono stream on the client before anything leaves the app. When both sources flow through one pipeline, the tracks can't die independently — there's no longer a two-device feedback shape for the browser to detect and mute. The trigger condition simply stops existing.

There was a delightful bonus. Two streams meant two client-to-backend WebSockets and two backend-to-provider connections; one mixed stream collapsed that to one and one. Fixing the earbuds bug halved our transcription cost as a side effect.

But mono mixing has an obvious problem: if you blend both people into one channel, how do you still know who said what? You've thrown away the very separation that told you.

Rebuilding "who spoke" from energy

The answer was to measure each source's loudness before mixing them, and attach a hint to every chunk of audio saying which source dominated — mic, system, or silence. Louder source wins. Simple enough on speakers.

Earbuds needed one more layer, and this is where the echo comes back into the story — pointed the opposite way from what you'd guess. On earbuds, the other person's voice leaks out of the bud and back into the mic. So without care, their echo, arriving on your mic channel, gets attributed to you. The fix is a set of echo-aware thresholds: when the system audio is playing above a small floor, the bar your microphone has to clear to count as "you speaking" jumps way up — from a normal 0.015 to 0.1. In other words: while the other person is talking, your mic has to be clearly, unambiguously louder than their echo before we'll credit the words to you. Ambiguous frames default to "system," on the assumption they're echo. It's a deliberate bias that gets attribution right in the exact situation that used to get it wrong.

That proof-of-concept — mono mixing plus energy-based, echo-aware voice detection — is the ancestor of the attribution system GeekBye runs today. The scaffolding was torn out in the same release once it proved itself, and the energy math later hardened into a properly tested module. But v1.6.12 is where the idea was born, under duress, by a user with a pair of earbuds.

Three things the earbuds bug taught

  1. A healthy handle can carry dead data. The entire investigation turned on trusting our energy meter over the platform's status flags — live, enabled, unmuted, all lying while the samples were zeros. Monitor the payload, not the API's self-report. If a stream claims to be fine, measure whether it actually is.
  2. When the platform fights your architecture, change the architecture — not the platform. We couldn't out-argue Chromium's feedback protection, and every attempt to disable it around the edges failed or backfired. Removing the observable condition — two streams becoming one — made the whole problem disappear. It's almost always cheaper to remove the trigger than to win a fight with the browser.
  3. When you collapse channels for robustness, you must rebuild what you destroyed. Mixing to mono killed the channel separation that carried "who spoke." That information had to be re-derived from pre-mix energy and echo-aware thresholds. The compensating logic isn't an afterthought to the fix — it is half of the fix.

This is the fourth chapter of the reliability story that becomes GeekBye v2. For the previous chapter, see a dropped connection shouldn't crash your whole app (v1.6.8); for the next — streaming the meeting report without the jitter — how to stream a live report without the flicker (v1.6.13); for the pipeline rewrite this audio work built on, we deleted 5,000 lines of audio code (v1.6.0); and for the whole arc, the anatomy of shipping software to perfection.