Steven
Steven8 minit bacaan

Tiga Kata Kerja yang Memastikan Web Audio Terus Hidup

Dua keluaran titik GeekBye, berselang dua bulan dan dalam dua fail berbeza, mengajar kod audio kami pelajaran yang sama daripada hujung yang bertentangan: berhenti memperlakukan AudioContext pelayar sebagai barangan sekali guna. Satu keluaran belajar untuk resume() satu context yang telah macOS gantung secara senyap di tengah rakaman; satu lagi belajar untuk suspend() dan bukan close() supaya sesi berturut-turut berhenti melanggar siling kira-kira enam context milik Chromium. Resume, suspend, close — itulah keseluruhan jalan ceritanya.

Kejuruteraan
Audio
Desktop
GeekBye Releases
Tiga Kata Kerja yang Memastikan Web Audio Terus Hidup

Kebanyakan masa, satu Web Audio AudioContext berkelakuan seperti objek biasa: anda membina satu, menyambungkan beberapa nod, dan melupakannya. Dua keluaran GeekBye ialah apa yang berlaku apabila andaian itu runtuh — dua kali, dalam dua bahagian aplikasi yang berbeza, berselang dua bulan. Tiada satu pun dirancang sebagai susulan kepada yang lain. Tetapi letakkan keduanya bersebelahan dan ia menceritakan satu kisah: AudioContext pelayar bukanlah satu objek sekali guna. Ia ialah satu sumber langka, milik sistem pengendalian, dengan satu mesin keadaan sebenar, dan tiga kata kerja mesin keadaan itu — resume, suspend, close — ialah keseluruhan jalan cerita kedua-dua keluaran.

GeekBye ada dua saluran paip audio berasingan, itulah sebabnya pelajaran yang sama terpaksa dipelajari dua kali. Satu ialah laluan rakaman video (ScreenRecordingService), yang membina satu graf Web Audio untuk mencampurkan audio mik dan sistem ke dalam satu fail video terakam. Satu lagi ialah laluan transkripsi masa nyata (RealtimeAudioCaptureService), yang membina grafnya sendiri untuk menyuap pertuturan-ke-teks. Setiap satu mempunyai AudioContext-nya sendiri. Setiap keluaran di bawah membetulkan salah satu daripadanya.

v1.8.16: video menjadi senyap, tetapi transkrip tidak

Pepijat pertama ialah otak-terbelah. Anda sedang merakam satu mesyuarat, anda bertukar tab sebentar ke aplikasi lain, dan sesuatu yang halus berlaku: fail video kehilangan mikrofon anda sementara transkrip langsung terus menatal seolah-olah tiada apa yang berubah. Transkrip bersumpah anda sedang bercakap. Video senyap. Itu jenis pepijat yang membuat naik angin, kerana dua perkara yang sepatutnya bersetuju — "adakah mik mendengar anda" — sama sekali tidak bersetuju.

Commit yang membetulkannya menamakan puncanya dengan tepat:

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.

Inilah mekanismenya. ScreenRecordingService.setupMonoMixingPipeline() membina satu AudioContext, satu MediaStreamDestination yang menjadi trek audio video, satu GainNode untuk bisu, dan sepasang ScriptProcessorNode (melalui createScriptProcessor) yang mengepam bingkai audio ke logik VAD/STT. Apabila macOS menggantung context — yang boleh ia lakukan apabila aplikasi hilang fokus semasa rakaman skrin — bahagian graf yang dipacu oleh jam perkakasan audio berhenti: MediaStreamDestination berhenti menerima audio mik, jadi video terakam menjadi sunyi. Tetapi panggilan balik onaudioprocess bagi satu ScriptProcessorNode berjalan pada thread utama JavaScript, dan thread itu terus berdetik. Jadi pihak STT terus menghasilkan entri transkrip daripada apa sahaja yang terakhir dimilikinya, dan kedua-dua pengguna mikrofon yang sama itu bercapah.

Pembetulannya ialah menyedari penggantungan itu dan membatalkannya:

