Pola desain worklet audio

Artikel sebelumnya tentang Worklet Audio menjelaskan konsep dasar dan penggunaannya. Sejak peluncurannya di Chrome 66, ada banyak permintaan untuk contoh lainnya terkait cara penggunaan dalam aplikasi sebenarnya. Audio Worklet memaksimalkan potensi WebAudio, tetapi memanfaatkannya bukanlah hal yang mudah karena memerlukan pemahaman pemrograman serentak yang digabungkan dengan beberapa JS API. Bahkan bagi developer yang sudah terbiasa dengan WebAudio, mengintegrasikan Audio Worklet dengan API lain (misalnya WebAssembly) bisa jadi sulit.

Artikel ini akan memberikan pemahaman yang lebih baik kepada pembaca tentang cara menggunakan Worklet Audio di dunia nyata dan memberikan tips untuk memanfaatkannya secara maksimal. Pastikan untuk melihat contoh kode dan demo langsung.

Rangkuman: Worklet Audio

Sebelum melanjutkan, mari kita rangkum istilah dan fakta seputar sistem Worklet Audio yang sebelumnya diperkenalkan dalam postingan ini.

  • BaseAudioContext: Objek utama Web Audio API.
  • Worklet Audio: Loader file skrip khusus untuk operasi Worklet Audio. Termasuk dalam BaseAudioContext. BaseAudioContext dapat memiliki satu Worklet Audio. File skrip yang dimuat akan dievaluasi dalam AudioWorkletGlobalScope dan digunakan untuk membuat instance AudioWorkletProcessor.
  • AudioWorkletGlobalScope : Cakupan global JS khusus untuk operasi Worklet Audio. Berjalan di thread rendering khusus untuk WebAudio. BaseAudioContext dapat memiliki satu AudioWorkletGlobalScope.
  • AudioWorkletNode: AudioNode yang dirancang untuk operasi Worklet Audio. Di-instance dari BaseAudioContext. BaseAudioContext dapat memiliki beberapa AudioWorkletNode yang serupa dengan AudioNode native.
  • AudioWorkletProcessor : Versi dari AudioWorkletNode. Isian sebenarnya dari AudioWorkletNode memproses streaming audio dengan kode yang diberikan pengguna. Instance ini dibuat di AudioWorkletGlobalScope saat AudioWorkletNode dibuat. AudioWorkletNode dapat memiliki satu AudioWorkletProcessor yang cocok.

Pola Desain

Menggunakan Worklet Audio dengan WebAssembly

WebAssembly adalah pendamping sempurna untuk AudioWorkletProcessor. Kombinasi kedua fitur ini memberikan berbagai keuntungan pemrosesan audio di web, tetapi dua manfaat terbesarnya adalah: a) menghadirkan kode pemrosesan audio C/C++ yang ada ke dalam ekosistem WebAudio dan b) menghindari overhead kompilasi JIT JS dan pembersihan sampah memori dalam kode pemrosesan audio.

Pendekatan ini penting bagi developer yang sudah memiliki investasi dalam kode dan library pemrosesan audio, tetapi library kedua sangat penting bagi hampir semua pengguna API. Di dunia WebAudio, anggaran pengaturan waktu untuk streaming audio stabil cukup berat: hanya 3 md pada frekuensi sampel 44,1 Khz. Bahkan sedikit gangguan dalam kode pemrosesan audio dapat menyebabkan gangguan. Developer harus mengoptimalkan kode untuk mempercepat pemrosesan, tetapi juga meminimalkan jumlah sampah JS yang dihasilkan. Menggunakan WebAssembly dapat menjadi solusi yang mengatasi kedua masalah secara bersamaan: lebih cepat dan tidak menghasilkan sampah dari kode.

Bagian berikutnya menjelaskan cara penggunaan WebAssembly dengan Worklet Audio dan contoh kode yang disertakan dapat ditemukan di sini. Untuk tutorial dasar tentang cara menggunakan Emscripten dan WebAssembly (terutama kode lem Emscripten), lihat artikel ini.

Menyiapkan

Semuanya terdengar bagus, tetapi kita perlu sedikit struktur untuk mengatur semuanya dengan benar. Pertanyaan desain pertama yang harus diajukan adalah bagaimana dan di mana membuat instance modul WebAssembly. Setelah mengambil kode glue Emscripten, ada dua jalur untuk pembuatan instance modul:

  1. Buat instance modul WebAssembly dengan memuat kode glue ke AudioWorkletGlobalScope melalui audioContext.audioWorklet.addModule().
  2. Buat instance modul WebAssembly dalam cakupan utama, lalu transfer modul melalui opsi konstruktor AudioWorkletNode.

