
Kolm tegusõna, mis hoiavad Web Audio elus
Kaks GeekBye väljalaset, kahe kuu vahega ja kahes eri failis, õpetasid meie helikoodile sama õppetunni vastasotstest: lõpeta brauseri AudioContexti kohtlemine ühekordselt kasutatavana. Üks väljalase õppis kutsuma `resume()` kontekstile, mille macOS oli salaja peatanud keset salvestust; teine õppis kutsuma `suspend()` mitte `close()`, et järjestikused sessioonid lakkaksid põrkumast vastu Chromiumi umbes-kuue-konteksti lage. Resume, suspend, close — see ongi kogu süžee.
Enamiku ajast käitub Web Audio AudioContext nagu tavaline objekt: sa ehitad ühe, ühendad mõned sõlmed ja unustad selle ära. Kaks GeekBye väljalaset on see, mis juhtus, kui see eeldus purunes — kaks korda, rakenduse kahes eri osas, kahe kuu vahega. Kumbki ei olnud planeeritud teise järjena. Aga pane nad kõrvuti ja nad jutustavad ühe loo: brauseri AudioContext ei ole ühekordselt kasutatav objekt. See on napp, operatsioonisüsteemile kuuluv ressurss tõelise olekumasinaga, ja selle olekumasina kolm tegusõna — resume, suspend, close — on mõlema väljalaske kogu süžee.
GeekBye-l on kaks eraldi helikonveierit, mistõttu sama õppetundi tuli õppida kaks korda. Üks on videosalvestuse rada (ScreenRecordingService), mis ehitab Web Audio graafi, et miksida mikrofoni ja süsteemiheli salvestatavasse videofaili. Teine on reaalajas transkriptsiooni rada (RealtimeAudioCaptureService), mis ehitab oma graafi, et toita kõne-tekstiks-teisendust. Igaühel on oma AudioContext. Iga alljärgnev väljalase parandab neist ühe.
v1.8.16: video jäi vaikseks, aga transkriptsioon mitte
Esimene viga on lõhestatud aju. Sa salvestad koosolekut, vahetad hetkeks teise rakendusse ja midagi peent juhtub: videofail kaotab su mikrofoni, samal ajal kui otsetranskriptsioon kerib edasi, nagu poleks midagi muutunud. Transkriptsioon vannub, et sa rääkisid. Video on vaikne. See on hulluksajav vea liik, sest kaks asja, mis peaksid nõustuma — "kas mikrofon kuulis sind" — lihtsalt ei nõustu.
Commit, mis selle parandab, nimetab põhjuse täpselt:
AudioContext can be suspended by macOS when the app loses focus during screen recording. This silences the mic in the video file while the ScriptProcessorNode (STT path) keeps running on the JS main thread, causing a split where transcripts continue but the video has no mic.
Siin on mehaanika. ScreenRecordingService.setupMonoMixingPipeline() ehitab ühe AudioContexti, MediaStreamDestinationi, millest saab video helirada, GainNode-i vaigistuseks ja paari ScriptProcessorNode-i (createScriptProcessor kaudu), mis pumpavad helikaadreid VAD/STT loogikasse. Kui macOS peatab konteksti — mida see võib teha, kui rakendus ekraanisalvestuse ajal fookuse kaotab — peatuvad graafi osad, mida juhib heliriistvara kell: MediaStreamDestination lakkab mikrofoniheli vastu võtmast, nii et salvestatud video jääb vaikseks. Aga ScriptProcessorNode-i onaudioprocess tagasikutse jookseb JavaScripti põhilõimes, ja see lõim tiksub edasi. Nii et STT-pool jätkab transkriptsioonikirjete tootmist sellest, mis tal viimati oli, ja sama mikrofoni kaks tarbijat lahknevad.
Parandus on peatamist märgata ja see tühistada:
audioContext.onstatechange = () => {
if (audioContext.state === 'suspended' && this.status === 'recording') {
logger.info('AudioContext suspended during recording, resuming')
audioContext.resume()
}
}
See on kogu korrektsusemuudatus: jälgi konteksti state-i, ja kui see langeb suspended-iks, samal ajal kui me peaksime veel salvestama, kutsu resume(). Väljalase ühendab ka onmute, onunmute ja onended kuularid mikrofoni MediaStreamTrack-i külge — mitte käitumise muutmiseks, vaid selleks, et OS-tasandi vaigistus või seadme lahtiühendus ilmuks logidesse, mitte ei oleks nähtamatu vaikuse põhjus. Kui vea kogu raskus on "miks heli lakkas", siis heli lakkamise jälgitavaks tegemine on pool parandusest.
Järgnev commit maandus paar minutit hiljem, et koristada nende uute kuularite järelt: cleanup() nüüd nullib micTrack.onmute/onunmute/onended ja seab audioContext.onstatechange = null, enne kui graafi lammutab. Commiti märkus ütleb, et see "vastab RealtimeAudioCaptureService koristusmustrile" — rakenduse teisele heliteenusele. Pea seda detaili meeles, sest kaks kuud hiljem oli just see muster see, mille järgmine väljalase pidi ümber kirjutama.
v2.0.2: heli ja transkriptsioonid surevad pärast umbes kuut sessiooni
Teine viga elab teises konveieris, RealtimeAudioCaptureService-s, ja sellel on imeline kuju: kõik töötab laitmatult, ja siis pärast seda, kui oled ühe rakendussessiooni jooksul salvestamist mõned korrad alustanud ja peatanud, heli lihtsalt... sureb. Kõigepealt jääb süsteemiheli vaikseks, siis peatub transkriptsioon. Taaskäivita rakendus ja kõik on jälle korras — kuni järgmise paari sessioonini.
Algpõhjuse märkus väärib täies mahus lugemist, sest see on õpikunäide veast-vea-taga:
Root cause: setupMonoMixingPipeline created a new AudioContext on every start() and cleanup() closed it. Chromium hard-caps hardware AudioContexts at ~6 per page and closed-but-not-yet-GC'd contexts keep their slot, so ~6 back-to-back sessions exhaust the cap. The next context is dead → the silent-monitor passthrough can't play (no system audio) AND the mixer worklet can't capture (no transcript) — the reported 'after 6+ sessions audio dies, then transcript dies'.
Lõks on selles, et vana kood tegi "vastutustundlikku" asja. Iga start() lõi värske AudioContexti; iga cleanup() kutsus selle peal close(). Ressursi sulgemine, kui oled sellega valmis, on täpselt see, mida sulle õpetatakse tegema. Seal oli isegi reasisene kommentaar, mis seda vahetust põhjendas — et AudioWorklet mooduli uuesti laadimine iga värske konteksti kohta on hea, sest "värske kontekst iga start() kohta tähendab, et me maksame selle hinna alati üks kord."
Aga Chromium piirab, mitu riistvarapõhist AudioContexti üks leht võib hoida — umbes kuus — ja, mis on määrav, close() ei anna pesa tagasi samal hetkel, kui sa selle kutsud. Suletud kontekst jätkab oma pesa hõivamist, kuni see prügikoristatakse, mis juhtub siis, kui JS-mootor selleni jõuab. Nii et hoolikas sulge-igakord poliitika on täpselt see asi, mis viib su lakke: kuus loo/sulge tsüklit hiljem hoiavad kõiki kuut pesa veel koristamata laibad, seitsmes new AudioContext() tuleb surnuna tagasi, ja iga sõlm, mille sa selle külge lisad, teeb vaikselt mitte midagi.
Parandus on lõpetada konteksti kohtlemine ühekordselt kasutatavana. Loo see üks kord ja taaskasuta seda teenuse eluea jooksul:
setupMonoMixingPipeline-s konstrueerinew AudioContext(...)ainult siis, kui seda veel pole või olemasolev onclosed. Kui see on olemas, aga onsuspended, kutsuawait audioContext.resume()uue loomise asemel.cleanup-s asendaaudioContext.close()audioContext.suspend()-iga (kaitstud, nii et seda ei kutsuta kunagi juba suletud kontekstil). Kontekst jääb sessiooni üle elama.- Kuna kontekst jääb ellu, jääb laetud
AudioWorkletmoodul sellega kaasa — kood ei lähtesta enam oma "worklet laetud" lippu koristusel, nii etaudioWorklet.addModule(...)makstakse üks kord kogu teenuse eluea jooksul, mitte igal sessioonil.
Sessioonipõhised osad — createMediaStreamSource sõlmed mikrofoni ja süsteemiheli jaoks, mikseri worklet ja vaikne läbilask, mis hoiab elus Chromiumi süsteemiheli tagasisideahela haru — ehitatakse ja lammutatakse endiselt igal tsüklil. Ainult AudioContext ise püsib, nii et korraga eksisteerib ülimalt üks. Lagi on kättesaamatu, sest sa ei virna kunagi kontekste selle vastu.
Üks aus märkus, mille commit iseenda kohta teeb: see kontrolliti käsitsi — "manuaalselt kontrollitud: 8+ järjestikust sessiooni hoiavad süsteemiheli kuuldavana ja transkriptsioonid voolavana (oli surnud 7. sessioonil)" — ja autor on aus selles, et Web Audio elutsükkel "ei ole jsdom-is ühiktestitav ilma suure uue mock-raamistikuta". See on õige aususe liik. Tõrge ilmub ainult tõelise heliseadme vastu üle tõeliste sessioonide; roheline ühiktestide komplekt poleks tõestanud midagi. "Umbes kuus" ja "surnud 7. sessioonil" on meeskonna tähelepanekud selle kohta, kus Chromiumi piir hammustas, mitte dokumenteeritud konstant — kood ise pehmendab tildega, ja nii peaks tegema ka igaüks, kes seda kordab.
Resume, suspend, close
Rivista kaks väljalaset ritta ja sümmeetria on peaaegu liiga puhas. Mõlemad on sama kolme meetodi kohta samasugusel objektil, lähenetud vastassuundadest:
- v1.8.16 õppis kutsuma
resume(). Kontekst sai OS-i poolt jalge altsuspended-iks, ja parandus on see kinni püüda ja jätkata — hoida kontekst, mis peaks jooksma, jooksmas. - v2.0.2 õppis kutsuma
suspend()mitteclose(). Vana kood oli liiga innukas konteksti hävitama, ja parandus on see peatada ja taaskasutada — hoida elus kontekst, mille uuesti loomine on kallis.
Nad on ühe põhimõtte kaks poolt: lõpeta AudioContexti kohtlemine äravisatavana. Üks pool ütleb: ära lase sel surra, kui ei peaks; teine ütleb: ära tapa seda, kui sa ei pea. Ja ajajoones on väike iroonia — v1.8.16 koristus kirjutati nii, et see "vastaks RealtimeAudioCaptureService-i koristusmustrile", ja siis v2.0.2 kirjutas just selle mustri RealtimeAudioCaptureService-s ümber, sest mustri close()-igakord harjumus osutus veaks. Kaks teenust jätkavad üksteise helielutsükli käsitlusest õppimist, üks parandus korraga.
Kolm asja, mida need väljalasked õpetasid
AudioContext-il on olekumasin — kohtlestate-i kandvana.running,suspendedjaclosedei ole tühiasjad. OS võib konteksti nende vahel liigutada sinult küsimata, ja sinu graafi eri osad reageerivad erinevalt (ScriptProcessorNodepõhilõimes jätkab; riistvarakellaga juhitud sihtkoht mitte). Kui sa ei jälgionstatechange, usaldad sa, et kontekst jääb sinna, kuhu sa selle jätsid — ja tõelisel töölaual, fookusemuutuste ja ekraanisalvestuse all, ei jää.close()ei olefree(). Intuitsioon, et ressursi kohene sulgemine on alati õige, on täpselt see, mis põhjustas teise vea. Kui ressurss on pesa kõvas, brauseri kehtestatud kogumis, mis vabastatakse alles GC ajal, on agressiivne sulgemine viis, kuidas sa kogumi ammendad. Mõnikord on õige elutsükkel loo-üks-kord-peata-tihti, mitte loo-ja-hävita-iga-kasutuse-kohta.- Mõned vead on kättesaadavad ainult tõelise riistvara ja tõelise aja ukse kaudu. "Sureb pärast umbes kuut sessiooni" ja "jääb vaikseks, kui vahetad ära" ei suuda kinni püüda ühiktest, mis matkib heliseadet — ja meeskond ütles seda otse, mitte ei katnud võlts-läbiva testiga. Nimeta oma testiraamistiku piirid; viga, mis vajab ilmumiseks kaheksat tõelist sessiooni, väärib ausat "manuaalselt kontrollitud", mitte rohelist linnukest, mis ei tähenda midagi.
Eelmise peatüki jaoks — kõne eristamine avatud rakendusest (v1.8.15–v1.8.19); ja kogu kaare jaoks — tarkvara täiuslikkuseni tarnimise anatoomia.