Steven
Steven5 분 소요

이어폰 버그: 상대방이 우리 전사에서 사라졌을 때

이중 채널 전사는 노트북 스피커와 오버이어 헤드폰에서 완벽하게 작동했습니다. 그러다 누군가 이어폰을 꽂았고, 그가 대화하던 상대방이 전사에서 그냥 사라졌습니다 — Chromium이 그가 말하는 순간 우리 오디오를 소리 없이 음소거했기 때문입니다. 열한 번의 수정이 실패했습니다. 열두 번째가 아키텍처 전체를 바꿨습니다.

엔지니어링
오디오
전사
GeekBye 릴리스
이어폰 버그: 상대방이 우리 전사에서 사라졌을 때

GeekBye는 대화의 양쪽을 전사합니다: 한 채널에는 당신의 마이크를, 다른 채널에는 상대 참가자의 오디오(당신의 컴퓨터가 재생하고 있는 것)를. 노트북 스피커에서는 흠 없이 작동했습니다. 오버이어 헤드폰에서도 작동했습니다. 그러다 한 사용자가 이어폰을 꽂았고, 그가 대화하던 사람이 전사에서 사라졌습니다. 뭉개진 게 아니었습니다. 잘못 표시된 것도 아니었습니다. 사라진 것입니다.

이것은 GeekBye v1.6.12의 이야기, 열한 번의 실패한 수정을 거친 버그, 그리고 우리가 브라우저와 협력하는 대신 브라우저와 싸우고 있었음을 깨달은 순간의 이야기입니다.

살아 있으면서 텅 빈 트랙

증상은 으스스했습니다. 회의를 시작하면 모든 것이 멀쩡해 보이다가, 당신이 한 마디를 뱉는 순간 — 그 순간부터 상대방의 오디오 채널이 순수한 디지털 무음으로 떨어졌습니다. 우리는 그것을 측정했습니다: 시스템 오디오 신호는 정상적인 에너지 수준(RMS 약 0.03–0.06)에 있다가, 마이크가 당신의 목소리를 포착하는 순간 0.0000으로 떨어져 세션이 끝날 때까지 거기 머물렀습니다.

여기가 우리에게 며칠을 앗아간 부분입니다. 모든 브라우저 API는 그 오디오 트랙이 완벽하게 건강하다고 우겼습니다. readyState: live. enabled: true. muted: false. 플랫폼이 노출하는 모든 상태 플래그로 보면, 스트림은 멀쩡했습니다. 죽어 있는 것은 샘플 이었습니다 — 0 말고는 아무것도 나르지 않는 살아 있는 파이프. 우리는 오랫동안 우리 자신의 에너지 계측기를 믿지 못했습니다. 공식적인 이야기가 그 트랙은 작동 중이라고 말했기 때문입니다.

방아쇠는 이어폰에 특유했고, 그 이유는 물리적입니다: 이어폰은 당신의 마이크 입력이자 동시에 오디오 출력인 하나의 기기입니다. 노트북 스피커와 오버이어 헤드폰은 그 둘을 분리합니다 — 출력은 한 곳으로 가고, 마이크는 다른 곳에 있습니다. 하지만 입력과 출력이 같은 기기일 때, 당신은 정의상 피드백 경로를 갖게 됩니다. 우리의 강력한 추론은(이건 브라우저의 내장 깊숙이 살고 있어서 우리가 조목조목 인용할 수는 없습니다) Chromium이 마이크와 스피커가 하나의 기기에 겹친 그 형태를 감지하고, 곧 삐 소리를 낼 피드백 루프라고 판단해 보호적으로 캡처를 음소거한다 는 것입니다 — 소리 없이, 앱이 읽을 수 있는 어떤 플래그로도 앱에 알리지 않고.

열한 번의 수정, 그중 몇은 더 나빴다

우리는 먼저 뻔한 방법들로 공략했고, 시도마다 전부 기록했습니다. 두 스트림 모두에서 에코 제거, 잡음 억제, 자동 게인을 끄기. 두 스트림을 함께가 아니라 지연을 두고 순차적으로 시작하기. 각 스트림에 자체 샘플레이트를 주기. 무음 게인 노드를 추가하기. 모든 것을 하나의 공유 오디오 컨텍스트로 라우팅하기. AudioWorklet으로 옮기기. 저 밑바닥의 원시 WebCodecs 트랙 프로세서까지 내려가기.

