
Trīs darbības vārdi, kas uztur Web Audio dzīvu
Divi GeekBye laidieni, divu mēnešu attālumā un divos dažādos failos, iemācīja mūsu audio kodam vienu un to pašu mācību no pretējiem galiem: pārstāt izturēties pret pārlūka AudioContext kā pret vienreizlietojamu. Viens laidiens iemācījās izsaukt resume() kontekstam, ko macOS bija klusi apturējis ieraksta vidū; otrs iemācījās izsaukt suspend() nevis close(), lai secīgas sesijas pārstātu triekties Chromium aptuveni sešu kontekstu griestos. Resume, suspend, close — tas ir viss sižets.
Lielāko daļu laika Web Audio AudioContext uzvedas kā parasts objekts: tu izveido vienu, savieno dažus mezglus un aizmirsti par to. Divi GeekBye laidieni ir tas, kas notika, kad šis pieņēmums sabruka — divreiz, divās dažādās lietotnes daļās, divu mēnešu attālumā. Neviens no tiem nebija plānots kā otra turpinājums. Bet noliec tos blakus, un tie stāsta vienu stāstu: pārlūka AudioContext nav vienreizlietojams objekts. Tas ir rets, operētājsistēmai piederošs resurss ar īstu stāvokļu mašīnu, un šīs stāvokļu mašīnas trīs darbības vārdi — resume, suspend, close — ir abu laidienu viss sižets.
GeekBye ir divi atsevišķi audio konveijeri, tāpēc vienu un to pašu mācību nācās iemācīties divreiz. Viens ir video ieraksta ceļš (ScreenRecordingService), kas veido Web Audio grafu, lai sajauktu mikrofona un sistēmas audio ierakstītā video failā. Otrs ir reāllaika transkripcijas ceļš (RealtimeAudioCaptureService), kas veido savu grafu, lai barotu runas-uz-tekstu. Katram ir savs AudioContext. Katrs zemāk aprakstītais laidiens izlabo vienu no tiem.
v1.8.16: video kļuva kluss, bet transkripcija ne
Pirmā kļūda ir šķelts prāts. Tu ieraksti sapulci, uz mirkli pārslēdzies uz citu lietotni, un notiek kaut kas smalks: video fails pazaudē tavu mikrofonu, kamēr dzīvā transkripcija turpina ritināties, it kā nekas nebūtu mainījies. Transkripcija zvēr, ka tu runāji. Video ir kluss. Tā ir prātu jaucoša kļūda, jo divas lietas, kurām vajadzētu saskanēt — "vai mikrofons tevi dzirdēja" — pilnīgi nesaskan.
Komits, kas to izlabo, precīzi nosauc cēloni:
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.
Lūk mehānika. ScreenRecordingService.setupMonoMixingPipeline() izveido vienu AudioContext, MediaStreamDestination, kas kļūst par video audio celiņu, GainNode izslēgšanai un pāris ScriptProcessorNode (caur createScriptProcessor), kas sūknē audio kadrus uz VAD/STT loģiku. Kad macOS aptur kontekstu — ko tas var darīt, kad lietotne zaudē fokusu ekrāna ieraksta laikā — grafa daļas, ko dzen audio aparatūras pulkstenis, apstājas: MediaStreamDestination pārstāj saņemt mikrofona audio, tāpēc ierakstītais video kļūst kluss. Bet ScriptProcessorNode onaudioprocess atzvans darbojas JavaScript galvenajā pavedienā, un šis pavediens turpina tikšķēt. Tāpēc STT puse turpina ražot transkripcijas ierakstus no tā, kas tai bija pēdējais, un divi tā paša mikrofona patērētāji atšķiras.
Labojums ir pamanīt apturēšanu un to atsaukt:
audioContext.onstatechange = () => {
if (audioContext.state === 'suspended' && this.status === 'recording') {
logger.info('AudioContext suspended during recording, resuming')
audioContext.resume()
}
}
Tā ir visa pareizības izmaiņa: vēro konteksta state, un, ja tas nokrīt uz suspended, kamēr mums vēl vajadzētu ierakstīt, izsauc resume(). Laidiens arī pievieno onmute, onunmute un onended klausītājus mikrofona MediaStreamTrack — nevis lai mainītu uzvedību, bet lai OS līmeņa izslēgšana vai ierīces atvienošanās parādītos žurnālos, nevis būtu neredzams klusuma cēlonis. Kad visa kļūdas grūtība ir "kāpēc audio apstājās", padarīt audio apstāšanos novērojamu ir puse no labojuma.
Turpinājuma komits nolaidās pāris minūtes vēlāk, lai sakoptu pēc šiem jaunajiem klausītājiem: cleanup() tagad iestata micTrack.onmute/onunmute/onended uz null un iestata audioContext.onstatechange = null, pirms tas nojauc grafu. Komita piezīme saka, ka tas "atbilst RealtimeAudioCaptureService sakopšanas šablonam" — lietotnes otram audio pakalpojumam. Paturi šo detaļu, jo divus mēnešus vēlāk tieši šo šablonu nākamajam laidienam nācās pārrakstīt.
v2.0.2: audio un transkripcijas mirst pēc apmēram sešām sesijām
Otrā kļūda dzīvo otrā konveijerā, RealtimeAudioCaptureService, un tai ir brīnišķīga forma: viss darbojas nevainojami, un tad, kad esi sācis un apturējis ierakstu dažas reizes vienā lietotnes sesijā, audio vienkārši... mirst. Vispirms sistēmas audio kļūst kluss, tad transkripcija apstājas. Restartē lietotni, un atkal viss ir kārtībā — līdz nākamajām dažām sesijām.
Cēloņa piezīme ir vērta izlasīšanas pilnībā, jo tā ir mācību grāmatas kļūda-aiz-kļūdas:
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'.
Slazds ir tas, ka vecais kods darīja "atbildīgo" lietu. Katrs start() izveidoja svaigu AudioContext; katrs cleanup() izsauca tam close(). Aizvērt resursu, kad esi ar to pabeidzis, ir tieši tas, ko tev māca darīt. Bija pat iekļauts komentārs, kas pamatoja šo maiņu — ka AudioWorklet moduļa pārlādēšana katram svaigam kontekstam ir labi, jo "svaigs konteksts katram start() nozīmē, ka mēs vienmēr maksājam šo cenu vienreiz."
Bet Chromium ierobežo, cik daudz aparatūras atbalstītu AudioContext viena lapa var turēt — aptuveni sešus — un, izšķiroši, close() neatdod slotu atpakaļ tajā pašā mirklī, kad to izsauc. Aizvērts konteksts turpina aizņemt savu slotu, līdz tas tiek atkritumu savākts, kas notiek, kad vien JS dzinējs līdz tam nonāk. Tāpēc rūpīga aizver-katru-reizi politika ir tieši tā lieta, kas ieved tevi griestos: sešus izveidot/aizvērt ciklus vēlāk visus sešus slotus tur vēl nesavākti līķi, septītais new AudioContext() atgriežas miris, un katrs mezgls, ko tam pievieno, klusi nedara neko.
Labojums ir pārstāt izturēties pret kontekstu kā pret vienreizlietojamu. Izveido to vienreiz un lieto atkārtoti visu pakalpojuma dzīves laiku:
setupMonoMixingPipelinekonstruēnew AudioContext(...)tikai tad, ja tāda vēl nav vai esošais irclosed. Ja tas eksistē, bet irsuspended, izsaucawait audioContext.resume(), nevis veido jaunu.cleanupaizstājaudioContext.close()araudioContext.suspend()(aizsargāts, lai tas nekad netiktu izsaukts jau aizvērtam kontekstam). Konteksts pārdzīvo sesiju.- Tā kā konteksts pārdzīvo, ielādētais
AudioWorkletmodulis pārdzīvo līdzi — kods vairs neatiestata savu "worklet ielādēts" karodziņu sakopšanas laikā, tāpēcaudioWorklet.addModule(...)tiek apmaksāts vienreiz visu pakalpojuma dzīves laiku, nevis katru sesiju.
Katras sesijas daļas — createMediaStreamSource mezgli mikrofonam un sistēmas audio, miksera worklet un klusais caurlaides, kas uztur dzīvu Chromium sistēmas audio cilpas atzarojumu — joprojām tiek veidotas un nojauktas katru ciklu. Tikai pats AudioContext saglabājas, tāpēc jebkurā brīdī pastāv ne vairāk kā viens. Griesti ir nesasniedzami, jo tu nekad nekrāj kontekstus pret tiem.
Viena godīga piezīme, ko komits izsaka par sevi: tas tika pārbaudīts ar rokām — "manuāli pārbaudīts: 8+ secīgas sesijas uztur sistēmas audio dzirdamu un transkripcijas plūstošas (bija miris 7. sesijā)" — un autors ir atklāts, ka Web Audio dzīves cikls "nav vienībtestējams jsdom bez lielas jaunas imitācijas iekārtas". Tā ir pareizā godīguma šķirne. Kļūme parādās tikai pret īstu audio ierīci vairākās īstās sesijās; zaļa vienībtestu kopa nebūtu pierādījusi neko. "Aptuveni seši" un "miris 7. sesijā" ir komandas novērojumi par to, kur Chromium ierobežojums iekoda, nevis dokumentēta konstante — pats kods piesardzīgi lieto tildi, un tā vajadzētu arī ikvienam, kas to atkārto.
Resume, suspend, close
Noliec abus laidienus rindā, un simetrija ir gandrīz pārāk glīta. Abi ir par vienām un tām pašām trim metodēm uz viena veida objekta, pieietiem no pretējiem virzieniem:
- v1.8.16 iemācījās izsaukt
resume(). OS kontekstu apturēja (suspended) tam zem kājām, un labojums ir noķert to un atsākt — turēt kontekstu, kam vajadzētu darboties, darbojošos. - v2.0.2 iemācījās izsaukt
suspend(), nevisclose(). Vecais kods bija pārāk dedzīgs iznīcināt kontekstu, un labojums ir to apturēt un lietot atkārtoti — turēt dzīvu kontekstu, kuru ir dārgi izveidot no jauna.
Tie ir vienota principa divas puses: pārstāj izturēties pret AudioContext kā pret izmetamu. Viena puse saka: neļauj tam mirt, kad nevajadzētu; otra saka: nenogalini to, kad tev nav jādara. Un laika grafikā ir maza ironija — v1.8.16 sakopšana tika uzrakstīta, lai "atbilstu RealtimeAudioCaptureService sakopšanas šablonam", un tad v2.0.2 pārrakstīja tieši šo šablonu RealtimeAudioCaptureService, jo šablona close()-katru-reizi ieradums izrādījās esam kļūda. Abi pakalpojumi turpina mācīties viens no otra audio dzīves cikla apstrādes, viens labojums pa reizei.
Trīs lietas, ko šie laidieni iemācīja
AudioContextir stāvokļu mašīna — izturies pretstatekā pret nesošu.running,suspendedunclosednav sīkumi. OS var pārvietot kontekstu starp tiem, tev neprasot, un dažādas tava grafa daļas reaģē atšķirīgi (ScriptProcessorNodegalvenajā pavedienā turpina darboties; aparatūras pulksteņa vadītais galamērķis ne). Ja tu nevēroonstatechange, tu uzticies, ka konteksts paliek tur, kur tu to atstāji — un uz īsta galddatora, fokusa izmaiņu un ekrāna ieraksta apstākļos, tas nepaliks.close()navfree(). Intuīcija, ka resursa tūlītēja aizvēršana vienmēr ir pareiza, ir tieši tas, kas izraisīja otro kļūdu. Kad resurss ir slots cietā, pārlūka uzspiestā baseinā, kas tiek atgūts tikai GC laikā, agresīva aizvēršana ir veids, kā tu izsmel baseinu. Dažreiz pareizais dzīves cikls ir izveido-vienreiz-aptur-bieži, nevis izveido-un-iznīcini-katrai-lietošanai.- Dažas kļūdas ir sasniedzamas tikai caur īstas aparatūras un īsta laika durvīm. "Mirst pēc apmēram sešām sesijām" un "kļūst kluss, kad pārslēdzies prom" nevar noķert vienībtests, kas imitē audio ierīci — un komanda to pateica atklāti, nevis apslēpa ar viltus-izturošu testu. Nosauc sava testa iekārtas robežas; kļūda, kurai vajag astoņas īstas sesijas, lai parādītos, ir pelnījusi godīgu "manuāli pārbaudīts", nevis zaļu ķeksīti, kas nenozīmē neko.
Iepriekšējai nodaļai — zvana atšķiršana no atvērtas lietotnes (v1.8.15–v1.8.19); un visai lokai — programmatūras piegādes līdz pilnībai anatomiju.