
Бъгът със слушалките: когато другият човек изчезна от нашата транскрипция
Двуканалната транскрипция работеше безупречно на високоговорителите на лаптопа и на слушалките тип over-ear. После някой включи слушалки тип „тапи" и човекът, с когото говореше, просто изчезна от транскрипцията — защото Chromium безшумно заглуши звука ни в мига, в който той проговори. Единайсет поправки се провалиха. Дванайсетата промени цялата архитектура.
GeekBye транскрибира двете страни на един разговор: твоят микрофон на един канал, звукът на другия участник (това, което възпроизвежда компютърът ти) на друг. Работеше безупречно на високоговорителите на лаптопа. Работеше на слушалките over-ear. После един потребител включи слушалки тип „тапи" и човекът, с когото говореше, изчезна от транскрипцията. Не изкривен. Не приписан на грешния. Изчезнал.
Това е историята на GeekBye v1.6.12, на бъга, който изисква единайсет провалени поправки, и на момента, в който осъзнахме, че се борим срещу браузъра, вместо да работим с него.
Тракът, който беше жив и празен
Симптомът беше зловещ. Пускаш среща, всичко изглежда наред, казваш една дума — и от този миг аудио каналът на другия човек минаваше в чиста цифрова тишина. Измерихме го: сигналът на системния звук стоеше на нормално енергийно ниво (RMS около 0.03–0.06), а в момента, в който микрофонът улавяше гласа ти, падаше до 0.0000 и оставаше там до края на сесията.
Ето частта, която ни струва дни. Всяко API в браузъра настояваше, че аудио тракът е напълно здрав. readyState: live. enabled: true. muted: false. По всеки статус флаг, който платформата излага, потокът беше наред. Семплите бяха мъртви — жива тръба, носеща само нули. Дълго време не вярвахме на собствения си измервател на енергия, защото официалната версия казваше, че тракът работи.
Спусъкът беше специфичен за слушалките „тапи" и причината е физическа: слушалката тип „тапи" е едно устройство, което е едновременно твоят вход за микрофон и твоят изход за звук. Високоговорителите на лаптопа и слушалките over-ear ги разделят — изходът отива на едно място, микрофонът е другаде. Но когато входът и изходът са едно и също устройство, имаш, по дефиниция, път на обратна връзка. Нашето силно заключение (това живее в недрата на браузъра, затова не можем да цитираме глава и стих) е, че Chromium засича тази форма на микрофон-плюс-високоговорител-на-едно-устройство, решава, че е цикъл на обратна връзка, готов да заквичи, и защитно заглушава улавянето — безшумно, без да казва на приложението чрез никой от флаговете, които едно приложение може да прочете.
Единайсет поправки, няколко от тях по-лоши
Атакувахме го първо по очевидните начини и записвахме всеки опит. Изключи потискането на ехо, шумопотискането и автоматичното усилване на двата потока. Стартирай двата потока последователно със забавяне, вместо заедно. Дай на всеки поток собствена честота на дискретизация. Добави безшумен gain възел. Прекарай всичко през един споделен аудио контекст. Премини към AudioWorklet. Слез чак до суровия процесор на трак от WebCodecs.
Единайсет подхода. Не само че се провалиха — няколко влошиха нещата. Един въведе забавяне от 30 секунди. Друг накара транскрипцията на твоя микрофон да се показва с етикет на другия човек. Бяхме затънали дълбоко в детайлите, лекувайки всеки симптом, а бъгът все се местеше.
Пробивът беше преформулиране: всяка поправка досега се опитваше да поддържа два отделни потока здрави. Но два отделни потока бяха точно това, което евристиката за обратна връзка на Chromium можеше да види и заглуши. Ами ако нямаше какво да се сравнява?
Поправката: дай на браузъра един поток, не два
v1.6.12 спря да се бори и промени архитектурата. Вместо да изпраща микрофона и системния звук като два независими потока, той ги смесва в един моно поток на клиента преди каквото и да е да напусне приложението. Когато и двата източника текат през един конвейер, траковете не могат да умрат независимо — вече няма форма на обратна връзка с две устройства, която браузърът да засече и заглуши. Условието на спусъка просто спира да съществува.
Имаше и приятен бонус. Два потока означаваха два WebSocket-а клиент-към-бекенд и две връзки бекенд-към-доставчик; един смесен поток свиваше това до едно и едно. Поправянето на бъга със слушалките наполовина намали разхода ни за транскрипция като страничен ефект.
Но моно смесването има очевиден проблем: ако смесиш и двамата души в един канал, как още знаеш кой какво е казал? Изхвърлил си точно онова разделяне, което ти го казваше.
Изграждане наново на „кой говори" от енергията
Отговорът беше да измериш силата на всеки източник преди да ги смесиш и да прикачиш към всеки къс аудио подсказка кой източник е доминирал — микрофон, система или тишина. По-силният източник печели. Достатъчно просто на високоговорители.
Слушалките „тапи" имаха нужда от още един слой, и тук ехото се връща в историята — насочено в обратната посока от онова, което би предположил. На слушалки „тапи" гласът на другия човек изтича от тапата и обратно в микрофона. Така че без внимание неговото ехо, пристигащо на твоя микрофонен канал, се приписва на теб. Поправката е набор от прагове, съобразени с ехото: когато системният звук свири над малък праг, летвата, която микрофонът ти трябва да прескочи, за да се брои като „ти говориш", скача рязко нагоре — от нормалните 0.015 до 0.1. С други думи: докато другият човек говори, микрофонът ти трябва да е ясно, недвусмислено по-силен от неговото ехо, преди да припишем думите на теб. Двусмислените кадри отиват по подразбиране към „система", при допускането, че са ехо. Това е умишлено пристрастие, което улучва приписването точно в ситуацията, която преди го грешеше.
Този proof-of-concept — моно смесване плюс разпознаване на глас, базирано на енергия и съобразено с ехото — е прародителят на системата за приписване, която GeekBye използва днес. Скелето беше изтръгнато в същото издание, щом се доказа, а математиката на енергията по-късно се втвърди в правилно тестван модул. Но v1.6.12 е мястото, където се роди идеята, под натиск, от потребител с чифт слушалки „тапи".
Три неща, на които бъгът със слушалките ни научи
- Здрав манипулатор може да носи мъртви данни. Цялото разследване се завъртя около това да се доверим на нашия измервател на енергия пред статус флаговете на платформата —
live,enabled,unmuted, всички лъжещи, докато семплите бяха нули. Наблюдавай полезния товар, не самоотчета на API-то. Ако един поток твърди, че е наред, измери дали наистина е. - Когато платформата се бори с архитектурата ти, промени архитектурата — не платформата. Не можахме да надвием в спор защитата от обратна връзка на Chromium и всеки опит да я изключим по краищата се провали или се обърна срещу нас. Премахването на наблюдаемото условие — два потока стават един — накара целия проблем да изчезне. Почти винаги е по-евтино да премахнеш спусъка, отколкото да спечелиш битка с браузъра.
- Когато сливаш канали заради устойчивост, трябва да изградиш наново това, което си унищожил. Смесването до моно уби разделянето на канали, което носеше „кой говори". Тази информация трябваше да бъде извлечена наново от енергията преди смесването и прагове, съобразени с ехото. Компенсиращата логика не е допълнение след поправката — тя е половината от поправката.
Това е четвъртата глава от историята за надеждността, която става GeekBye v2. За предишната глава виж една прекъсната връзка не бива да срива цялото ти приложение (v1.6.8); за пренаписването на конвейера, върху което тази аудио работа е стъпила, изтрихме 5000 реда аудио код (v1.6.0); а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.