Ses iş uygulaması tasarım deseni

Hongchan Choi

Ses İş Akışı ile ilgili bir önceki makalede temel kavramlar ve kullanım ayrıntıları açıklandı. Chrome 66'da kullanıma sunulmasından bu yana, gerçek uygulamalarda nasıl kullanılabileceğine dair daha fazla örnek için çok sayıda istek alındı. Audio Worklet, WebAudio'nun tam potansiyelini ortaya çıkarır ancak birkaç JS API'si ile sarmalanmış eş zamanlı programlamayı anlamayı gerektirdiğinden bunun avantajlarından yararlanmak zor olabilir. WebAudio'ya aşina olan geliştiriciler için bile, Ses İş Akışını diğer API'lerle (ör. WebAssembly) entegre etmek zor olabilir.

Bu makale, okuyucunun Ses İş Akışı'nı gerçek dünyada nasıl kullanacağını daha iyi anlamasını ve en iyi şekilde yararlanma konusunda ipuçları vermesini sağlar. Kod örneklerine ve canlı demolara da göz atmayı unutmayın.

Özet: Ses İş Akışı

Başlamadan önce, bu gönderide anlatılan Ses İş Akışı sistemiyle ilgili terimleri ve bilgileri hızlı bir şekilde özetleyelim.

  • BaseAudioContext: Web Audio API'sının birincil nesnesi.
  • Ses İş Akışı: Ses İş Akışı işlemi için özel bir komut dosyası dosyası yükleyicidir. BaseAudioContext'e aittir. BaseAudioContext'in bir Ses İş Akışı olabilir. Yüklenen komut dosyası, AudioWorkletGlobalScope'ta değerlendirilir ve AudioWorkletProcessor örneklerini oluşturmak için kullanılır.
  • AudioWorkletGlobalScope : Ses İş Akışı işlemi için özel bir JS global kapsamı. WebAudio için özel bir oluşturma iş parçacığında çalıştırılır. BaseAudioContext'te bir AudioWorkletGlobalScope olabilir.
  • AudioWorkletNode: Ses İş Akışı işlemi için tasarlanmış bir AudioNode. BaseAudioContext'ten somutlaştırılır. BaseAudioContext'in, yerel AudioNode'lara benzer şekilde birden fazla AudioWorkletNode'u olabilir.
  • AudioWorkletProcessor: AudioWorkletNode'un bir muadili. Ses akışını kullanıcı tarafından sağlanan koda göre işleyen AudioWorkletNode'un gerçek içgüdüleri. Bir AudioWorkletNode oluşturulduğunda AudioWorkletGlobalScope'ta somutlaştırılır. AudioWorkletNode'da eşleşen bir AudioWorkletProcessor olabilir.

Tasarım Kalıpları

Ses İş Akışını WebAssembly ile kullanma

WebAssembly, AudioWorkletProcessor için mükemmel bir tamamlayıcıdır. Bu iki özelliğin kombinasyonu, web'de ses işlemeye çeşitli avantajlar sunar ancak en önemli iki avantaj şunlardır: a) mevcut C/C++ ses işleme kodunu WebAudio ekosistemine taşıma ve b) JS JIT derlemesi ve ses işleme kodunda çöp toplama işlemlerinin yapılmasından kaçınma.

Ses işleme kodu ve kitaplıklarına halihazırda yatırım yapan geliştiriciler için birinci seçenek önemlidir. Ancak ikinci yöntem, API'nin neredeyse tüm kullanıcıları için kritik öneme sahiptir. WebAudio dünyasında, kararlı ses akışı için zamanlama bütçesi çok zorludur: 44,1 Khz örnek hızında yalnızca 3 ms'dir. Ses işleme kodundaki küçük bir kesinti bile hatalara neden olabilir. Geliştiricinin, hem kodu daha hızlı işlemek için optimize etmesi hem de oluşturulan JS çöpü miktarını en aza indirmesi gerekir. WebAssembly'yi kullanmak her iki sorunu aynı anda ele alan bir çözüm olabilir: Daha hızlıdır ve kodla atık üretmez.

Sonraki bölümde WebAssembly'nin Ses İş Akışı ile nasıl kullanılabileceği açıklanmaktadır. Eşlik eden kod örneğini burada bulabilirsiniz. Emscripten ve WebAssembly (özellikle Emscripten yapıştırıcı kodu) kullanımıyla ilgili temel eğitim için lütfen bu makaleye göz atın.

Kurulum

