
Το bug των ακουστικών: όταν ο άλλος συνομιλητής εξαφανίστηκε από τη μεταγραφή μας
Η μεταγραφή δύο καναλιών δούλευε άψογα στα ηχεία του laptop και στα over-ear ακουστικά. Μετά κάποιος έβαλε ακουστικά ψείρες, και ο άνθρωπος με τον οποίο μιλούσε απλώς εξαφανίστηκε από τη μεταγραφή — επειδή το Chromium σίγασε σιωπηλά τον ήχο μας τη στιγμή που εκείνος μίλησε. Έντεκα διορθώσεις απέτυχαν. Η δωδέκατη άλλαξε ολόκληρη την αρχιτεκτονική.
Το GeekBye μεταγράφει δύο πλευρές μιας συνομιλίας: το μικρόφωνό σου σε ένα κανάλι, τον ήχο του άλλου συμμετέχοντα (αυτό που αναπαράγει ο υπολογιστής σου) σε άλλο. Δούλευε άψογα στα ηχεία του laptop. Δούλευε στα over-ear ακουστικά. Μετά ένας χρήστης έβαλε ακουστικά ψείρες, και ο άνθρωπος με τον οποίο μιλούσε εξαφανίστηκε από τη μεταγραφή. Όχι παραμορφωμένος. Όχι λάθος αποδοσμένος. Εξαφανισμένος.
Αυτή είναι η ιστορία του GeekBye v1.6.12, του bug που χρειάστηκε έντεκα αποτυχημένες διορθώσεις, και της στιγμής που συνειδητοποιήσαμε ότι πολεμούσαμε τον browser αντί να συνεργαζόμαστε μαζί του.
Το track που ήταν ζωντανό και άδειο
Το σύμπτωμα ήταν ανατριχιαστικό. Ξεκινούσες μια συνάντηση, όλα φαίνονταν καλά, έλεγες μία λέξη — και από εκείνη τη στιγμή, το κανάλι ήχου του άλλου ανθρώπου περνούσε σε καθαρή ψηφιακή σιγή. Το μετρήσαμε: το σήμα του ήχου συστήματος στεκόταν σε κανονικό επίπεδο ενέργειας (RMS γύρω στο 0.03–0.06), και τη στιγμή που το μικρόφωνο έπιανε τη φωνή σου, έπεφτε στο 0.0000 και έμενε εκεί για το υπόλοιπο της συνεδρίας.
Να το κομμάτι που μας κόστισε μέρες. Κάθε API του browser επέμενε ότι το track ήχου ήταν απόλυτα υγιές. readyState: live. enabled: true. muted: false. Με κάθε flag κατάστασης που εξέθετε η πλατφόρμα, το ρεύμα ήταν εντάξει. Τα δείγματα ήταν νεκρά — ένας ζωντανός σωλήνας που κουβαλούσε μόνο μηδενικά. Περάσαμε πολλή ώρα μη πιστεύοντας το δικό μας μετρητή ενέργειας, επειδή η επίσημη εκδοχή έλεγε ότι το track δούλευε.
Ο πυροδότης ήταν ειδικός για τα ακουστικά ψείρες, και ο λόγος είναι φυσικός: μια ψείρα είναι μία συσκευή που είναι ταυτόχρονα η είσοδος του μικροφώνου σου και η έξοδος του ήχου σου. Τα ηχεία του laptop και τα over-ear ακουστικά τα διαχωρίζουν — η έξοδος πάει ένα μέρος, το μικρόφωνο είναι αλλού. Αλλά όταν η είσοδος και η έξοδος είναι η ίδια συσκευή, έχεις, εξ ορισμού, μια διαδρομή ανάδρασης. Το ισχυρό μας συμπέρασμα (αυτό ζει στα σπλάχνα του browser, οπότε δεν μπορούμε να παραθέσουμε κεφάλαιο και εδάφιο) είναι ότι το Chromium ανιχνεύει αυτό το σχήμα μικροφώνου-συν-ηχείου-σε-μία-συσκευή, αποφασίζει ότι είναι ένας βρόχος ανάδρασης έτοιμος να σφυρίξει, και προστατευτικά σιγάζει τη σύλληψη — σιωπηλά, χωρίς να το πει στην εφαρμογή μέσω κανενός από τα flags που μπορεί να διαβάσει μια εφαρμογή.
Έντεκα διορθώσεις, αρκετές από αυτές χειρότερες
Της επιτεθήκαμε πρώτα με τους προφανείς τρόπους, και γράφαμε κάθε προσπάθεια. Απενεργοποίησε την ακύρωση ηχούς, την καταστολή θορύβου και το αυτόματο gain και στα δύο ρεύματα. Ξεκίνα τα δύο ρεύματα διαδοχικά με μια καθυστέρηση αντί μαζί. Δώσε σε κάθε ρεύμα τον δικό του ρυθμό δειγματοληψίας. Πρόσθεσε έναν σιωπηλό κόμβο gain. Δρομολόγησε τα πάντα μέσα από ένα κοινό audio context. Πέρασε σε ένα AudioWorklet. Κατέβα μέχρι κάτω στον ωμό επεξεργαστή track του WebCodecs.
Έντεκα προσεγγίσεις. Όχι μόνο απέτυχαν — αρκετές το χειροτέρεψαν. Μία εισήγαγε μια καθυστέρηση 30 δευτερολέπτων. Άλλη έκανε τη μεταγραφή του μικροφώνου σου να εμφανίζεται με την ετικέτα του άλλου ανθρώπου. Ήμασταν βαθιά χωμένοι στα ζιζάνια, θεραπεύοντας κάθε σύμπτωμα, και το bug συνέχιζε να μετακινείται.
Η ανακάλυψη ήταν μια αναπλαισίωση: κάθε διόρθωση μέχρι τότε προσπαθούσε να κρατήσει δύο ξεχωριστά ρεύματα υγιή. Αλλά δύο ξεχωριστά ρεύματα ήταν ακριβώς το πράγμα που η ευρετική ανάδρασης του Chromium μπορούσε να δει και να σιγάσει. Κι αν δεν υπήρχε τίποτα να συγκριθεί;
Η διόρθωση: δώσε στον browser ένα ρεύμα, όχι δύο
Το v1.6.12 σταμάτησε να παλεύει και άλλαξε την αρχιτεκτονική. Αντί να στέλνει το μικρόφωνο και τον ήχο συστήματος ως δύο ανεξάρτητα ρεύματα, τα μιξάρει σε ένα ενιαίο mono ρεύμα στον client προτού οτιδήποτε φύγει από την εφαρμογή. Όταν και οι δύο πηγές ρέουν μέσα από έναν αγωγό, τα tracks δεν μπορούν να πεθάνουν ανεξάρτητα — δεν υπάρχει πια σχήμα ανάδρασης δύο συσκευών για να ανιχνεύσει και να σιγάσει ο browser. Η συνθήκη πυροδότησης απλώς παύει να υπάρχει.
Υπήρχε κι ένα ευχάριστο μπόνους. Δύο ρεύματα σήμαιναν δύο WebSocket client-προς-backend και δύο συνδέσεις backend-προς-πάροχο· ένα μιξαρισμένο ρεύμα κατέρρευσε αυτό σε ένα και ένα. Η διόρθωση του bug των ακουστικών μείωσε στο μισό το κόστος μεταγραφής μας ως παρενέργεια.
Αλλά η mono μίξη έχει ένα προφανές πρόβλημα: αν αναμείξεις και τους δύο ανθρώπους σε ένα κανάλι, πώς ξέρεις ακόμα ποιος είπε τι; Πέταξες ακριβώς εκείνον τον διαχωρισμό που σου το έλεγε.
Ξαναχτίζοντας το „ποιος μίλησε" από την ενέργεια
Η απάντηση ήταν να μετρήσεις την ένταση κάθε πηγής πριν τις μιξάρεις, και να προσαρτήσεις σε κάθε κομμάτι ήχου μια ένδειξη για το ποια πηγή κυριάρχησε — μικρόφωνο, σύστημα ή σιγή. Η δυνατότερη πηγή κερδίζει. Αρκετά απλό στα ηχεία.
Τα ακουστικά ψείρες χρειάζονταν ένα ακόμα στρώμα, και εδώ η ηχώ επιστρέφει στην ιστορία — στραμμένη προς την αντίθετη κατεύθυνση από ό,τι θα μάντευες. Στα ακουστικά ψείρες, η φωνή του άλλου ανθρώπου διαρρέει από την ψείρα και πίσω στο μικρόφωνο. Έτσι, χωρίς προσοχή, η δική του ηχώ, φτάνοντας στο κανάλι του μικροφώνου σου, αποδίδεται σε εσένα. Η διόρθωση είναι ένα σύνολο κατωφλιών με επίγνωση της ηχούς: όταν ο ήχος συστήματος παίζει πάνω από ένα μικρό δάπεδο, ο πήχης που το μικρόφωνό σου πρέπει να ξεπεράσει για να μετρήσει ως „εσύ μιλάς" εκτοξεύεται ψηλά — από ένα κανονικό 0.015 στο 0.1. Με άλλα λόγια: όσο ο άλλος άνθρωπος μιλάει, το μικρόφωνό σου πρέπει να είναι καθαρά, χωρίς αμφισημία, δυνατότερο από την ηχώ του προτού πιστώσουμε τις λέξεις σε εσένα. Τα διφορούμενα καρέ πάνε εξ ορισμού στο „σύστημα", με την υπόθεση ότι είναι ηχώ. Είναι μια σκόπιμη προκατάληψη που πετυχαίνει την απόδοση σωστά ακριβώς στην κατάσταση που παλιά την έκανε λάθος.
Εκείνο το proof-of-concept — mono μίξη συν ανίχνευση φωνής βασισμένη στην ενέργεια, με επίγνωση της ηχούς — είναι ο πρόγονος του συστήματος απόδοσης που τρέχει το GeekBye σήμερα. Η σκαλωσιά ξηλώθηκε στην ίδια κυκλοφορία μόλις αποδείχθηκε, και τα μαθηματικά της ενέργειας αργότερα σκλήρυναν σε ένα σωστά δοκιμασμένο module. Αλλά το v1.6.12 είναι εκεί που γεννήθηκε η ιδέα, υπό πίεση, από έναν χρήστη με ένα ζευγάρι ακουστικά ψείρες.
Τρία πράγματα που μας έμαθε το bug των ακουστικών
- Ένας υγιής χειριστής μπορεί να κουβαλάει νεκρά δεδομένα. Ολόκληρη η έρευνα στράφηκε στο να εμπιστευτούμε τον μετρητή ενέργειάς μας πάνω από τα flags κατάστασης της πλατφόρμας —
live,enabled,unmuted, όλα να λένε ψέματα ενώ τα δείγματα ήταν μηδενικά. Παρακολούθα το payload, όχι την αυτο-αναφορά του API. Αν ένα ρεύμα ισχυρίζεται ότι είναι εντάξει, μέτρα αν πραγματικά είναι. - Όταν η πλατφόρμα πολεμά την αρχιτεκτονική σου, άλλαξε την αρχιτεκτονική — όχι την πλατφόρμα. Δεν μπορούσαμε να νικήσουμε σε επιχειρήματα την προστασία ανάδρασης του Chromium, και κάθε προσπάθεια να την απενεργοποιήσουμε στα άκρα απέτυχε ή γύρισε μπούμερανγκ. Η αφαίρεση της παρατηρήσιμης συνθήκης — δύο ρεύματα να γίνονται ένα — έκανε ολόκληρο το πρόβλημα να εξαφανιστεί. Είναι σχεδόν πάντα φθηνότερο να αφαιρέσεις τον πυροδότη παρά να κερδίσεις μια μάχη με τον browser.
- Όταν συμπτύσσεις κανάλια για ανθεκτικότητα, πρέπει να ξαναχτίσεις ό,τι κατέστρεψες. Η μίξη σε mono σκότωσε τον διαχωρισμό καναλιών που κουβαλούσε το „ποιος μίλησε". Εκείνη η πληροφορία έπρεπε να επαναπαραχθεί από την ενέργεια πριν τη μίξη και κατώφλια με επίγνωση της ηχούς. Η αντισταθμιστική λογική δεν είναι μεταγενέστερη σκέψη στη διόρθωση — είναι το μισό της διόρθωσης.
Αυτό είναι το τέταρτο κεφάλαιο της ιστορίας αξιοπιστίας που γίνεται GeekBye v2. Για το προηγούμενο κεφάλαιο, δες μια χαμένη σύνδεση δεν θα έπρεπε να ρίχνει ολόκληρη την εφαρμογή σου (v1.6.8)· για την επανεγγραφή του pipeline πάνω στην οποία χτίστηκε αυτή η δουλειά ήχου, διαγράψαμε 5.000 γραμμές κώδικα ήχου (v1.6.0)· και για ολόκληρο το τόξο, η ανατομία της παράδοσης λογισμικού στην τελειότητα.