Keputusannya sebagian besar bergantung pada desain dan preferensi Anda, tetapi idenya adalah bahwa modul WebAssembly dapat menghasilkan instance WebAssembly di AudioWorkletGlobalScope, yang menjadi kernel pemrosesan audio dalam instance AudioWorkletProcessor.

Pola pembuatan instance modul WebAssembly A: Menggunakan panggilan .addModule()
Pola pembuatan instance modul WebAssembly A: Menggunakan panggilan .addModule()

Agar pola A berfungsi dengan benar, Emscripten memerlukan beberapa opsi untuk menghasilkan kode glue WebAssembly yang benar untuk konfigurasi:

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

Opsi ini memastikan kompilasi sinkron modul WebAssembly di AudioWorkletGlobalScope. Kode ini juga menambahkan definisi class AudioWorkletProcessor di mycode.js sehingga dapat dimuat setelah modul diinisialisasi. Alasan utama untuk menggunakan kompilasi sinkron adalah karena resolusi promise audioWorklet.addModule() tidak menunggu penyelesaian promise di AudioWorkletGlobalScope. Pemuatan atau kompilasi sinkron di thread utama umumnya tidak direkomendasikan karena memblokir tugas lain di thread yang sama, tetapi di sini kita dapat mengabaikan aturan karena kompilasi terjadi pada AudioWorkletGlobalScope, yang berjalan dari thread utama. (Lihat ini untuk info selengkapnya.)

Pola pembuatan instance modul WASM B: Menggunakan transfer lintas thread konstruktor AudioWorkletNode
Pola pembuatan instance modul WASM B: Menggunakan transfer lintas thread konstruktor AudioWorkletNode

Pola B dapat berguna jika diperlukan angkat berat asinkron. Library ini menggunakan thread utama untuk mengambil kode glue dari server dan mengompilasi modul. Kemudian, GPU akan mentransfer modul WASM melalui konstruktor AudioWorkletNode. Pola ini akan semakin bermakna jika Anda harus memuat modul secara dinamis setelah AudioWorkletGlobalScope mulai merender streaming audio. Bergantung pada ukuran modul, mengompilasi modul di tengah-tengah rendering dapat menyebabkan gangguan dalam streaming.

Heap WASM dan Data Audio

Kode WebAssembly hanya berfungsi pada memori yang dialokasikan dalam heap WASM khusus. Untuk memanfaatkannya, data audio harus di-clone bolak-balik antara heap WASM dan array data audio. Class HeapAudioBuffer dalam kode contoh menangani operasi ini dengan baik.

Class HeapAudioBuffer untuk memudahkan penggunaan heap WASM
Class HeapAudioBuffer untuk memudahkan penggunaan heap WASM

Ada proposal awal yang sedang dibahas untuk mengintegrasikan heap WASM secara langsung ke sistem Worklet Audio. Menghilangkan cloning data redundan antara memori JS dan heap WASM tampak alami, tetapi detail spesifiknya perlu dikerjakan.

Menangani Ketidakcocokan Ukuran Buffer

Pasangan AudioWorkletNode dan AudioWorkletProcessor dirancang untuk berfungsi seperti AudioNode biasa; AudioWorkletNode menangani interaksi dengan kode lain sedangkan AudioWorkletProcessor menangani pemrosesan audio internal. Karena AudioNode biasa memproses 128 frame sekaligus, AudioWorkletProcessor harus melakukan hal yang sama untuk menjadi fitur inti. Ini adalah salah satu keunggulan desain Worklet Audio yang memastikan tidak ada latensi tambahan karena buffering internal terjadi dalam AudioWorkletProcessor, tetapi dapat menjadi masalah jika fungsi pemrosesan memerlukan ukuran buffer yang berbeda dari 128 frame. Solusi umum untuk kasus ini adalah dengan menggunakan buffer cincin, yang juga dikenal sebagai buffer sirkular atau FIFO.

Berikut adalah diagram AudioWorkletProcessor yang menggunakan dua buffer cincin di dalamnya untuk mengakomodasi fungsi WASM yang memerlukan 512 frame masuk dan keluar. (Nomor 512 di sini dipilih secara arbitrer.)

Menggunakan RingBuffer di dalam metode `process()` AudioWorkletProcessor
Menggunakan RingBuffer di dalam metode `process()` AudioWorkletProcessor

Algoritma untuk diagram adalah:

  1. AudioWorkletProcessor mendorong 128 frame ke Input RingBuffer dari Input-nya.
  2. Lakukan langkah-langkah berikut hanya jika Input RingBuffer memiliki lebih besar dari atau sama dengan 512 frame.
    1. Ambil 512 frame dari Input RingBuffer.
    2. Memproses 512 frame dengan fungsi WASM yang ditentukan.
    3. Mengirim frame 512 ke Output RingBuffer.
  3. AudioWorkletProcessor menarik 128 frame dari Output RingBuffer untuk mengisi Output-nya.