Kulağa hoş geliyor, ancak her şeyi düzgün bir şekilde ayarlayabilmek için biraz yapıya ihtiyacımız var. Sorulacak ilk tasarım sorusu, WebAssembly modülünün nasıl ve nerede örnekleneceğidir. Emscripten'in yapıştırıcı kodunu getirdikten sonra modül örneklendirmesi için iki yol vardır:

  1. Tutkal kodunu audioContext.audioWorklet.addModule() üzerinden AudioWorkletGlobalScope'a yükleyerek WebAssembly modülünü örnekleyin.
  2. Ana kapsamda bir WebAssembly modülünü örnekleyin, ardından AudioWorkletNode'un kurucu seçenekleri aracılığıyla modülü 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'ta bir WebAssembly örneği oluşturup AudioWorkletProcessor örneğinde bir ses işleme çekirdeği oluşturabileceğini hatırlatmak isteriz.

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

A kalıbının düzgün çalışması için Emscripten'ın, yapılandırmamıza yönelik doğru WebAssembly yapıştırıcı kodunu oluşturmak üzere birkaç seçeneğe ihtiyacı vardır:

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

Bu seçenekler, AudioWorkletGlobalScope'ta WebAssembly modülünün eşzamanlı derlenmesini sağlar. Ayrıca, modül başlatıldıktan sonra yüklenebilmesi için AudioWorkletProcessor'ın sınıf tanımını mycode.js öğesine ekler. Eşzamanlı derlemeyi kullanmanın birincil nedeni, audioWorklet.addModule() için vaat çözümlemesinin AudioWorkletGlobalScope'taki vaatlerin çözümlenmesini beklememesidir. Ana iş parçacığındaki eşzamanlı yükleme veya derleme, aynı iş parçacığındaki diğer görevleri engellediği için genellikle önerilmez. Ancak derleme, ana iş parçacığının dışında kalan AudioWorkletGlobalScope'ta gerçekleştiği için burada kuralı atlayabiliriz. (Daha fazla bilgi için buraya göz atın.)

WASM modülü örneklendirme kalıbı B: AudioWorkletNode oluşturucunun iş parçacıkları arası aktarımını kullanma
WASM modülü örneklendirme kalıbı B: AudioWorkletNode oluşturucunun iş parçacıkları arası aktarımını kullanma

Eşzamansız ağır kaldırma gerektiren durumlarda B modeli yararlı olabilir. Sunucudan yapışkan kodu getirmek ve modülü derlemek için ana iş parçacığını kullanır. Ardından, AudioWorkletNode kurucusu üzerinden WASM modülünü aktarır. AudioWorkletGlobalScope ses akışını oluşturmaya başladıktan sonra modülü dinamik olarak yüklemeniz gerektiğinde bu yöntem daha da anlamlı olur. Modülün boyutuna bağlı olarak, oluşturma işleminin ortasında derlemek akışta sorunlara neden olabilir.

WASM Yığın ve Ses Verileri

WebAssembly kodu yalnızca özel bir WASM yığınında ayrılan bellekte çalışır. Bu özellikten yararlanmak için ses verilerinin, WASM yığını ile ses veri dizileri arasında ileri geri klonlanması gerekir. Örnek koddaki HeapAudioBuffer sınıfı bu işlemi sorunsuzca işler.

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

WASM yığınının doğrudan Ses İşleti sistemine entegre edilmesiyle ilgili erken bir teklif sunulmaktadır. JS belleği ile WASM yığını arasındaki bu gereksiz veri klonlamasından kurtulmak doğal görünse de belirli ayrıntıların üzerinde çalışılması gerekir.

Arabellek Boyutu Uyuşmazlığını Ele Alma

AudioWorkletNode ve AudioWorkletProcessor çifti normal bir AudioNode gibi çalışacak şekilde tasarlanmıştır. AudioWorkletNode diğer kodlarla etkileşimi gerçekleştirirken AudioWorkletProcessor ise dahili ses işlemeyle ilgilenir. Normal bir AudioNode, aynı anda 128 kare işlediğinden AudioWorkletProcessor temel özellik haline gelmek için aynı şeyi yapmalıdır. Bu, Ses İş Akışı tasarımının, AudioWorkletProcessor içinde dahili arabelleğe alma nedeniyle ek gecikme yaşanmamasını sağlayan avantajlarından biridir. Ancak, işleme işlevi 128 kareden farklı bir arabellek boyutu gerektiriyorsa sorun olabilir. Bu tür durumların yaygın çözümü, dairesel tampon veya FIFO olarak da bilinen halka tamponu kullanmaktır.

Burada, 512 kareyi içeri ve dışarı alan bir WASM işlevine yer vermek için içeride iki halka arabelleği kullanan bir AudioWorkletProcessor şeması verilmiştir. (Buradaki 512 sayısı rastgele seçilmiştir.)

AudioWorkletProcessor'ın "process()" yöntemi içinde RingBuffer kullanma
AudioWorkletProcessor'ın "process()" yöntemi içinde RingBuffer kullanma