audioContext.onstatechange = () => {
  if (audioContext.state === 'suspended' && this.status === 'recording') {
    logger.info('AudioContext suspended during recording, resuming')
    audioContext.resume()
  }
}

Itulah keseluruhan perubahan ketepatannya: perhatikan state context, dan jika ia jatuh ke suspended sementara kita masih sepatutnya merakam, panggil resume(). Keluaran ini juga menyambungkan pendengar onmute, onunmute, dan onended pada MediaStreamTrack mikrofon — bukan untuk mengubah tingkah laku, tetapi supaya satu pembisuan peringkat-OS atau satu pemutusan peranti muncul dalam log dan bukan menjadi punca kesenyapan yang tidak kelihatan. Apabila keseluruhan kesukaran satu pepijat ialah "mengapa audio berhenti," menjadikan pemberhentian audio itu boleh cerap ialah separuh daripada pembetulan.

Satu commit susulan mendarat beberapa minit kemudian untuk mengemas selepas pendengar-pendengar baharu itu: cleanup() kini menjadikan micTrack.onmute/onunmute/onended null dan menetapkan audioContext.onstatechange = null sebelum ia merobohkan graf. Nota commit itu berkata ia "matching RealtimeAudioCaptureService cleanup pattern" — perkhidmatan audio lain milik aplikasi. Pegang butiran itu, kerana dua bulan kemudian corak itulah yang terpaksa ditulis semula oleh keluaran seterusnya.

v2.0.2: audio dan transkrip mati selepas kira-kira enam sesi

Pepijat kedua tinggal dalam saluran paip yang lain, RealtimeAudioCaptureService, dan ia mempunyai bentuk yang indah: semuanya berfungsi dengan sempurna, dan kemudian selepas anda memulakan dan menghentikan rakaman beberapa kali dalam satu sesi aplikasi, audio itu begitu sahaja... mati. Mula-mula audio sistem menjadi sunyi, kemudian transkrip berhenti. Mulakan semula aplikasi dan ia elok semula — sehingga beberapa sesi lagi.

Nota punca-akar berbaloi dibaca sepenuhnya, kerana ia satu pepijat-di-sebalik-pepijat gaya buku teks:

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'.

Perangkapnya ialah kod lama sedang melakukan perkara yang "bertanggungjawab." Setiap start() mencipta satu AudioContext yang segar; setiap cleanup() memanggil close() padanya. Menutup satu sumber apabila anda selesai dengannya ialah tepat apa yang diajar untuk anda lakukan. Malah ada satu komen sebaris yang merasionalkan pergolakan itu — bahawa memuat semula modul AudioWorklet bagi setiap context segar itu tidak mengapa kerana "a fresh context per start() means we always pay this cost once."

Tetapi Chromium mengehadkan berapa banyak AudioContext bersokongan perkakasan yang boleh dipegang oleh satu halaman — kira-kira enam — dan, yang penting, close() tidak memulangkan slot itu sebaik anda memanggilnya. Satu context tertutup terus menduduki slotnya sehingga ia dikutip oleh pengutip sampah, yang berlaku bila-bila masa enjin JS sempat menanganinya. Jadi satu dasar tutup-setiap-kali yang teliti ialah tepat perkara yang membawa anda masuk ke siling: enam kitaran cipta/tutup kemudian, keenam-enam slot semuanya dipegang oleh mayat yang belum dikutip, new AudioContext() ketujuh kembali dalam keadaan mati, dan setiap nod yang anda sambungkan padanya secara senyap tidak melakukan apa-apa.

Pembetulannya ialah berhenti memperlakukan context sebagai barangan sekali guna. Cipta ia sekali dan guna semula untuk sepanjang hayat perkhidmatan:

  • Dalam setupMonoMixingPipeline, bina satu new AudioContext(...) hanya jika belum ada satu pun atau yang sedia ada ialah closed. Jika ia wujud tetapi suspended, await audioContext.resume() dan bukan membuat yang baharu.
  • Dalam cleanup, gantikan audioContext.close() dengan audioContext.suspend() (dikawal supaya ia tidak pernah dipanggil pada context yang telah ditutup). Context bertahan melepasi sesi.
  • Kerana context bertahan, modul AudioWorklet yang dimuatkan bertahan bersamanya — kod tidak lagi menetapkan semula bendera "worklet loaded"-nya semasa cleanup, jadi audioWorklet.addModule(...) dibayar sekali untuk sepanjang hayat perkhidmatan dan bukan setiap sesi.

