
Bug tai nghe nhét tai: Khi người kia biến mất khỏi bản chép lời của chúng tôi
Chép lời hai kênh chạy hoàn hảo trên loa laptop và tai nghe chụp tai. Rồi ai đó cắm tai nghe nhét tai vào, và người họ đang trò chuyện cùng bỗng biến mất khỏi bản chép lời — vì Chromium lặng lẽ tắt tiếng âm thanh của chúng tôi ngay khoảnh khắc họ cất lời. Mười một bản vá thất bại. Bản thứ mười hai thay đổi cả kiến trúc.
GeekBye chép lời hai phía của một cuộc trò chuyện: micro của bạn trên một kênh, âm thanh của người tham gia kia (thứ máy tính của bạn đang phát) trên một kênh khác. Nó chạy hoàn hảo trên loa laptop. Nó chạy trên tai nghe chụp tai. Rồi một người dùng cắm tai nghe nhét tai vào, và người họ đang trò chuyện cùng biến mất khỏi bản chép lời. Không phải méo tiếng. Không phải gán nhầm. Biến mất.
Đây là câu chuyện về GeekBye v1.6.12, cái bug ngốn mất mười một bản vá thất bại, và khoảnh khắc chúng tôi nhận ra mình đang chống lại trình duyệt thay vì làm việc cùng nó.
Cái track vừa sống vừa rỗng
Triệu chứng nghe rợn người. Bạn bắt đầu một cuộc họp, mọi thứ trông ổn, bạn nói một từ — và từ khoảnh khắc đó, kênh âm thanh của người kia rơi vào im lặng kỹ thuật số thuần khiết. Chúng tôi đo được: tín hiệu âm thanh hệ thống nằm ở mức năng lượng bình thường (một chỉ số RMS quanh 0.03–0.06), và ngay khoảnh khắc mic bắt được giọng bạn, nó tụt xuống 0.0000 và ở lì đó suốt phần còn lại của phiên.
Đây là phần đã ngốn của chúng tôi nhiều ngày. Mọi API của trình duyệt đều một mực khẳng định track âm thanh hoàn toàn khỏe mạnh. readyState: live. enabled: true. muted: false. Theo mọi cờ trạng thái mà nền tảng phơi ra, luồng vẫn ổn. Chính các mẫu âm thanh mới là thứ đã chết — một đường ống sống nhưng chỉ chở toàn số không. Chúng tôi mất một quãng dài không dám tin chính máy đo năng lượng của mình, vì câu chuyện chính thống nói rằng track vẫn đang chạy.
Yếu tố kích hoạt chỉ xảy ra riêng với tai nghe nhét tai, và lý do thuộc về vật lý: một chiếc tai nghe nhét tai là một thiết bị vừa đồng thời là đầu vào micro vừa là đầu ra âm thanh của bạn. Loa laptop và tai nghe chụp tai tách hai thứ đó ra — đầu ra đi một nơi, còn mic ở nơi khác. Nhưng khi đầu vào và đầu ra là cùng một thiết bị, theo định nghĩa bạn có một đường phản hồi. Suy luận vững chắc của chúng tôi (chuyện này nằm sâu trong ruột trình duyệt nên không thể trích dẫn từng chương từng câu) là Chromium phát hiện cái hình dạng mic-cộng-loa-trên-một-thiết-bị đó, kết luận rằng đây là một vòng lặp phản hồi sắp rít lên, và tắt tiếng việc thu để tự bảo vệ — một cách lặng lẽ, không hề báo cho ứng dụng qua bất kỳ cờ nào mà ứng dụng có thể đọc.
Mười một bản vá, vài cái còn tệ hơn
Chúng tôi tấn công nó theo những cách hiển nhiên trước, và ghi lại từng lần thử. Tắt khử tiếng vọng, khử ồn và tự động điều chỉnh độ khuếch đại trên cả hai luồng. Khởi động hai luồng tuần tự có độ trễ thay vì cùng lúc. Cho mỗi luồng một sample rate riêng. Thêm một nút gain im lặng. Định tuyến tất cả qua một audio context dùng chung duy nhất. Chuyển sang AudioWorklet. Xuống tận trình xử lý track WebCodecs thô.
Mười một hướng tiếp cận. Không những thất bại — vài cái còn làm tệ hơn. Một cái gây độ trễ 30 giây. Một cái khác khiến bản chép lời micro của bạn hiện ra với nhãn của người kia. Chúng tôi ngập sâu trong đám cỏ rối, chữa từng triệu chứng, mà cái bug cứ dịch chuyển hoài.
Bước đột phá là một cách nhìn lại: cho tới giờ mọi bản vá đều cố giữ cho hai luồng riêng biệt khỏe mạnh. Nhưng hai luồng riêng biệt lại chính xác là thứ mà heuristic phản hồi của Chromium có thể thấy và tắt tiếng. Nếu chẳng có gì để so sánh thì sao?
Bản vá: đưa cho trình duyệt một luồng, không phải hai
v1.6.12 thôi chống cự và thay đổi kiến trúc. Thay vì gửi mic và âm thanh hệ thống thành hai luồng độc lập, nó trộn chúng thành một luồng mono duy nhất ở phía client trước khi bất cứ thứ gì rời khỏi ứng dụng. Khi cả hai nguồn chảy qua một đường ống, các track không thể chết độc lập — không còn cái hình dạng phản hồi hai thiết bị nào để trình duyệt phát hiện và tắt tiếng nữa. Điều kiện kích hoạt đơn giản là thôi tồn tại.
Có một phần thưởng thú vị. Hai luồng nghĩa là hai WebSocket từ client tới backend và hai kết nối từ backend tới nhà cung cấp; một luồng trộn thu gọn tất cả về một và một. Việc sửa cái bug tai nghe nhét tai cắt đôi chi phí chép lời của chúng tôi như một hệ quả phụ.
Nhưng trộn mono có một vấn đề hiển nhiên: nếu bạn hòa cả hai người vào một kênh, làm sao còn biết ai đã nói gì? Bạn đã vứt bỏ chính cái sự tách biệt vốn cho bạn biết điều đó.
Dựng lại "ai vừa nói" từ năng lượng
Lời giải là đo độ lớn của từng nguồn trước khi trộn chúng, và đính vào mỗi khối âm thanh một gợi ý cho biết nguồn nào chiếm ưu thế — mic, hệ thống, hay im lặng. Nguồn lớn hơn thắng. Đủ đơn giản trên loa.
Tai nghe nhét tai cần thêm một lớp nữa, và đây là chỗ tiếng vọng quay lại trong câu chuyện — chĩa theo hướng ngược với điều bạn đoán. Trên tai nghe nhét tai, giọng của người kia rò ra khỏi cái tai nghe và vọng ngược vào mic. Nên nếu không cẩn thận, tiếng vọng của họ, đến trên kênh mic của bạn, bị gán cho bạn. Bản vá là một tập các ngưỡng có ý thức về tiếng vọng: khi âm thanh hệ thống đang phát trên một mức sàn nhỏ, cái vạch mà micro của bạn phải vượt qua để được tính là "bạn đang nói" nhảy vọt lên — từ mức bình thường 0.015 lên 0.1. Nói cách khác: trong khi người kia đang nói, mic của bạn phải to hơn tiếng vọng của họ một cách rõ ràng, không mập mờ, thì chúng tôi mới ghi công lời nói cho bạn. Các khung mập mờ mặc định về "hệ thống", với giả định chúng là tiếng vọng. Đó là một thiên lệch có chủ ý, giúp gán đúng người nói trong đúng cái tình huống trước đây từng gán sai.
Bản chứng minh khái niệm đó — trộn mono cộng với nhận diện giọng nói dựa trên năng lượng và có ý thức về tiếng vọng — là tổ tiên của hệ thống gán người nói mà GeekBye đang chạy hôm nay. Phần giàn giáo đã bị gỡ bỏ ngay trong chính bản phát hành đó một khi nó tự chứng minh được mình, và phần toán năng lượng về sau đông cứng lại thành một mô-đun được kiểm thử đàng hoàng. Nhưng v1.6.12 là nơi ý tưởng ra đời, trong cảnh bức bách, bởi một người dùng với một cặp tai nghe nhét tai.
Ba điều cái bug tai nghe nhét tai đã dạy
- Một tay cầm khỏe mạnh vẫn có thể chở dữ liệu chết. Toàn bộ cuộc điều tra xoay quanh việc tin máy đo năng lượng của chúng tôi hơn các cờ trạng thái của nền tảng —
live,enabled,unmuted, tất cả đều nói dối trong khi các mẫu âm thanh toàn số không. Hãy giám sát dữ liệu, đừng nghe bản tự khai của API. Nếu một luồng tự nhận là ổn, hãy đo xem nó có thật sự ổn không. - Khi nền tảng chống lại kiến trúc của bạn, hãy đổi kiến trúc — chứ đừng đổi nền tảng. Chúng tôi không thể cãi thắng cơ chế bảo vệ phản hồi của Chromium, và mọi nỗ lực tắt nó bằng cách vòng vo bên rìa đều thất bại hoặc phản tác dụng. Loại bỏ chính điều kiện quan sát được — hai luồng gộp thành một — làm cả vấn đề biến mất. Gần như luôn rẻ hơn khi gỡ bỏ yếu tố kích hoạt so với việc cãi thắng trình duyệt.
- Khi bạn gộp các kênh để đổi lấy sự bền bỉ, bạn phải dựng lại thứ mình đã phá. Trộn thành mono đã giết đi sự tách kênh vốn chở "ai vừa nói". Thông tin đó phải được suy ra lại từ năng lượng tiền-trộn và các ngưỡng có ý thức về tiếng vọng. Phần logic bù trừ không phải là chuyện nghĩ ra sau bản vá — nó chính là một nửa của bản vá.
Đây là chương thứ tư của câu chuyện độ tin cậy trở thành GeekBye v2. Với chương trước, xem một cú rớt kết nối không nên làm sập cả ứng dụng (v1.6.8); với bản viết lại đường ống mà công việc âm thanh này dựa lên, chúng tôi đã xóa 5.000 dòng mã âm thanh (v1.6.0); và với toàn bộ cung đường, giải phẫu học của việc phát hành phần mềm đến độ hoàn hảo.