Diyagramın algoritması şöyle olabilir:

  1. AudioWorkletProcessor, Girişinden Giriş RingBuffer'a 128 kare aktarır.
  2. Aşağıdaki adımları yalnızca Giriş RingBuffer'ın 512 kareden büyük veya 512'ye eşitse uygulayın.
    1. Input RingBuffer'dan 512 kare çekin.
    2. Belirtilen WASM işleviyle 512 kareler işleyin.
    3. 512 kareyi Çıkış RingBuffer'a aktarın.
  3. AudioWorkletProcessor, Çıkışı doldurmak için Çıkış RingBuffer'dan 128 kare çeker.

Şemada gösterildiği gibi Giriş çerçeveleri her zaman InputRingBuffer'da toplanır ve arabellekteki en eski çerçeve bloğunun üzerine yazarak arabellek taşmasını işler. Bu, gerçek zamanlı bir ses uygulaması için makul bir şeydir. Benzer şekilde, Çıkış çerçevesi bloğu her zaman sistem tarafından çekilir. Çıkış RingBuffer'daki arabellek alt akışı (yeterli veri yok) susturularak akışta aksama meydana gelir.

Bu kalıp, ScriptProcessorNode'u (SPN) AudioWorkletNode ile değiştirirken faydalı olur. SPN, geliştiricinin 256 ile 16384 kare arasında bir arabellek boyutu seçmesine izin verdiğinden SPN'nin AudioWorkletNode ile değiştirilmesi zor olabilir ve halka arabelleği kullanmak iyi bir çözüm sağlar. Ses kaydedici, bu tasarımın üzerine inşa edilebilecek harika bir örnek.

Ancak, bu tasarımın yalnızca arabellek boyutu uyumsuzluğunu giderdiğini ve belirtilen komut dosyası kodunu çalıştırmak için daha fazla zaman sağlamadığının anlaşılması önemlidir. Kod, oluşturma kuantumunun zamanlama bütçesi (44,1 Khz'de yaklaşık 3 ms) içinde görevi tamamlayamazsa sonraki geri çağırma işlevinin başlangıç zamanlamasını etkiler ve sonuçta hatalara neden olur.

WASM yığını çevresindeki bellek yönetimi nedeniyle bu tasarımı WebAssembly ile birlikte kullanmak karmaşık olabilir. Yazma sırasında, WASM yığınına giren ve çıkan verilerin klonlanması gerekir ancak bellek yönetimini biraz kolaylaştırmak için HeapAudioBuffer sınıfını kullanabiliriz. Gereksiz veri klonlamayı azaltmak için kullanıcı tarafından ayrılmış bellek kullanma fikri ileride ele alınacaktır.

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

WebAudio Powerhouse: Ses İş Akışı ve SharedArrayBuffer

Bu makaledeki son tasarım kalıbı, birkaç son teknoloji API'yi tek bir yere yerleştirmektir: Audio Worklet, SharedArrayBuffer, Atomics ve Worker. Basit olmayan bu kurulumla, C/C++ dilinde yazılmış mevcut ses yazılımlarının sorunsuz bir kullanıcı deneyimi sağlarken bir web tarayıcısında çalışması için bir yol açmaktadır.

Son tasarım kalıbına genel bakış: Ses İş Akışı, SharedArrayBuffer ve Çalışan
Son tasarım kalıbına genel bakış: Ses İş Akışı, SharedArrayBuffer ve Çalışan

Bu tasarımın en büyük avantajı, ses işleme için yalnızca bir DedicatedWorkerGlobalScope oluşturabilmesidir. Chrome'da WorkerGlobalScope, WebAudio oluşturma iş parçacığından daha düşük öncelikli bir iş parçacığında çalışır, ancak AudioWorkletGlobalScope'a göre bazı avantajlara sahiptir. DedicatedWorkerGlobalScope, kapsamdaki API yüzeyi açısından daha az kısıtlamalıdır. Ayrıca, Worker API birkaç yıldır mevcut olduğundan Emscripten'den daha iyi destek bekleyebilirsiniz.

SharedArrayBuffer, bu tasarımın verimli bir şekilde çalışmasında kritik bir rol oynar. Hem Worker hem de AudioWorkletProcessor eşzamansız mesajlaşma (MessagePort) ile donatılmış olsa da, tekrarlanan bellek ayırmaları ve mesajlaşma gecikmesi nedeniyle gerçek zamanlı ses işleme için ideal değildir. Bu nedenle, hızlı çift yönlü veri aktarımı için her iki iş parçacığından erişilebilen bir bellek bloğunu öne ayırırız.

Web Audio API'nın bakış açısından bu tasarım yetersiz görünebilir çünkü bu tasarım, Audio İş Akışı'nı basit bir "ses havuzu" olarak kullanır ve her şeyi Çalışan'da yapar. Ancak JavaScript'te C/C++ projelerini yeniden yazmanın maliyetinin yıkıcı olabileceği düşünüldüğünde bu tasarım bu tür projeler için en verimli uygulama yolu olabilir.

Paylaşılan Durumlar ve Atomics

Ses verileri için paylaşılan bir bellek kullanılırken her iki taraftan erişim dikkatli bir şekilde koordine edilmelidir. Atomsal olarak erişilebilir durumları paylaşmak bu tür sorunlar için bir çözümdür. Bu amaçla, SAB tarafından desteklenen Int32Array avantajlarından yararlanabiliriz.

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

Senkronizasyon mekanizması: SharedArrayBuffer ve Atomics

Durum dizisindeki her alan, paylaşılan tamponlarla ilgili hayati bilgileri temsil eder. En önemlisi senkronizasyon alanıdır (REQUEST_RENDER). Çalışanın AudioWorkletProcessor tarafından bu alana dokunmasını beklemesi ve uyandığında sesi işlemesi fikrinden ibarettir. SharedArrayBuffer (SAB) ile birlikte Atomics API bu mekanizmayı mümkün kılar.

İki iş parçacığının senkronizasyonunun oldukça gevşek olduğuna dikkat edin. Worker.process() başlangıcı AudioWorkletProcessor.process() yöntemi tarafından tetiklenir ancak AudioWorkletProcessor, Worker.process() tamamlanana kadar beklemez. Bu, tasarım gereğidir; AudioWorkletProcessor ses geri çağırma tarafından yönlendirildiğinden eşzamanlı olarak engellenmemelidir. En kötü senaryoda, ses akışı kopyalanması veya kesilmesiyle ilgili sorun yaşayabilir ancak oluşturma performansı dengelendiğinde sonunda düzelir.

Kurulum ve Çalıştırma

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

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

Başlatma işlemi tamamlandığında AudioWorkletProcessor.process() çağrılmaya başlar. Oluşturma döngüsünün her iterasyonunda gerçekleşecekler aşağıda açıklanmıştır.

Oluşturma Döngüsü
SharedArrayBuffers ile çok iş parçacıklı oluşturma
SharedArrayBuffers ile çok iş parçacıklı oluşturma
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs), her oluşturma kuantumu için çağrılır.
    1. inputs, Giriş SAB'sine aktarılır.
    2. outputs, Çıkış SAB'sinde ses verileri tüketilerek doldurulur.
    3. Durum SAB'sini uygun şekilde yeni arabellek dizinleriyle günceller.
    4. Çıkış SAB alt akış eşiğine yaklaşırsa Çalışanı daha fazla ses verisi oluşturması için uyandırın.
  2. [DWGS] Çalışan, AudioWorkletProcessor.process() kaynağından gelen uyanık kalma sinyalini bekler (uyku). Uyandığında:
    1. Durumlar SAB'sinden arabellek dizinlerini getirir.
    2. İşlem işlevini, Çıkış SAB'sini doldurmak için Giriş SAB'deki verilerle çalıştırın.
    3. Durum SAB'sini arabellek dizinlerini uygun şekilde günceller.
    4. Uykuya dalar ve bir sonraki sinyalin gelmesini bekler.

