Steven
Steven8 dk okuma

Web Audio'yu Hayatta Tutan Üç Fiil

İki ay arayla ve iki farklı dosyada, iki GeekBye nokta sürümü, ses kodumuza aynı dersi zıt uçlardan öğretti: tarayıcının AudioContext'ini tek kullanımlıkmış gibi ele almayı bırak. Bir sürüm, macOS'un kayıt ortasında sessizce askıya aldığı bir bağlamı resume() etmeyi öğrendi; diğeri, ardışık oturumların Chromium'un yaklaşık altı bağlamlık tavanına toslamayı bırakması için close() yerine suspend() yapmayı öğrendi. Resume, suspend, close — bütün olay örgüsü bu.

Mühendislik
Audio
Desktop
GeekBye Releases
Web Audio'yu Hayatta Tutan Üç Fiil

Çoğu zaman, bir Web Audio AudioContext sıradan bir nesne gibi davranır: bir tane oluşturur, birkaç düğüm bağlar, ve onu unutursun. İki GeekBye sürümü, o varsayım kırıldığında olan şeydir — iki kez, uygulamanın iki farklı parçasında, iki ay arayla. İkisi de diğerinin devamı olarak planlanmamıştı. Ama onları yan yana koy ve tek bir hikâye anlatırlar: tarayıcının AudioContext'i tek kullanımlık bir nesne değildir. Gerçek bir durum makinesine sahip, kıt, işletim sistemine ait bir kaynaktır, ve o durum makinesinin üç fiili — resume, suspend, close — her iki sürümün de bütün olay örgüsüdür.

GeekBye'in iki ayrı ses işlem hattı vardır, işte bu yüzden aynı ders iki kez öğrenilmek zorunda kaldı. Biri video-kayıt yoludur (ScreenRecordingService), mikrofon ve sistem sesini kaydedilen bir video dosyasına karıştırmak için bir Web Audio grafiği kurar. Diğeri gerçek-zamanlı transkripsiyon yoludur (RealtimeAudioCaptureService), konuşmadan-metne beslemek için kendi grafiğini kurar. Her birinin kendi AudioContext'i vardır. Aşağıdaki her sürüm bunlardan birini düzeltir.

v1.8.16: video sustu, ama transkript susmadı

İlk hata bir bölünmüş-beyin. Bir toplantı kaydediyorsun, bir anlığına başka bir uygulamaya sekme değiştiriyorsun, ve ince bir şey oluyor: video dosyası mikrofonunu kaybediyor ama canlı transkript hiçbir şey değişmemiş gibi kaymaya devam ediyor. Transkript konuştuğuna yemin ediyor. Video sessiz. Bu, insanı çıldırtan bir tür hata, çünkü hemfikir olması gereken iki şey — "mikrofon seni duydu mu" — kesinkes anlaşamıyor.

Onu düzelten commit, nedeni tam olarak adlandırıyor:

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.

İşte mekanizması. ScreenRecordingService.setupMonoMixingPipeline() bir AudioContext, videonun ses parçası hâline gelen bir MediaStreamDestination, susturma için bir GainNode, ve VAD/STT mantığına ses kareleri pompalayan bir çift ScriptProcessorNode (via createScriptProcessor) kurar. macOS bağlamı askıya aldığında — ki bunu bir ekran kaydı sırasında uygulama odağını kaybettiğinde yapabilir — grafiğin ses donanım saatiyle sürülen parçaları durur: MediaStreamDestination mikrofon sesi almayı keser, dolayısıyla kaydedilen video sessizleşir. Ama bir ScriptProcessorNode'un onaudioprocess geri çağırması JavaScript ana iş parçacığında çalışır, ve o iş parçacığı tıklamaya devam eder. Böylece STT tarafı elindeki son veriden transkript girişleri üretmeye devam eder, ve aynı mikrofonun iki tüketicisi birbirinden ayrışır.

Düzeltme, askıya almayı fark etmek ve geri almaktır:

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