Seperti yang ditunjukkan dalam diagram, Frame input selalu terakumulasi ke dalam Input RingBuffer dan menangani buffer overflow dengan menimpa blok frame terlama dalam buffer. Itu adalah hal yang wajar untuk dilakukan pada aplikasi audio real-time. Demikian pula, blok frame Output akan selalu ditarik oleh sistem. Underflow buffer (data tidak cukup) di RingBuffer Output akan menghasilkan senyap yang menyebabkan glitch dalam streaming.

Pola ini berguna saat mengganti ScriptProcessorNode (SPN) dengan AudioWorkletNode. Karena SPN memungkinkan developer memilih ukuran buffer antara 256 dan 16384 frame, sehingga penggantian langsung SPN dengan AudioWorkletNode bisa jadi sulit, dan menggunakan buffer cincin memberikan solusi yang bagus. Perekam audio akan menjadi contoh bagus yang dapat dikembangkan dari desain ini.

Namun, penting untuk dipahami bahwa desain ini hanya merekonsiliasi ketidakcocokan ukuran buffer dan tidak memberikan lebih banyak waktu untuk menjalankan kode skrip yang diberikan. Jika kode tidak dapat menyelesaikan tugas dalam anggaran waktu kuantum render (~3 md pada 44,1 Khz), kode tersebut akan memengaruhi waktu mulai fungsi callback berikutnya dan pada akhirnya akan menyebabkan gangguan.

Menggabungkan desain ini dengan WebAssembly dapat menjadi rumit karena pengelolaan memori di sekitar heap WASM. Pada saat penulisan, data yang masuk dan keluar dari heap WASM harus di-clone, tetapi kita dapat menggunakan class HeapAudioBuffer untuk mempermudah pengelolaan memori. Ide penggunaan memori yang dialokasikan pengguna untuk mengurangi cloning data redundan akan dibahas pada masa mendatang.

Class RingBuffer dapat ditemukan di sini.

WebAudio Powerhouse: Audio Worklet dan SharedArrayBuffer

Pola desain terakhir dalam artikel ini adalah menempatkan beberapa API canggih ke satu tempat; Audio Worklet, SharedArrayBuffer, Atomics, dan Worker. Melalui penyiapan yang tidak mudah ini, fitur ini membuka jalur bagi software audio yang ada yang ditulis dalam C/C++ untuk berjalan di browser web sambil mempertahankan pengalaman pengguna yang lancar.

Ringkasan pola desain terakhir: Audio Worklet, SharedArrayBuffer, dan Worker
Ringkasan pola desain terakhir: Worklet Audio, SharedArrayBuffer, dan Worker

Keuntungan terbesar dari desain ini adalah kemampuan menggunakan DedicatedWorkerGlobalScope hanya untuk pemrosesan audio. Di Chrome, WorkerGlobalScope berjalan pada thread prioritas lebih rendah daripada thread rendering WebAudio, tetapi memiliki beberapa keunggulan dibandingkan AudioWorkletGlobalScope . DedicatedWorkerGlobalScope tidak terlalu dibatasi dalam hal platform API yang tersedia dalam cakupan. Anda juga bisa mendapatkan dukungan yang lebih baik dari Emscripten karena Worker API telah ada selama beberapa tahun.

SharedArrayBuffer memainkan peran penting agar desain ini dapat bekerja secara efisien. Meskipun Worker dan AudioWorkletProcessor dilengkapi dengan pesan asinkron (MessagePort), fitur ini kurang optimal untuk pemrosesan audio real-time karena alokasi memori dan latensi pesan yang berulang. Jadi, kami mengalokasikan blok memori di depan yang dapat diakses dari kedua thread untuk transfer data dua arah yang cepat.

Dari sudut pandang murni Web Audio API, desain ini mungkin terlihat kurang optimal karena menggunakan Worklet Audio sebagai "sink audio" sederhana dan melakukan semua hal di Pekerja. Namun, mengingat biaya penulisan ulang project C/C++ di JavaScript dapat menjadi hambatan atau bahkan tidak mungkin, desain ini dapat menjadi jalur implementasi yang paling efisien untuk project tersebut.

Status Bersama dan Atomik

Saat menggunakan memori bersama untuk data audio, akses dari kedua sisi harus dikoordinasikan dengan cermat. Membagikan status yang dapat diakses secara atomik adalah solusi untuk masalah tersebut. Kita dapat memanfaatkan Int32Array yang didukung oleh SAB untuk tujuan ini.

Mekanisme sinkronisasi: SharedArrayBuffer dan Atomics
Mekanisme sinkronisasi: SharedArrayBuffer dan Atomics

Mekanisme sinkronisasi: SharedArrayBuffer dan Atomics

