Ses iş uygulaması tasarım deseni

Hongchan Choi

Ses iş parçacığı ile ilgili önceki makalede temel kavramlar ve kullanım ayrıntılı olarak açıklanmıştır. Chrome 66'da kullanıma sunulmasından bu yana, gerçek uygulamalarda nasıl kullanılabileceğine dair daha fazla örnek talep edildi. Ses işleyici, WebAudio'nun tüm potansiyelini ortaya çıkarır ancak çeşitli JS API'leriyle sarmalanmış eşzamanlı programlamayı anlamanızı gerektirdiği için bu avantajdan yararlanmak zor olabilir. WebAudio'ye aşina olan geliştiriciler için bile ses iş parçacığını diğer API'lerle (ör. WebAssembly) entegre etmek zor olabilir.

Bu makale, okuyucuya ses işleyicinin gerçek dünyada nasıl kullanılacağını daha iyi anlama ve bu aracın gücünden en iyi şekilde yararlanma konusunda ipuçları sunma fırsatı sunar. Kod örneklerine ve canlı demolara da göz atın.

Özet: Ses İş Aracı

Konuyu incelemeye başlamadan önce, daha önce bu yayında tanıtılan ses iş parçacığı sistemiyle ilgili terimleri ve bilgileri kısaca özetleyelim.

  • BaseAudioContext: Web Audio API'nin birincil nesnesi.
  • Ses Workleti: Ses Workleti işlemi için özel bir komut dosyası yükleyici. BaseAudioContext'e aittir. BaseAudioContext'te bir Audio Worklet olabilir. Yüklenen komut dosyası, AudioWorkletGlobalScope içinde değerlendirilir ve AudioWorkletProcessor örneklerini oluşturmak için kullanılır.
  • AudioWorkletGlobalScope: Audio Worklet işlemi için özel bir JS global kapsamı. WebAudio için özel bir oluşturma iş parçacığında çalışır. BaseAudioContext bir AudioWorkletGlobalScope'ya sahip olabilir.
  • AudioWorkletNode: Ses işleyici işlemi için tasarlanmış bir AudioNode. BaseAudioContext'ten oluşturulur. BaseAudioContext, yerel AudioNodes'a benzer şekilde birden fazla AudioWorkletNode'a sahip olabilir.
  • AudioWorkletProcessor: AudioWorkletNode'un karşılığıdır. Kullanıcı tarafından sağlanan kodla ses akışını işleyen AudioWorkletNode'un asıl çekirdeği. Bir AudioWorkletNode oluşturulduğunda AudioWorkletGlobalScope içinde örneklenir. Bir AudioWorkletNode'un eşleşen bir AudioWorkletProcessor'u olabilir.

Tasarım Kalıpları

Ses iş parçacığını WebAssembly ile kullanma

WebAssembly, AudioWorkletProcessor için mükemmel bir tamamlayıcıdır. Bu iki özelliğin birlikte kullanılması, web'de ses işlemenin çeşitli avantajlarını sağlar. Ancak en büyük iki avantaj şunlardır: a) Mevcut C/C++ ses işleme kodunu WebAudio ekosistemine getirmek ve b) ses işleme kodunda JS JIT derlemenin ve çöp toplamanın ek yükünden kaçınmak.

İlki, ses işleme koduna ve kitaplıklarına yatırım yapmış geliştiriciler için önemlidir ancak ikincisi, API'nin neredeyse tüm kullanıcıları için kritiktir. WebAudio dünyasında, kararlı ses akışı için zamanlama bütçesi oldukça yüksektir: 44,1 KHz örnek hızında yalnızca 3 ms'dir. Ses işleme kodunda küçük bir aksaklık bile aksamalara neden olabilir. Geliştirici, kodu daha hızlı işleme için optimize etmenin yanı sıra oluşturulan JS çöp miktarını da en aza indirmelidir. WebAssembly'i kullanmak, her iki sorunu da aynı anda ele alan bir çözüm olabilir: Daha hızlıdır ve koddan gereksiz veri oluşturmaz.