Bütün doğruluk değişikliği bu: bağlamın state'ini izle, ve hâlâ kaydetmemiz gerekiyorken suspended'a düşerse, resume() çağır. Sürüm ayrıca mikrofonun MediaStreamTrack'ine onmute, onunmute, ve onended dinleyicilerini bağlar — davranışı değiştirmek için değil, işletim sistemi düzeyindeki bir susturma veya bir cihaz kesintisi, sessizliğin görünmez bir nedeni olmak yerine kayıtlarda görünsün diye. Bir hatanın bütün zorluğu "ses neden durdu" olduğunda, sesin durmasını gözlemlenebilir kılmak düzeltmenin yarısıdır.

Bir takip commit'i, o yeni dinleyicilerin ardını toplamak için birkaç dakika sonra geldi: cleanup() artık grafiği sökmeden önce micTrack.onmute/onunmute/onended'ı null yapıyor ve audioContext.onstatechange = null ayarlıyor. Commit notu, bunun "matching RealtimeAudioCaptureService cleanup pattern" olduğunu söylüyor — uygulamanın diğer ses servisi. Bu ayrıntıyı aklında tut, çünkü iki ay sonra tam olarak o kalıp, bir sonraki sürümün yeniden yazmak zorunda kaldığı şeydir.

v2.0.2: yaklaşık altı oturumdan sonra ses ve transkriptler ölüyor

İkinci hata diğer işlem hattında, RealtimeAudioCaptureService'te yaşıyor, ve harika bir şekli var: her şey kusursuz çalışıyor, ve sonra tek bir uygulama oturumunda bir avuç kez kaydı başlatıp durdurduktan sonra, ses öylece... ölüyor. Önce sistem sesi susuyor, sonra transkript duruyor. Uygulamayı yeniden başlat ve yine düzeliyor — bir avuç oturum daha geçene kadar.

Kök-neden notu tamamıyla okunmaya değer, çünkü ders kitabı kıvamında bir hata-ardındaki-hata:

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

Tuzak, eski kodun "sorumlu" olan şeyi yapıyor olmasıydı. Her start() taze bir AudioContext oluşturuyordu; her cleanup() ona close() çağırıyordu. İşin bittiğinde bir kaynağı kapatmak, tam olarak yapman öğretilen şeydir. Bu çalkantıyı gerekçelendiren bir satır içi yorum bile vardı — taze bağlam başına AudioWorklet modülünü yeniden yüklemenin sorun olmadığı, çünkü "a fresh context per start() means we always pay this cost once."

Ama Chromium tek bir sayfanın kaç donanım destekli AudioContext tutabileceğini sınırlar — kabaca altı — ve, can alıcı biçimde, close() onu çağırdığın anda yuvayı geri vermez. Kapatılmış bir bağlam, çöp toplayıcı tarafından toplanana kadar yuvasını işgal etmeye devam eder, ki bu JS motoru ne zaman fırsat bulursa o zaman olur. Yani titiz bir her-seferinde-kapat politikası, seni tavana tam olarak yürüten şeydir: altı oluştur/kapat döngüsü sonra, altı yuvanın hepsi henüz-toplanmamış cesetler tarafından tutulur, yedinci new AudioContext() ölü döner, ve ona bağladığın her düğüm sessizce hiçbir şey yapmaz.

Düzeltme, bağlamı tek kullanımlıkmış gibi ele almayı bırakmaktır. Onu bir kez oluştur ve servisin ömrü boyunca yeniden kullan:

  • setupMonoMixingPipeline'da, yalnızca henüz yoksa veya var olan closed ise bir new AudioContext(...) oluştur. Varsa ama suspended ise, yenisini yapmak yerine await audioContext.resume().
  • cleanup'ta, audioContext.close()'u audioContext.suspend() ile değiştir (zaten kapatılmış bir bağlamda asla çağrılmayacak şekilde korumalı). Bağlam oturumu atlatır.
  • Bağlam hayatta kaldığı için, yüklenen AudioWorklet modülü de onunla hayatta kalır — kod artık temizlemede "worklet loaded" bayrağını sıfırlamaz, dolayısıyla audioWorklet.addModule(...) her oturum yerine bütün servis ömrü için bir kez ödenir.

