Artikel sebelumnya tentang Worklet Audio menjelaskan konsep dan penggunaan dasar. Sejak peluncurannya di Chrome 66, ada banyak permintaan untuk contoh lain tentang cara menggunakannya dalam aplikasi yang sebenarnya. Audio Worklet membuka potensi penuh WebAudio, tetapi memanfaatkannya bisa jadi sulit karena memerlukan pemahaman pemrograman serentak yang digabungkan dengan beberapa JS API. Bahkan bagi developer yang sudah terbiasa dengan WebAudio, mengintegrasikan Worklet Audio dengan API lain (misalnya WebAssembly) dapat menjadi sulit.
Artikel ini akan memberi pembaca pemahaman yang lebih baik tentang cara menggunakan Worklet Audio di setelan dunia nyata dan menawarkan tips untuk memanfaatkan kemampuannya secara maksimal. Pastikan Anda juga melihat contoh kode dan demo langsung.
Recap: Worklet Audio
Sebelum membahas lebih lanjut, mari kita rangkum istilah dan fakta seputar sistem Worklet Audio yang sebelumnya diperkenalkan di postingan ini.
- BaseAudioContext: Objek utama Web Audio API.
- Audio Worklet: Pemuat file skrip khusus untuk operasi Audio Worklet. Termasuk dalam BaseAudioContext. BaseAudioContext dapat memiliki satu Worklet Audio. File skrip yang dimuat dievaluasi di AudioWorkletGlobalScope dan digunakan untuk membuat instance AudioWorkletProcessor.
- AudioWorkletGlobalScope : Cakupan global JS khusus untuk operasi Audio Worklet. Berjalan di thread rendering khusus untuk WebAudio. BaseAudioContext dapat memiliki satu AudioWorkletGlobalScope.
- AudioWorkletNode : AudioNode yang dirancang untuk operasi Audio Worklet. Dibuat instance-nya dari BaseAudioContext. BaseAudioContext dapat memiliki beberapa AudioWorkletNodes seperti halnya AudioNodes native.
- AudioWorkletProcessor : Pendamping AudioWorkletNode. Inti sebenarnya dari AudioWorkletNode yang memproses streaming audio oleh kode yang disediakan pengguna. Class ini dibuat instance-nya 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 membawa berbagai keuntungan untuk 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 pengumpulan sampah dalam kode pemrosesan audio.
Yang pertama penting bagi developer yang memiliki investasi dalam kode dan library pemrosesan audio yang ada, tetapi yang kedua sangat penting bagi hampir semua pengguna API. Di dunia WebAudio, anggaran pengaturan waktu untuk streaming audio yang stabil cukup menuntut: hanya 3 md pada frekuensi sampel 44,1 Khz. Bahkan gangguan kecil dalam kode pemrosesan audio dapat menyebabkan gangguan. Developer harus mengoptimalkan kode untuk pemrosesan yang lebih cepat, tetapi juga meminimalkan jumlah sampah JS yang dihasilkan. Menggunakan WebAssembly dapat menjadi solusi yang mengatasi kedua masalah tersebut secara bersamaan: lebih cepat dan tidak menghasilkan sampah dari kode.
Bagian berikutnya menjelaskan cara WebAssembly dapat digunakan dengan Worklet Audio dan contoh kode yang menyertainya dapat ditemukan di sini. Untuk tutorial dasar tentang cara menggunakan Emscripten dan WebAssembly (terutama kode perekat Emscripten), lihat artikel ini.
Menyiapkan
Semuanya terdengar bagus, tetapi kita memerlukan sedikit struktur untuk menyiapkan semuanya dengan benar. Pertanyaan desain pertama yang harus diajukan adalah cara dan tempat membuat instance modul WebAssembly. Setelah mengambil kode glue Emscripten, ada dua jalur untuk pembuatan instance modul:
- Buat instance modul WebAssembly dengan memuat kode lem ke dalam
AudioWorkletGlobalScope melalui
audioContext.audioWorklet.addModule()
. - Buat instance modul WebAssembly dalam cakupan utama, lalu transfer modul melalui opsi konstruktor AudioWorkletNode.
Keputusan ini sangat bergantung pada desain dan preferensi Anda, tetapi idenya adalah modul WebAssembly dapat menghasilkan instance WebAssembly di AudioWorkletGlobalScope, yang menjadi kernel pemrosesan audio dalam instance AudioWorkletProcessor.
Agar pola A berfungsi dengan benar, Emscripten memerlukan beberapa opsi untuk membuat kode lem WebAssembly yang benar untuk konfigurasi kita:
-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js
Opsi ini memastikan kompilasi sinkron modul WebAssembly di
AudioWorkletGlobalScope. Class ini juga menambahkan definisi class
AudioWorkletProcessor di mycode.js
sehingga dapat dimuat setelah modul diinisialisasi.
Alasan utama untuk menggunakan kompilasi sinkron adalah resolusi promise
audioWorklet.addModule()
tidak menunggu resolusi
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 di AudioWorkletGlobalScope, yang berjalan dari thread
utama. (Lihat
ini
untuk mengetahui info selengkapnya.)
Pola B dapat berguna jika diperlukan tugas berat asinkron. Class ini menggunakan thread utama untuk mengambil kode glue dari server dan mengompilasi modul. Kemudian, modul WASM akan ditransfer melalui konstruktor AudioWorkletNode. Pola ini akan lebih masuk akal jika Anda harus memuat modul secara dinamis setelah AudioWorkletGlobalScope mulai merender streaming audio. Bergantung pada ukuran modul, mengompilasi modul di tengah rendering dapat menyebabkan gangguan pada streaming.
Data Audio dan Heap WASM
Kode WebAssembly hanya berfungsi pada memori yang dialokasikan dalam heap WASM khusus. Untuk memanfaatkannya, data audio perlu di-clone bolak-balik antara heap WASM dan array data audio. Class HeapAudioBuffer dalam kode contoh menangani operasi ini dengan baik.
Ada proposal awal yang sedang dibahas untuk mengintegrasikan heap WASM langsung ke dalam sistem Worklet Audio. Menghapus cloning data yang redundan ini antara memori JS dan heap WASM tampaknya wajar, tetapi detail spesifiknya perlu dibahas.
Menangani Ketidakcocokan Ukuran Buffer
Pasangan AudioWorkletNode dan AudioWorkletProcessor dirancang agar berfungsi seperti AudioNode biasa; AudioWorkletNode menangani interaksi dengan kode lain sedangkan AudioWorkletProcessor menangani pemrosesan audio internal. Karena AudioNode reguler memproses 128 frame sekaligus, AudioWorkletProcessor harus melakukan hal yang sama agar menjadi fitur inti. Ini adalah salah satu keunggulan desain Audio Worklet yang memastikan tidak ada latensi tambahan karena buffering internal diperkenalkan dalam AudioWorkletProcessor, tetapi hal ini dapat menjadi masalah jika fungsi pemrosesan memerlukan ukuran buffer yang berbeda dari 128 frame. Solusi umum untuk kasus tersebut adalah menggunakan ring buffer, yang juga dikenal sebagai buffer melingkar atau FIFO.
Berikut adalah diagram AudioWorkletProcessor yang menggunakan dua buffer cincin di dalamnya untuk menampung fungsi WASM yang mengambil 512 frame masuk dan keluar. (Angka 512 di sini dipilih secara acak.)
Algoritma untuk diagram adalah:
- AudioWorkletProcessor mendorong 128 frame ke Input RingBuffer dari Input-nya.
- Lakukan langkah-langkah berikut hanya jika Input RingBuffer memiliki lebih dari atau sama dengan 512 frame.
- Ambil 512 frame dari Input RingBuffer.
- Memproses 512 frame dengan fungsi WASM yang diberikan.
- Kirim 512 frame ke Output RingBuffer.
- AudioWorkletProcessor mengambil 128 frame dari Output RingBuffer untuk mengisi Output-nya.
Seperti yang ditunjukkan pada diagram, Frame input selalu diakumulasikan ke dalam Input RingBuffer dan menangani overflow buffering dengan menimpa blok frame terlama dalam buffering. Hal ini wajar dilakukan untuk aplikasi audio real-time. Demikian pula, blok frame Output akan selalu diambil oleh sistem. Buffer underflow (data tidak cukup) di Output RingBuffer akan menyebabkan diam yang menyebabkan gangguan 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 drop-in SPN dengan AudioWorkletNode dapat menjadi sulit dan menggunakan buffer ring memberikan solusi yang bagus. Perekam audio akan menjadi contoh bagus yang dapat dibuat berdasarkan desain ini.
Namun, penting untuk dipahami bahwa desain ini hanya merekonsiliasi ketidakcocokan ukuran buffering dan tidak memberikan lebih banyak waktu untuk menjalankan kode skrip yang diberikan. Jika kode tidak dapat menyelesaikan tugas dalam anggaran waktu render quantum (~3 md pada 44,1 Khz), hal ini akan memengaruhi waktu awal fungsi callback berikutnya dan pada akhirnya 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 heap WASM harus di-clone, tetapi kita dapat menggunakan class HeapAudioBuffer untuk membuat pengelolaan memori sedikit lebih mudah. Ide penggunaan memori yang dialokasikan pengguna untuk mengurangi duplikasi data yang tidak diperlukan akan dibahas pada masa mendatang.
Class RingBuffer dapat ditemukan di sini.
WebAudio Powerhouse: Worklet Audio dan SharedArrayBuffer
Pola desain terakhir dalam artikel ini adalah menempatkan beberapa API canggih ke dalam satu tempat; Audio Worklet, SharedArrayBuffer, Atomics, dan Worker. Dengan penyiapan yang tidak biasa ini, Anda dapat membuka jalur untuk software audio yang ada yang ditulis dalam C/C++ agar dapat berjalan di browser web sekaligus mempertahankan pengalaman pengguna yang lancar.
Keuntungan terbesar dari desain ini adalah dapat menggunakan DedicatedWorkerGlobalScope hanya untuk pemrosesan audio. Di Chrome, WorkerGlobalScope berjalan pada thread dengan prioritas lebih rendah daripada thread rendering WebAudio, tetapi memiliki beberapa kelebihan dibandingkan AudioWorkletGlobalScope . DedicatedWorkerGlobalScope kurang dibatasi dalam hal platform API yang tersedia dalam cakupan. Selain itu, Anda dapat mengharapkan dukungan yang lebih baik dari Emscripten karena Worker API telah ada selama beberapa tahun.
SharedArrayBuffer memainkan peran penting agar desain ini berfungsi secara efisien. Meskipun Worker dan AudioWorkletProcessor dilengkapi dengan pesan asinkron (MessagePort), hal ini tidak optimal untuk pemrosesan audio real-time karena alokasi memori berulang dan latensi pesan. Jadi, kita mengalokasikan blok memori di awal yang dapat diakses dari kedua thread untuk transfer data dua arah yang cepat.
Dari sudut pandang Web Audio API purist, desain ini mungkin terlihat kurang optimal karena menggunakan Audio Worklet sebagai "audio sink" sederhana dan melakukan semuanya di Worker. Namun, mengingat biaya penulisan ulang project C/C++ dalam JavaScript dapat menjadi penghalang atau bahkan tidak mungkin, desain ini dapat menjadi jalur penerapan yang paling efisien untuk project tersebut.
Status Bersama dan Atom
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
Setiap kolom array States mewakili informasi penting tentang buffering
bersama. Yang paling penting adalah kolom untuk sinkronisasi
(REQUEST_RENDER
). Idenya adalah Worker menunggu kolom ini disentuh
oleh AudioWorkletProcessor dan memproses audio saat aktif. Bersama dengan
SharedArrayBuffer (SAB), Atomics API memungkinkan mekanisme ini.
Perhatikan bahwa sinkronisasi dua thread agak longgar. Awal
Worker.process()
akan dipicu oleh metode
AudioWorkletProcessor.process()
, tetapi AudioWorkletProcessor tidak menunggu hingga Worker.process()
selesai. Hal ini memang disengaja; 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 distabilkan.
Menyiapkan dan Menjalankan
Seperti yang ditunjukkan pada diagram di atas, desain ini memiliki beberapa komponen untuk diatur: DedicatedWorkerGlobalScope (DWGS), AudioWorkletGlobalScope (AWGS), SharedArrayBuffer, dan thread utama. Langkah-langkah berikut menjelaskan hal yang harus terjadi dalam fase inisialisasi.
Inisialisasi
- [Main] Konstruktor AudioWorkletNode dipanggil.
- Buat Pekerja.
- AudioWorkletProcessor terkait akan dibuat.
- [DWGS] Pekerja membuat 2 SharedArrayBuffer. (satu untuk status bersama dan satu lagi untuk data audio)
- [DWGS] Pekerja mengirim referensi SharedArrayBuffer ke AudioWorkletNode.
- [Main] AudioWorkletNode mengirimkan referensi SharedArrayBuffer ke AudioWorkletProcessor.
- [AWGS] AudioWorkletProcessor memberi tahu AudioWorkletNode bahwa penyiapan selesai.
Setelah inisialisasi selesai, AudioWorkletProcessor.process()
mulai
dipanggil. Berikut adalah hal yang akan terjadi dalam setiap iterasi loop rendering.
Loop Rendering
- [AWGS]
AudioWorkletProcessor.process(inputs, outputs)
dipanggil untuk setiap kuantum render.inputs
akan didorong ke Input SAB.outputs
akan diisi dengan menggunakan data audio di Output SAB.- Memperbarui States SAB dengan indeks buffering baru yang sesuai.
- Jika Output SAB mendekati nilai minimum underflow, Wake Worker akan merender lebih banyak data audio.
- [DWGS] Pekerja menunggu (tidur) sinyal bangun dari
AudioWorkletProcessor.process()
. Saat perangkat aktif:- Mengambil indeks buffering dari States SAB.
- Jalankan fungsi proses dengan data dari Input SAB untuk mengisi Output SAB.
- Memperbarui States SAB dengan indeks buffering yang sesuai.
- Berhenti beroperasi dan menunggu sinyal berikutnya.
Kode contoh dapat ditemukan di sini, tetapi perlu diperhatikan bahwa tanda eksperimen SharedArrayBuffer harus diaktifkan agar demo ini berfungsi. Kode ini ditulis dengan kode JS murni untuk memudahkan, tetapi dapat diganti dengan kode WebAssembly jika diperlukan. Kasus tersebut harus ditangani dengan sangat hati-hati dengan menggabungkan pengelolaan memori dengan class HeapAudioBuffer.
Kesimpulan
Tujuan utama Audio Worklet adalah membuat Web Audio API benar-benar "dapat diperluas". Upaya selama bertahun-tahun telah dilakukan dalam desainnya untuk memungkinkan implementasi Web Audio API lainnya dengan Audio Worklet. Akibatnya, sekarang kita 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 khusus kepada Chris Wilson, Jason Miller, Joshua Bell, dan Raymond Toy atas peninjauan draf artikel ini dan pemberian masukan yang bermanfaat.