Sonraki bölümde, WebAssembly'nin bir ses işleyiciyle nasıl kullanılabileceği açıklanmaktadır. İlgili kod örneğini burada bulabilirsiniz. Emscripten ve WebAssembly'in (özellikle Emscripten yapıştırma kodu) nasıl kullanılacağıyla ilgili temel eğitim için lütfen bu makaleye göz atın.

Kurulum

Bu plan harika görünüyor ancak her şeyi düzgün bir şekilde ayarlamak için biraz yapıya ihtiyacımız var. Sorulması gereken ilk tasarım sorusu, bir WebAssembly modülünün nasıl ve nerede oluşturulacağıdır. Emscripten'in yapıştırma kodu getirildikten sonra modülün oluşturulması için iki yol vardır:

  1. Birleştirme kodunu audioContext.audioWorklet.addModule() aracılığıyla AudioWorkletGlobalScope'a yükleyerek bir WebAssembly modülü oluşturun.
  2. Ana kapsamda bir WebAssembly modülü oluşturun, ardından modülü AudioWorkletNode'un kurucu seçenekleri aracılığıyla aktarın.

Karar büyük ölçüde tasarımınıza ve tercihinize bağlıdır. Ancak WebAssembly modülünün, AudioWorkletGlobalScope içinde bir WebAssembly örneği oluşturabileceği ve bu örneğin bir AudioWorkletProcessor örneği içinde ses işleme çekirdeği haline geleceği varsayılır.

WebAssembly modülü örneklendirme kalıbı A: .addModule() çağrısını kullanma
WebAssembly modülü başlatma kalıbı A: .addModule() çağrısını kullanarak

A kalıbının düzgün çalışması için Emscripten'in, yapılandırmamız için doğru WebAssembly yapıştırma kodunu oluşturması gerekir:

-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js

Bu seçenekler, AudioWorkletGlobalScope'da bir WebAssembly modülünün senkronize derlenmesini sağlar. Ayrıca, modül başlatıldıktan sonra yüklenebilmesi için AudioWorkletProcessor sınıf tanımını mycode.js içine ekler. Senkronize derlemeyi kullanmanın birincil nedeni, audioWorklet.addModule()'ün söz verme çözümünün AudioWorkletGlobalScope'daki söz verme çözümünü beklememesidir. Ana iş parçacığında senkronize yükleme veya derleme, aynı iş parçacığındaki diğer görevleri engellediği için genellikle önerilmez. Ancak burada derleme, ana iş parçacığında çalışan AudioWorkletGlobalScope'ta gerçekleştiği için kuralı atlayabiliriz. (Daha fazla bilgi için bu makaleyi inceleyin.)

WASM modülü oluşturma kalıbı B: AudioWorkletNode yapıcısının iş parçacığı arası aktarımını kullanma
WASM modülü başlatma kalıbı B: AudioWorkletNode yapıcısının iş parçacığı arası aktarımını kullanma

Ağır iş yükü gerektiren ve eşzamanlı olmayan işlemler yapılması gerekiyorsa B kalıbı yararlı olabilir. Ana ileti dizisi, sunucudan yapıştırma kodunu almak ve modülü derlemek için kullanılır. Ardından, WASM modülünü AudioWorkletNode'un kurucusu aracılığıyla aktarır. Bu kalıp, AudioWorkletGlobalScope ses akışını oluşturmaya başladıktan sonra modülü dinamik olarak yüklemeniz gerektiğinde daha da anlamlı hale gelir. Modülün boyutuna bağlı olarak, oluşturma işleminin ortasında derlenmesi yayında aksaklıklara neden olabilir.

WASM Yığını ve Ses Verileri

WebAssembly kodu yalnızca özel bir WASM yığınında ayrılan bellekte çalışır. Bu avantajdan yararlanmak için ses verilerinin WASM yığını ile ses verisi dizileri arasında gidip gelerek klonlanması gerekir. Örnek koddaki HeapAudioBuffer sınıfı bu işlemi iyi bir şekilde yönetir.

WASM yığınının daha kolay kullanımı için HeapAudioBuffer sınıfı
WASM yığınının daha kolay kullanımı için HeapAudioBuffer sınıfı

WASM yığınını doğrudan Audio Worklet sistemine entegre etmek için üzerinde çalışılan bir erken teklif var. JS belleği ile WASM yığını arasında bu gereksiz veri kopyalama işleminden kurtulmak doğal bir çözüm gibi görünse de belirli ayrıntıların üzerinde çalışılması gerekiyor.