Oturum başına parçalar — mikrofon ve sistem sesi için createMediaStreamSource düğümleri, karıştırıcı worklet, ve Chromium'un sistem-sesi geri döngü musluğunu canlı tutan sessiz geçiş — hâlâ her döngüde kurulur ve sökülür. Yalnızca AudioContext'in kendisi kalıcıdır, dolayısıyla herhangi bir anda en fazla bir tane vardır. Tavana ulaşılamaz çünkü ona karşı asla bağlam yığmazsın.

Commit'in kendisi hakkında yaptığı dürüst bir not: bu elle doğrulandı — "manually verified: 8+ back-to-back sessions keep system audio audible and transcripts flowing (was dead at session 7)" — ve yazar, Web Audio yaşam döngüsünün "isn't unit-testable in jsdom without a large new mock harness." konusunda açık sözlü. Bu, doğru türden bir dürüstlük. Arıza yalnızca gerçek bir ses cihazına karşı, gerçek oturumlar boyunca ortaya çıkar; yeşil bir birim takımı hiçbir şey kanıtlamazdı. "Kabaca altı" ve "7. oturumda ölü", ekibin Chromium'un sınırının nerede ısırdığına dair gözlemleridir, belgelenmiş bir sabit değil — kodun kendisi bir tilde ile ihtiyatlı davranır, ve onu tekrarlayan herkes de öyle yapmalı.

Resume, suspend, close

İki sürümü yan yana diz ve simetri neredeyse fazla düzgün. İkisi de aynı tür nesne üzerinde aynı üç yöntemle ilgilidir, zıt yönlerden yaklaşılmış:

  • v1.8.16 resume() yapmayı öğrendi. Bağlam, işletim sistemi tarafından ayağının altından suspended edildi, ve düzeltme bunu yakalayıp devam ettirmek — çalışması gereken bir bağlamı çalışır tut.
  • v2.0.2 close() yerine suspend() yapmayı öğrendi. Eski kod bağlamı yok etmeye fazla hevesliydi, ve düzeltme onu askıya alıp yeniden kullanmak — yeniden oluşturması pahalı bir bağlamı canlı tut.

Bunlar tek bir ilkenin iki yarısıdır: AudioContext'i bir kullan-at gibi ele almayı bırak. Bir yarı diyor ki olmaması gerekirken ölmesine izin verme; diğeri diyor ki mecbur değilken onu öldürme. Ve zaman çizelgesinde küçük bir ironi var — v1.8.16'nın temizliği "match RealtimeAudioCaptureService's cleanup pattern," için yazılmıştı, ve sonra v2.0.2 tam olarak o kalıbı RealtimeAudioCaptureService'te yeniden yazdı, çünkü kalıbın close()-her-seferinde alışkanlığı hatanın ta kendisi çıktı. İki servis, birbirinin ses-yaşam-döngüsü işlemesinden öğrenmeye devam ediyor, her seferinde bir düzeltme.