Kepingan per-sesi — nod createMediaStreamSource untuk audio mik dan sistem, worklet pencampur, dan laluan senyap yang mengekalkan pili gelung balik audio-sistem Chromium tetap hidup — masih dibina dan dirobohkan setiap kitaran. Hanya AudioContext itu sendiri yang berkekalan, jadi paling banyak ada satu yang wujud pada bila-bila masa. Siling itu tidak boleh dicapai kerana anda tidak pernah menimbun context terhadapnya.

Satu nota jujur yang commit itu buat tentang dirinya sendiri: ini disahkan secara manual — "manually verified: 8+ back-to-back sessions keep system audio audible and transcripts flowing (was dead at session 7)" — dan penulisnya berterus terang bahawa kitaran hayat Web Audio "isn't unit-testable in jsdom without a large new mock harness." Itu jenis kejujuran yang betul. Kegagalan itu hanya muncul terhadap peranti audio sebenar merentasi sesi sebenar; satu suite unit yang hijau tidak akan membuktikan apa-apa. "Kira-kira enam" dan "mati pada sesi 7" ialah pemerhatian pasukan tentang di mana had Chromium menggigit, bukan satu pemalar terdokumentasi — kod itu sendiri berhati-hati dengan satu tilde, dan begitu juga sesiapa yang mengulanginya.

Resume, suspend, close

Barisankan kedua-dua keluaran dan simetrinya hampir terlalu kemas. Kedua-duanya tentang tiga kaedah yang sama pada jenis objek yang sama, didekati daripada arah yang bertentangan:

  • v1.8.16 belajar untuk resume(). Context itu di-suspended dari bawahnya oleh OS, dan pembetulannya ialah menangkap itu dan menyambung semula — kekalkan satu context yang sepatutnya berjalan, tetap berjalan.
  • v2.0.2 belajar untuk suspend() dan bukan close(). Kod lama terlalu ghairah untuk memusnahkan context, dan pembetulannya ialah menggantung dan menggunakannya semula — kekalkan satu context yang mahal untuk dicipta semula, tetap hidup.

Ia ialah dua bahagian daripada satu prinsip: berhenti memperlakukan AudioContext sebagai buangan. Satu bahagian berkata jangan biarkan ia mati apabila ia tidak sepatutnya; satu lagi berkata jangan bunuh ia apabila anda tidak perlu. Dan ada sedikit ironi dalam garis masa — cleanup v1.8.16 ditulis untuk "match RealtimeAudioCaptureService's cleanup pattern," dan kemudian v2.0.2 menulis semula tepat corak itu dalam RealtimeAudioCaptureService, kerana tabiat close()-setiap-kali corak itu ternyata ialah pepijatnya. Kedua-dua perkhidmatan terus belajar daripada pengendalian kitaran-hayat-audio satu sama lain, satu pembetulan pada satu masa.