열한 가지 접근. 그것들은 실패했을 뿐 아니라 — 몇몇은 상황을 더 악화시켰습니다. 하나는 30초의 지연을 만들어 냈습니다. 다른 하나는 당신의 마이크 전사가 상대방 으로 표시되게 만들었습니다. 우리는 잡초 속 깊이 빠져 각 증상만 다루고 있었고, 버그는 계속 자리를 옮겼습니다.

돌파구는 관점의 전환이었습니다: 지금까지의 모든 수정은 두 개의 분리된 스트림을 건강하게 유지하려 애썼습니다. 그런데 두 개의 분리된 스트림이야말로 Chromium의 피드백 휴리스틱이 보고 음소거할 수 있는 바로 그것 이었습니다. 만약 비교할 것이 없다면 어떨까?

수정: 브라우저에게 두 스트림이 아니라 한 스트림을 주기

v1.6.12는 싸움을 멈추고 아키텍처를 바꿨습니다. 마이크와 시스템 오디오를 두 개의 독립된 스트림으로 보내는 대신, 그것들을 앱을 떠나기 전에 클라이언트에서 하나의 mono 스트림으로 섞습니다. 두 소스가 하나의 파이프라인을 통과하면, 트랙은 독립적으로 죽을 수 없습니다 — 브라우저가 감지하고 음소거할 두 기기 피드백 형태가 더는 없습니다. 방아쇠 조건이 그냥 존재하기를 멈추는 것입니다.

기분 좋은 덤이 있었습니다. 두 스트림은 두 개의 클라이언트-백엔드 WebSocket과 두 개의 백엔드-제공자 연결을 의미했습니다; 하나로 섞인 스트림은 그것을 하나와 하나로 줄였습니다. 이어폰 버그를 고치는 것은 부수 효과로 우리 전사 비용을 절반으로 줄였습니다.

하지만 mono 믹싱에는 뻔한 문제가 있습니다: 두 사람을 하나의 채널로 섞으면, 여전히 누가 무엇을 말했는지 어떻게 알까요? 그것을 알려 주던 바로 그 분리를 던져 버린 것인데요.

에너지로부터 "누가 말했는가"를 다시 세우기

답은 섞기 전에 각 소스의 크기를 측정하고, 오디오 청크마다 어느 소스가 우세했는지 — 마이크, 시스템, 또는 무음 — 를 알려 주는 힌트를 붙이는 것이었습니다. 더 큰 소스가 이깁니다. 스피커에서는 충분히 간단합니다.

이어폰은 한 겹이 더 필요했고, 여기서 에코가 이야기 속으로 돌아옵니다 — 당신이 짐작할 방향과는 반대로 향한 채로. 이어폰에서는 상대방 의 목소리가 이어버드 밖으로 새어 나와 다시 마이크로 들어옵니다. 그래서 조심하지 않으면, 당신의 마이크 채널로 도착하는 그들의 에코가 당신에게 귀속됩니다. 수정은 에코 인식 임계값의 집합입니다: 시스템 오디오가 작은 바닥값 위로 재생되고 있을 때, 당신의 마이크가 "당신이 말하는 것"으로 인정받기 위해 넘어야 하는 문턱이 확 올라갑니다 — 평소의 0.015에서 0.1로. 다시 말해: 상대방이 말하고 있는 동안, 당신의 마이크는 그들의 에코보다 분명하고 명백하게 더 커야만 우리가 그 말을 당신의 공으로 돌립니다. 애매한 프레임은 에코라는 가정 아래 기본적으로 "시스템"으로 처리됩니다. 이것은 예전에 틀렸던 바로 그 상황에서 화자 구분을 옳게 만드는 의도적인 편향입니다.

그 개념 증명 — mono 믹싱에 에너지 기반, 에코 인식 음성 감지를 더한 것 — 은 오늘날 GeekBye가 돌리는 화자 구분 시스템의 조상입니다. 비계는 그것이 스스로를 증명하자마자 같은 릴리스에서 뜯겨 나갔고, 에너지 계산은 나중에 제대로 테스트된 모듈로 단단해졌습니다. 하지만 v1.6.12는 그 아이디어가 태어난 곳입니다 — 압박 속에서, 이어폰 한 쌍을 가진 사용자에 의해.

