
Le bug des écouteurs : quand l'autre personne a disparu de notre transcription
La transcription à double canal fonctionnait parfaitement avec les haut-parleurs du portable et un casque circum-auriculaire. Puis quelqu'un a branché des écouteurs intra-auriculaires, et la personne à qui il parlait a tout simplement disparu de la transcription — parce que Chromium a coupé notre audio en silence à l'instant où il parlait. Onze correctifs ont échoué. Le douzième a changé toute l'architecture.
GeekBye transcrit les deux côtés d'une conversation : votre microphone sur un canal, l'audio de l'autre personne (ce que votre ordinateur diffuse) sur un autre. Cela fonctionnait sans faille avec les haut-parleurs du portable. Cela fonctionnait avec un casque circum-auriculaire. Puis un utilisateur a branché des écouteurs intra-auriculaires, et la personne à qui il parlait a disparu de la transcription. Pas déformée. Pas mal attribuée. Disparue.
Voici l'histoire de GeekBye v1.6.12, du bug qui a demandé onze correctifs ratés, et du moment où nous avons compris que nous luttions contre le navigateur au lieu de travailler avec lui.
La piste qui était vivante et vide
Le symptôme était étrange. Vous démarriez une réunion, tout semblait normal, vous disiez un seul mot — et à partir de cet instant, le canal audio de l'autre personne tombait à un pur silence numérique. Nous l'avons mesuré : le signal audio système se tenait à un niveau d'énergie normal (un RMS autour de 0.03–0.06), et au moment où le micro captait votre voix, il chutait à 0.0000 et y restait pour le reste de la session.
Voici la partie qui nous a coûté des jours. Toutes les API du navigateur insistaient sur le fait que la piste audio était parfaitement saine. readyState: live. enabled: true. muted: false. Selon chaque indicateur d'état exposé par la plateforme, le flux allait bien. C'étaient les échantillons qui étaient morts — un tuyau vivant ne transportant que des zéros. Nous avons passé longtemps à ne pas croire notre propre indicateur d'énergie, parce que la version officielle disait que la piste fonctionnait.
Le déclencheur était spécifique aux écouteurs intra-auriculaires, et la raison est physique : un écouteur intra-auriculaire est un seul appareil qui est à la fois votre entrée micro et votre sortie audio. Les haut-parleurs du portable et les casques circum-auriculaires séparent ces deux fonctions — la sortie va à un endroit, le micro est ailleurs. Mais quand l'entrée et la sortie sont le même appareil, vous avez, par définition, un chemin de rétroaction. Notre forte déduction (cela vit dans les entrailles du navigateur, donc nous ne pouvons pas citer chapitre et verset) est que Chromium détecte cette forme de micro-plus-haut-parleur-sur-un-seul-appareil, décide que c'est une boucle de rétroaction sur le point de siffler, et coupe la capture par protection — en silence, sans le dire à l'application via aucun des indicateurs qu'une application peut lire.
Onze correctifs, plusieurs d'entre eux pires
Nous l'avons attaqué d'abord par les voies évidentes, et avons noté chaque tentative. Désactiver l'annulation d'écho, la suppression de bruit et le gain automatique sur les deux flux. Démarrer les deux flux séquentiellement avec un délai plutôt qu'ensemble. Donner à chaque flux sa propre fréquence d'échantillonnage. Ajouter un nœud de gain silencieux. Router tout à travers un seul contexte audio partagé. Passer à un AudioWorklet. Descendre jusqu'au processeur de piste brut de WebCodecs.
Onze approches. Non seulement elles ont échoué — plusieurs ont empiré les choses. L'une a introduit un délai de 30 secondes. Une autre a fait que la transcription de votre micro apparaissait étiquetée comme celle de l'autre personne. Nous étions enfoncés jusqu'au cou, traitant chaque symptôme, et le bug continuait de se déplacer.
La percée a été une reformulation : chaque correctif jusque-là essayait de garder sains deux flux séparés. Mais deux flux séparés étaient exactement ce que l'heuristique de rétroaction de Chromium pouvait voir et couper. Et s'il n'y avait rien à comparer ?
Le correctif : donnez au navigateur un flux, pas deux
v1.6.12 a arrêté de lutter et a changé l'architecture. Au lieu d'envoyer le micro et l'audio système comme deux flux indépendants, elle les mixe en un seul flux mono sur le client avant que quoi que ce soit ne quitte l'application. Quand les deux sources passent par un seul pipeline, les pistes ne peuvent pas mourir indépendamment — il n'y a plus de forme de rétroaction à deux appareils que le navigateur puisse détecter et couper. La condition du déclencheur cesse simplement d'exister.
Il y eut une récompense charmante. Deux flux signifiaient deux WebSockets client-vers-backend et deux connexions backend-vers-fournisseur ; un seul flux mixé a réduit cela à un et un. Corriger le bug des écouteurs a réduit de moitié notre coût de transcription comme effet secondaire.
Mais le mixage mono a un problème évident : si vous fusionnez les deux personnes en un seul canal, comment savez-vous encore qui a dit quoi ? Vous avez jeté la séparation même qui vous le disait.
Reconstruire le « qui a parlé » à partir de l'énergie
La réponse a été de mesurer le volume de chaque source avant de les mixer, et d'attacher un indice à chaque fragment d'audio indiquant quelle source dominait — micro, système ou silence. La source la plus forte l'emporte. Assez simple avec des haut-parleurs.
Les écouteurs intra-auriculaires ont nécessité une couche de plus, et c'est ici que l'écho revient dans l'histoire — pointant dans le sens opposé à ce que vous imagineriez. Avec des écouteurs intra-auriculaires, la voix de l'autre personne s'échappe de l'écouteur et rentre dans le micro. Donc sans précaution, son écho, arrivant sur votre canal micro, est attribué à vous. Le correctif est un ensemble de seuils conscients de l'écho : quand l'audio système joue au-dessus d'un petit plancher, la barre que votre micro doit franchir pour compter comme « vous qui parlez » monte en flèche — d'un 0.015 normal à 0.1. Autrement dit : pendant que l'autre personne parle, votre micro doit être clairement et sans ambiguïté plus fort que son écho avant que nous ne créditions les mots à vous. Les trames ambiguës vont par défaut à « système », en supposant qu'il s'agit d'écho. C'est un biais délibéré qui réussit l'attribution dans la situation exacte qui la ratait autrefois.
Cette preuve de concept — mixage mono plus détection de voix basée sur l'énergie et consciente de l'écho — est l'ancêtre du système d'attribution que GeekBye fait tourner aujourd'hui. L'échafaudage a été arraché dans la même version une fois qu'il a fait ses preuves, et les mathématiques de l'énergie se sont ensuite durcies en un module dûment testé. Mais v1.6.12 est là où l'idée est née, sous la contrainte, grâce à un utilisateur avec une paire d'écouteurs intra-auriculaires.
Trois choses que le bug des écouteurs a enseignées
- Une poignée saine peut transporter des données mortes. Toute l'enquête a tenu au fait de faire confiance à notre indicateur d'énergie plutôt qu'aux indicateurs d'état de la plateforme —
live,enabled,unmuted, tous mentant alors que les échantillons étaient des zéros. Surveillez la charge utile, pas l'autodéclaration de l'API. Si un flux prétend aller bien, mesurez s'il va réellement bien. - Quand la plateforme lutte contre votre architecture, changez l'architecture — pas la plateforme. Nous ne pouvions pas gagner la discussion contre la protection de rétroaction de Chromium, et chaque tentative de la désactiver par les bords a échoué ou s'est retournée contre nous. Supprimer la condition observable — deux flux devenant un — a fait disparaître tout le problème. Il est presque toujours moins coûteux de supprimer le déclencheur que de gagner une bataille contre le navigateur.
- Quand vous fusionnez des canaux pour la robustesse, vous devez reconstruire ce que vous avez détruit. Le mixage en mono a tué la séparation des canaux qui transportait le « qui a parlé ». Cette information a dû être re-dérivée de l'énergie avant mixage et de seuils conscients de l'écho. La logique compensatoire n'est pas une réflexion après coup du correctif — elle est la moitié du correctif.
C'est le quatrième chapitre de l'histoire de fiabilité qui devient GeekBye v2. Pour le chapitre précédent, voir une connexion perdue ne devrait pas faire planter toute votre app (v1.6.8) ; pour la réécriture du pipeline sur laquelle ce travail audio s'est appuyé, nous avons supprimé 5 000 lignes de code audio (v1.6.0) ; et pour tout l'arc, l'anatomie de la livraison d'un logiciel jusqu'à la perfection.