Arabellek Boyutu Uyuşmazlığını Yönetme

AudioWorkletNode ve AudioWorkletProcessor çifti, normal bir AudioNode gibi çalışacak şekilde tasarlanmıştır. AudioWorkletNode, diğer kodlarla etkileşimi yönetirken AudioWorkletProcessor, dahili ses işlemeyi yönetir. Normal bir AudioNode bir seferde 128 kare işlediği için AudioWorkletProcessor'ın temel bir özellik haline gelmesi için aynısını yapması gerekir. Bu, AudioWorkletProcessor'da dahili arabelleğe alma nedeniyle ek gecikme yaşanmamasını sağlayan Audio Worklet tasarımının avantajlarından biridir ancak bir işleme işlevi 128 kareden farklı bir arabelleğe alma boyutu gerektiriyorsa sorun olabilir. Bu tür durumlar için yaygın çözüm, dairesel tampon veya FIFO olarak da bilinen bir halka tampon kullanmaktır.

512 kare giriş ve çıkış alan bir WASM işlevini barındırmak için içinde iki halka arabelleği kullanan AudioWorkletProcessor şemasını burada bulabilirsiniz. (Burada 512 numarası rastgele seçilmiştir.)

AudioWorkletProcessor'ın "process" yönteminde RingBuffer kullanma
AudioWorkletProcessor'ın "process()` yönteminde RingBuffer kullanma

Diyagramın algoritması şu şekilde olur:

  1. AudioWorkletProcessor, girişinden Giriş Halka Arabelleğine 128 kare gönderir.
  2. Aşağıdaki adımları yalnızca Giriş Halka Arabelleği 512 kare veya daha fazlaysa uygulayın.
    1. Giriş Halka Arabelleği'nden 512 kare alın.
    2. Belirtilen WASM işleviyle 512 kare işleyin.
    3. 512 kareyi Çıkış RingBuffer'ına gönderin.
  3. AudioWorkletProcessor, Çıkış'ını doldurmak için Çıkış Halka Arabelleği'nden 128 kare alır.

Diyagramda gösterildiği gibi, giriş kareleri her zaman giriş halka tamponunda toplanır ve tampondaki en eski kare bloğunun üzerine yazarak tampon taşmasını ele alır. Gerçek zamanlı ses uygulaması için bu makul bir işlemdir. Benzer şekilde, Çıkış çerçevesi bloğu her zaman sistem tarafından çekilir. Çıkış RingBuffer'unda arabellek alt akış (yeterli veri yok) olması, yayında aksaklıklara neden olan sessizliğe yol açar.

Bu kalıp, ScriptProcessorNode (SPN) öğesini AudioWorkletNode ile değiştirirken kullanışlıdır. SPN, geliştiricinin 256 ile 16.384 kare arasında bir arabellek boyutu seçmesine olanak tanıdığından, SPN'nin AudioWorkletNode ile doğrudan ikame edilmesi zor olabilir. Bu durumda, halka arabellek kullanmak iyi bir çözümdür. Bu tasarımın üzerine inşa edilebilecek mükemmel bir örnek ses kaydedicidir.