이어폰 버그가 가르쳐 준 세 가지

  1. 건강한 핸들이 죽은 데이터를 나를 수 있다. 조사 전체는 플랫폼의 상태 플래그보다 우리의 에너지 계측기를 믿는 데 달려 있었습니다 — live, enabled, unmuted, 샘플이 0인 동안 전부 거짓말을 하고 있었습니다. API의 자기 보고가 아니라 페이로드를 감시하세요. 스트림이 멀쩡하다고 주장하면, 실제로 그런지 측정하세요.
  2. 플랫폼이 당신의 아키텍처와 싸울 때, 플랫폼이 아니라 아키텍처를 바꿔라. 우리는 Chromium의 피드백 보호를 논리로 이길 수 없었고, 가장자리에서 그것을 끄려는 모든 시도는 실패하거나 역효과를 냈습니다. 관찰 가능한 조건 — 두 스트림이 하나가 되는 것 — 을 제거하자 문제 전체가 사라졌습니다. 브라우저와의 싸움에서 이기는 것보다 방아쇠를 제거하는 편이 거의 언제나 더 싸게 먹힙니다.
  3. 견고함을 위해 채널을 합칠 때는, 파괴한 것을 다시 세워야 한다. mono로 섞는 것은 "누가 말했는가"를 나르던 채널 분리를 죽였습니다. 그 정보는 믹싱 전 에너지와 에코 인식 임계값으로부터 다시 도출되어야 했습니다. 그 보상 로직은 수정에 딸린 뒷생각이 아닙니다 — 그것이 수정의 절반입니다.

이것은 GeekBye v2가 되는 안정성 이야기의 네 번째 장입니다. 이전 장은 연결이 끊겼다고 앱 전체가 죽어서는 안 된다(v1.6.8)를; 이 오디오 작업이 딛고 선 파이프라인 재작성은 음성 코드 5,000줄을 삭제했다(v1.6.0)를; 그리고 전체 궤적에 대해서는 소프트웨어를 완벽하게 출시하는 것의 해부학을 보세요.

관련 글

오디오 코드 5,000줄을 지웠더니 — 회의록이 두 번씩 나타나기 시작했다
Steven
Steven5 분 소요

오디오 코드 5,000줄을 지웠더니 — 회의록이 두 번씩 나타나기 시작했다

GeekBye v1.6은 온디바이스 Swift 전사기 두 개를 뜯어내고 하나의 통합 파이프라인으로 교체했습니다 — 순삭 5,000줄이 넘는 작업을, 사람들이 앱에서 한창 회의 중일 때 해냈습니다. 그러자 회의록이 두 번씩 나타나기 시작했습니다. 버그가 여전히 엔진이 두 개라고 믿고 있던 그 한 층으로 옮겨 갔기 때문입니다.

엔지니어링
전사
아키텍처
방화벽이 WebSocket을 막을 때, 실시간 전사는 어떻게 되는가
Steven
Steven4 분 소요

방화벽이 WebSocket을 막을 때, 실시간 전사는 어떻게 되는가

기업 네트워크는 HTTPS는 통과시키면서 WebSocket 업그레이드는 조용히 죽여버리길 좋아합니다. 그게 실시간 전사를 소리 없이 망가뜨립니다. GeekBye v2.0.8은 순수 HTTPS 트랜스포트로 자동 폴백합니다 — 그리고 그걸 출시하는 과정에서, 기능 전체를 무용지물로 만들 뻔한 버그가 드러났습니다.

전사
네트워킹
엔지니어링
AI 전사는 왜 기술 용어를 잘못 알아듣는가 (그리고 우리는 어떻게 고쳤나)
Steven
Steven5 분 소요

AI 전사는 왜 기술 용어를 잘못 알아듣는가 (그리고 우리는 어떻게 고쳤나)

라이브 세션이 "what is the pointer in C++"를 "what is the point in life"로 받아적었습니다. 그 전사 기록에서 GeekBye v2.0.11까지 이어진 포렌식 추적기 — keyterm 바이어싱, 연결을 끊어버리던 타이밍 레이스, 그리고 우리의 수정이 역효과를 낸 날의 이야기입니다.

전사
안정성
엔지니어링