Örnek kodu burada bulabilirsiniz. Ancak bu demonun çalışması için SharedArrayBuffer deneysel işaretinin etkinleştirilmesi gerektiğini unutmayın. Kod, kolaylık sağlaması için saf JS koduyla yazılmıştır, ancak gerekirse WebAssembly koduyla değiştirilebilir. Bu tür bir durum, bellek yönetimi HeapAudioBuffer sınıfıyla sarmalanarak daha dikkatli bir şekilde ele alınmalıdır.

Sonuç

Ses İş Akışı'nın asıl amacı Web Audio API'sını gerçekten "genişletilebilir" hale getirmektir. Web Audio API'nın geri kalanının Audio Worklet ile uygulanmasını mümkün kılmak için tasarımında çok yıllarca çalışıldı. Buna karşılık, artık tasarımında daha fazla karmaşıklık var ve bu beklenmedik bir sorun olabiliyor.

Neyse ki bu karmaşıklığın sebebi sadece geliştiricileri güçlendirmek. WebAssembly'yi AudioWorkletGlobalScope'ta çalıştırabilmek, web'de yüksek performanslı ses işleme için büyük bir potansiyel ortaya çıkarır. C veya C++ dilinde yazılan büyük ölçekli ses uygulamaları için SharedArrayBuffers ve Çalışanlarla bir Ses İş Akışını kullanmak keşfetmek için cazip bir seçenek olabilir.

Kredi

Bu makalenin taslağını inceleyip faydalı geri bildirimde bulundukları için Chris Wilson, Jason Miller, Joshua Bell ve Raymond Toy'a özel teşekkür ederiz.