Ancak bu tasarımın yalnızca arabellek boyutu uyuşmazlığını giderdiğini ve belirli komut dosyası kodunun çalışması için daha fazla zaman vermediğini anlamak önemlidir. Kod, görevi oluşturma kuantumunun zamanlama bütçesi içinde (44,1 Khz'de yaklaşık 3 ms) tamamlayamazsa sonraki geri çağırma işlevinin başlangıç zamanlamasını etkiler ve sonunda aksamalara neden olur.

WASM yığınındaki bellek yönetimi nedeniyle bu tasarımı WebAssembly ile karıştırmak karmaşık olabilir. Bu makalenin yazıldığı sırada, WASM yığınına giren ve yığından çıkan veriler klonlanmalıdır ancak bellek yönetimini biraz daha kolaylaştırmak için HeapAudioBuffer sınıfından yararlanabiliriz. Gereksiz veri kopyalama işlemlerini azaltmak için kullanıcı tarafından ayrılan belleği kullanma fikri gelecekte ele alınacaktır.

RingBuffer sınıfını burada bulabilirsiniz.

WebAudio Powerhouse: Audio Worklet ve SharedArrayBuffer

Bu makaledeki son tasarım kalıbı, Audio Worklet, SharedArrayBuffer, Atomics ve Worker gibi birkaç son teknoloji API'yi tek bir yere koymaktır. Bu basit olmayan kurulumla, C/C++ ile yazılmış mevcut ses yazılımlarının sorunsuz bir kullanıcı deneyimi sunarken web tarayıcısında çalışabilmesi için bir yol açılır.

Son tasarım modeline genel bakış: Sesli Worklet, SharedArrayBuffer ve Worker
Son tasarım modeline genel bakış: Ses Workleti, SharedArrayBuffer ve işleyici

Bu tasarımın en büyük avantajı, yalnızca ses işleme için DedicatedWorkerGlobalScope kullanabilmektir. Chrome'da WorkerGlobalScope, WebAudio oluşturma iş parçacığına kıyasla daha düşük öncelikli bir iş parçacığında çalışır ancak AudioWorkletGlobalScope'ya kıyasla çeşitli avantajları vardır. DedicatedWorkerGlobalScope, kapsamda kullanılabilen API yüzeyi açısından daha az kısıtlıdır. Ayrıca Worker API birkaç yıldır mevcut olduğundan Emscripten'den daha iyi destek alabilirsiniz.

SharedArrayBuffer, bu tasarımın verimli bir şekilde çalışması için kritik bir rol oynar. Hem Worker hem de AudioWorkletProcessor, asenkron mesajlaşma (MessagePort) ile donatılmış olsa da tekrarlanan hafıza tahsisi ve mesajlaşma gecikmesi nedeniyle gerçek zamanlı ses işleme için en uygun seçenek değildir. Bu nedenle, hızlı iki yönlü veri aktarımı için her iki iş parçacığında da erişilebilecek bir bellek bloğu önceden ayırırız.

Web Audio API'yi katı bir şekilde kullananların bakış açısından bu tasarım, Audio Worklet'i basit bir "ses kaynağı" olarak kullandığı ve her şeyi Worker'da yaptığı için en uygun tasarım gibi görünmeyebilir. Ancak C/C++ projelerinin JavaScript'te yeniden yazılmasının maliyeti çok yüksek olabilir veya hatta imkansız olabilir. Bu nedenle, bu tür projeler için en verimli uygulama yolu bu tasarım olabilir.

Paylaşılan Durumlar ve Atomlar

Ses verileri için paylaşılan bellek kullanıldığında her iki taraftan da erişim dikkatlice koordine edilmelidir. Atomik olarak erişilebilir durumları paylaşma bu tür bir soruna çözümdür. Bu amaçla, bir SAB tarafından desteklenen Int32Array'ten yararlanabiliriz.

Senkronizasyon mekanizması: SharedArrayBuffer ve Atomics
Senkronizasyon mekanizması: SharedArrayBuffer ve Atomics

Senkronizasyon mekanizması: SharedArrayBuffer ve Atomics

States dizisinin her alanı, paylaşılan arabelleklerle ilgili önemli bilgileri temsil eder. Bunlardan en önemlisi senkronizasyon için kullanılan bir alandır (REQUEST_RENDER). Buradaki amaç, Worker'ın bu alana AudioWorkletProcessor tarafından dokunulmasını beklemesi ve uyandığı zaman sesi işlemektir. Atomics API, SharedArrayBuffer (SAB) ile birlikte bu mekanizmayı mümkün kılar.

İki ileti dizisinin senkronizasyonunun oldukça gevşek olduğunu unutmayın. Worker.process() yöntemi tarafından AudioWorkletProcessor.process() yöntemi tetiklenir ancak AudioWorkletProcessor, Worker.process() yönteminin tamamlanmasını beklemez. Bu durum, tasarım gereğidir. AudioWorkletProcessor, ses geri çağırması tarafından yönlendirildiğinden senkronize olarak engellenmemesi gerekir. En kötü senaryoda ses akışı kopyalanabilir veya kesinti yaşayabilir ancak oluşturma performansı sabitlendiğinde bu durum düzelecektir.

Kurulum ve Kullanıma Başlama

Yukarıdaki şemada gösterildiği gibi, bu tasarımda düzenlenmesi gereken birkaç bileşen vardır: DedicatedWorkerGlobalScope (DWGS), AudioWorkletGlobalScope (AWGS), SharedArrayBuffer ve ana iş parçacığı. Aşağıdaki adımlarda, başlatma aşamasında ne olması gerektiği açıklanmaktadır.

Başlatma
  1. [Main] AudioWorkletNode oluşturucusu çağrılıyor.
    1. Çalışan oluşturun.
    2. İlişkili AudioWorkletProcessor oluşturulur.
  2. [DWGS] İşçi 2 SharedArrayBuffer oluşturur. (biri paylaşılan durumlar, diğeri ses verileri için)
  3. [DWGS] İşleyici, AudioWorkletNode'a SharedArrayBuffer referansları gönderiyor.
  4. [Main] AudioWorkletNode, SharedArrayBuffer referanslarını AudioWorkletProcessor'a gönderir.
  5. [AWGS] AudioWorkletProcessor, AudioWorkletNode'a kurulumun tamamlandığını bildirir.

İlk kullanıma hazırlama işlemi tamamlandıktan sonra AudioWorkletProcessor.process() çağrılmaya başlar. Oluşturma döngüsünün her iterasyonunda aşağıdakiler gerçekleşmelidir.

Oluşturma Döngüsü
SharedArrayBuffers ile çok iş parçacıklı oluşturma
Çok iş parçacıklı oluşturma (SharedArrayBuffers ile)
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs) her oluşturma kuantumu için çağrılır.
    1. inputs, Giriş SAB'a gönderilir.
    2. outputs, Çıkış SAB'ında ses verileri kullanılarak doldurulur.
    3. States SAB'ı yeni arabellek dizeleriyle uygun şekilde günceller.
    4. Çıkış SAB'si alt akış eşiğine yaklaşırsa daha fazla ses verisi oluşturmak için Wake Worker'ı uyandırın.
  2. [DWGS] Çalışan, AudioWorkletProcessor.process()'ten gelen uyanma sinyalini bekler (uyur). Uyandığında:
    1. Eyalet SAB'sinden arabelleği dizinlerini getirir.
    2. Çıkış SAB'ını doldurmak için işlem işlevini Giriş SAB'ındaki verilerle çalıştırın.
    3. States SAB'ı uygun şekilde arabellek dizinleriyle günceller.
    4. Uyku moduna geçer ve bir sonraki sinyali bekler.