Bu sürümlerin bize öğrettiği üç şey

  1. Bir AudioContext'in bir durum makinesi vardır — state'i yük taşıyan olarak ele al. running, suspended, ve closed önemsiz ayrıntılar değildir. İşletim sistemi bir bağlamı sana sormadan bunların arasında taşıyabilir, ve grafiğinin farklı parçaları farklı tepki verir (ana iş parçacığındaki bir ScriptProcessorNode devam eder; donanım-saatli hedef etmez). onstatechange'i izlemezsen, bağlamın onu bıraktığın yerde kaldığına güveniyorsundur — ve gerçek bir masaüstünde, odak değişimleri ve ekran kaydı altında, kalmayacaktır.
  2. close(), free() değildir. Bir kaynağı hemen kapatmanın her zaman doğru olduğu sezgisi, ikinci hataya tam olarak neden olan şeydir. Kaynak, yalnızca GC zamanında geri alınan sert, tarayıcı dayatmalı bir havuzdaki bir yuva olduğunda, agresif kapatma havuzu tüketme biçimindir. Bazen doğru yaşam döngüsü bir-kez-oluştur-sık-askıya-al'dır, oluştur-ve-kullanım-başına-yok-et değil.
  3. Bazı hatalara yalnızca gerçek donanım ve gerçek zaman kapısından ulaşılabilir. "Yaklaşık altı oturumdan sonra ölür" ve "sekme değiştirince susar", ses cihazını taklit eden bir birim testiyle yakalanamaz — ve ekip bunu sahte-geçen bir testle örtbas etmek yerine açıkça söyledi. Test donanımının sınırlarını adlandır; ortaya çıkmak için sekiz gerçek oturuma ihtiyaç duyan bir hata, hiçbir şey ifade etmeyen yeşil bir onay işareti değil, dürüst bir "elle doğrulandı" hak eder.

v1 hikâyesinin bir önceki bölümü için, bir görüşmeyi açık bir uygulamadan ayırt etmek (v1.8.15–v1.8.19); ve tüm yay için, yazılımı kusursuzluğa kadar sevk etmenin anatomisi.

İlgili Makaleler

Sessizlik Yük Taşıyordu
Steven
Steven6 dk okuma

Sessizlik Yük Taşıyordu

GeekBye v1'in son iki sürümü aynı rahatsız edici gerçekle ilgili: gerçek bir ağ üzerinden gerçek-zamanlı transkripsiyon kayıpsız değildir, ve dürüst hamle öyleymiş gibi yapmayı bırakmaktır. v1.8.20, her ses parçasının bir kopyasını, bir yeniden bağlanma sırasında atmadan önce diske sakladı, ve transkriptteki boşlukları yüksek sesle işaretlemeye başladı. v1.9.0, bant genişliğinden tasarruf etmek için sessizlik göndermeyi bıraktı — ve sessizliğin, transkripsiyoncunun bir cümlenin bittiğini anlamak için kullandığı tam da o sinyal olduğunu keşfetti. Bir şeyleri atmanın bedeli üzerine iki sürüm.

Mühendislik
Audio
Güvenilirlik
Bir Görüşmeyi Açık Bir Uygulamadan Ayırt Etmek
Steven
Steven7 dk okuma

Bir Görüşmeyi Açık Bir Uygulamadan Ayırt Etmek

GeekBye bir video görüşmesine katıldığınızı fark edip onu kaydetmeyi teklif edebilir. Tespit, kolay yarı olarak çıkıyor — her on saniyede pencere başlıklarını okuyan bir Swift ikili dosyası. Zor yarı kesinliktir: Zoom sadece açıkken tetiklenmemek, zaten kaydettiğiniz bir görüşme için sormamak, ve gerçekten içinde bulunduğunuz görüşmede mikrofonu susturmamak. Üç sürüm, ve her biri kendini yenmemeyi öğrenmek zorunda kalan bir korumadır.

Mühendislik
macOS
Desktop
Backend'i Yükleme Yolundan Çıkarmak
Steven
Steven7 dk okuma

Backend'i Yükleme Yolundan Çıkarmak

GeekBye ekranınızı kaydeder ve videoyu Google Drive'ınıza kaydeder. İlk sürüm her kaydı oraya giderken GeekBye'in kendi sunucuları üzerinden geçirdi; bir sürüm sonra, dosya makinenizden doğrudan Drive'a gitti, ve backend tek bir işaretçiyi tutmaya indirgendi. İlginç kısım, 'doğrudan, devam ettirilebilir' sürümün aslında ne kadar az kod içerdiğidir — çünkü devam ettirilebilirlik bir proxy yazmaktan değil, bir proxy'yi silmekten geldi.

Mühendislik
Mimari
Desktop