Tiga perkara yang keluaran ini ajarkan kepada kami

  1. Satu AudioContext mempunyai satu mesin keadaan — perlakukan state sebagai penanggung beban. running, suspended, dan closed bukan perkara remeh. OS boleh menggerakkan satu context antara mereka tanpa bertanya kepada anda, dan bahagian graf yang berbeza bertindak balas secara berbeza (satu ScriptProcessorNode pada thread utama terus berjalan; destinasi berjam-perkakasan tidak). Jika anda tidak memerhatikan onstatechange, anda mempercayai bahawa context kekal di tempat anda tinggalkannya — dan pada desktop sebenar, di bawah perubahan fokus dan rakaman skrin, ia tidak akan begitu.
  2. close() bukan free(). Gerak hati bahawa menutup satu sumber dengan segera sentiasa betul ialah tepat apa yang menyebabkan pepijat kedua. Apabila sumber itu ialah satu slot dalam satu kolam keras yang dikenakan pelayar yang hanya dituntut semula pada masa GC, menutup secara agresif ialah cara anda menghabiskan kolam itu. Kadangkala kitaran hayat yang betul ialah cipta-sekali-gantung-kerap, bukan cipta-dan-musnah-setiap-guna.
  3. Sesetengah pepijat hanya boleh dicapai melalui pintu perkakasan sebenar dan masa sebenar. "Mati selepas kira-kira enam sesi" dan "menjadi senyap apabila anda bertukar tab" tidak boleh ditangkap oleh satu ujian unit yang mengolok peranti audio — dan pasukan itu mengatakannya secara terus terang dan bukan menutupinya dengan satu ujian yang lulus palsu. Namakan had-had harnes ujian anda; satu pepijat yang memerlukan lapan sesi sebenar untuk muncul berhak mendapat satu "disahkan secara manual" yang jujur, bukan satu tanda semak hijau yang tidak bermakna apa-apa.

Untuk bab sebelumnya, membezakan panggilan daripada aplikasi yang terbuka (v1.8.15–v1.8.19); dan untuk keseluruhan lengkungnya, anatomi mengeluarkan perisian hingga ke kesempurnaan.

Artikel Berkaitan

Kesenyapan Itu Menanggung Beban
Steven
Steven7 minit bacaan

Kesenyapan Itu Menanggung Beban

Dua keluaran terakhir GeekBye v1 membicarakan kebenaran tidak selesa yang sama: transkripsi masa nyata di atas rangkaian sebenar tidaklah tanpa kehilangan, dan langkah jujurnya ialah berhenti berpura-pura seolah-olah begitu. v1.8.20 menyimpan satu salinan setiap chunk audio ke cakera sebelum membuangnya semasa satu reconnect, dan mula menandakan jurang-jurang dalam transkrip secara terbuka. v1.9.0 berhenti menghantar kesenyapan demi menjimatkan lebar jalur — dan mendapati bahawa kesenyapan itu ialah isyarat tepat yang digunakan transkriber untuk tahu bahawa satu ayat telah tamat. Dua keluaran tentang kos membuang sesuatu.

Kejuruteraan
Audio
Kebolehpercayaan
Membezakan Panggilan daripada Aplikasi yang Terbuka
Steven
Steven8 minit bacaan

Membezakan Panggilan daripada Aplikasi yang Terbuka

GeekBye boleh perasan bahawa anda telah menyertai satu mesyuarat video dan menawarkan untuk merakamnya. Pengesanan ternyata separuh yang mudah — satu binari Swift yang membaca tajuk tetingkap setiap sepuluh saat. Separuh yang sukar ialah kepersisan: tidak mencetus apabila Zoom hanya terbuka, tidak menawarkan satu mesyuarat yang sudah anda rakam, dan tidak membisukan mikrofon dalam panggilan yang sebenarnya anda sertai. Tiga keluaran, dan setiap satu ialah satu pengawal yang terpaksa belajar untuk tidak mengalahkan dirinya sendiri.

Kejuruteraan
macOS
Desktop
Mengeluarkan Backend daripada Laluan Muat Naik
Steven
Steven8 minit bacaan

Mengeluarkan Backend daripada Laluan Muat Naik

GeekBye merakam skrin anda dan menyimpan videonya ke Google Drive anda. Versi pertama menghantar setiap rakaman melalui pelayan milik GeekBye sendiri dalam perjalanannya; satu keluaran kemudian, fail itu pergi terus daripada mesin anda ke Drive, dan backend diturunkan taraf kepada hanya memegang satu penunjuk. Bahagian yang menarik ialah betapa sedikit kod yang sebenarnya dikandung versi 'terus, boleh sambung semula' itu — kerana keupayaan-menyambung datang daripada memadam satu proxy, bukan daripada menulis satu.

Kejuruteraan
Seni Bina
Desktop