Örnek kodu burada bulabilirsiniz ancak bu demonun çalışması için SharedArrayBuffer deneysel işaretinin etkinleştirilmesi gerektiğini unutmayın. Kod, basitlik için saf JS koduyla yazılmıştır ancak gerekirse WebAssembly koduyla değiştirilebilir. Bu tür durumlar, bellek yönetimi HeapAudioBuffer sınıfıyla sarmalanarak ekstra dikkatle ele alınmalıdır.

Sonuç

Ses işleyicinin nihai hedefi, Web Audio API'yi gerçekten "genişletilebilir" hale getirmektir. Web Audio API'nin geri kalanını Audio Worklet ile uygulamayı mümkün kılmak için tasarımında çok yıllık bir çalışma yapıldı. Bu da tasarımın daha karmaşık hale gelmesine neden oluyor. Bu da beklenmedik bir zorluk olabilir.

Neyse ki bu karmaşıklığın nedeni, geliştiricilere güç vermektir. WebAssembly'i AudioWorkletGlobalScope üzerinde çalıştırabilmek, web'de yüksek performanslı ses işleme için büyük bir potansiyelin kilidini açar. C veya C++ ile yazılmış büyük ölçekli ses uygulamaları için SharedArrayBuffers ve çalışanlarla birlikte bir ses işleyicisi kullanmak ilgi çekici bir seçenek olabilir.

Kredi

Bu makalenin taslağını inceleyip değerli geri bildirimler veren Chris Wilson, Jason Miller, Joshua Bell ve Raymond Toy'a özel teşekkürler.