Setiap kolom array State mewakili informasi penting tentang buffer bersama. Yang paling penting adalah kolom untuk sinkronisasi (REQUEST_RENDER). Idenya adalah Pekerja menunggu kolom ini disentuh oleh AudioWorkletProcessor dan memproses audio saat pekerja bangun. Bersama dengan SharedArrayBuffer (SAB), Atomics API memungkinkan mekanisme ini.

Perhatikan bahwa sinkronisasi dua thread agak longgar. Onset Worker.process() akan dipicu oleh metode AudioWorkletProcessor.process(), tetapi AudioWorkletProcessor tidak akan menunggu sampai Worker.process() selesai. Ini memang didesain; AudioWorkletProcessor didorong oleh callback audio sehingga tidak boleh diblokir secara sinkron. Dalam skenario terburuk, streaming audio mungkin mengalami duplikat atau terputus, tetapi pada akhirnya akan pulih saat performa rendering stabil.

Menyiapkan dan Menjalankan

Seperti ditunjukkan dalam diagram di atas, desain ini memiliki beberapa komponen yang dapat disusun: DedicatedWorkerGlobalScope (DWGS), AudioWorkletGlobalScope (AWGS), SharedArrayBuffer, dan thread utama. Langkah-langkah berikut menjelaskan hal yang harus terjadi dalam fase inisialisasi.

Inisialisasi
  1. [Utama] Konstruktor AudioWorkletNode dipanggil.
    1. Membuat Pekerja.
    2. AudioWorkletProcessor terkait akan dibuat.
  2. [DWGS] Pekerja membuat 2 SharedArrayBuffer. (satu untuk status bersama dan satu lagi untuk data audio)
  3. [DWGS] Pekerja mengirimkan referensi SharedArrayBuffer ke AudioWorkletNode.
  4. [Utama] AudioWorkletNode mengirimkan referensi SharedArrayBuffer ke AudioWorkletProcessor.
  5. [AWGS] AudioWorkletProcessor memberi tahu AudioWorkletNode bahwa penyiapan telah selesai.

Setelah inisialisasi selesai, AudioWorkletProcessor.process() akan mulai dipanggil. Berikut adalah hal yang harus terjadi dalam setiap iterasi loop rendering.

Loop Rendering
Rendering multi-thread dengan SharedArrayBuffer
Rendering multi-thread dengan SharedArrayBuffer
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs) dipanggil untuk setiap kuantum render.
    1. inputs akan dikirim ke Input SAB.
    2. outputs akan diisi dengan menggunakan data audio di Output SAB.
    3. Memperbarui States SAB dengan indeks buffer baru yang sesuai.
    4. Jika Output SAB mendekati batas underflow, Wake Worker untuk merender lebih banyak data audio.
  2. [DWGS] Pekerja menunggu (tidur) untuk mendapatkan sinyal bangun dari AudioWorkletProcessor.process(). Saat bangun:
    1. Mengambil indeks buffer dari States SAB.
    2. Jalankan fungsi proses dengan data dari Input SAB untuk mengisi Output SAB.
    3. Memperbarui States SAB dengan indeks buffer yang sesuai.
    4. Tidur dan menunggu sinyal berikutnya.

Kode contoh dapat ditemukan di sini, tetapi perhatikan bahwa tanda eksperimental SharedArrayBuffer harus diaktifkan agar demo ini dapat berfungsi. Kode tersebut ditulis dengan kode JS murni agar lebih praktis, tetapi dapat diganti dengan kode WebAssembly jika diperlukan. Kasus tersebut harus ditangani dengan lebih hati-hati dengan menggabungkan pengelolaan memori dengan class HeapAudioBuffer.

Kesimpulan

Tujuan utama dari Audio Worklet adalah untuk membuat Web Audio API benar-benar "dapat diperluas". Sebuah upaya multitahun yang diupayakan untuk memungkinkan penerapan Web Audio API lainnya dengan Audio Worklet. Sebaliknya, sekarang kami memiliki kompleksitas yang lebih tinggi dalam desainnya dan ini bisa menjadi tantangan yang tidak terduga.

Untungnya, alasan kompleksitas tersebut semata-mata untuk memberdayakan developer. Kemampuan untuk menjalankan WebAssembly di AudioWorkletGlobalScope membuka potensi besar untuk pemrosesan audio berperforma tinggi di web. Untuk aplikasi audio berskala besar yang ditulis dalam C atau C++, menggunakan Worklet Audio dengan SharedArrayBuffers dan Pekerja dapat menjadi opsi yang menarik untuk dijelajahi.

Kredit

Terima kasih banyak kepada Chris Wilson, Jason Miller, Joshua Bell, dan Raymond Toy yang telah meninjau draf artikel ini dan memberikan masukan yang bermanfaat.