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:
- Buat instance modul WebAssembly dengan memuat kode glue ke
AudioWorkletGlobalScope melalui
audioContext.audioWorklet.addModule()
. - 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.
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 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.
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.)
Algoritma untuk diagram adalah:
- AudioWorkletProcessor mendorong 128 frame ke Input RingBuffer dari Input-nya.
- Lakukan langkah-langkah berikut hanya jika Input RingBuffer memiliki lebih besar dari atau sama dengan 512 frame.
- Ambil 512 frame dari Input RingBuffer.
- Memproses 512 frame dengan fungsi WASM yang ditentukan.
- Mengirim frame 512 ke Output RingBuffer.
- 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.
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
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
- [Utama] Konstruktor AudioWorkletNode dipanggil.
- Membuat Pekerja.
- AudioWorkletProcessor terkait akan dibuat.
- [DWGS] Pekerja membuat 2 SharedArrayBuffer. (satu untuk status bersama dan satu lagi untuk data audio)
- [DWGS] Pekerja mengirimkan referensi SharedArrayBuffer ke AudioWorkletNode.
- [Utama] AudioWorkletNode mengirimkan referensi SharedArrayBuffer ke AudioWorkletProcessor.
- [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
- [AWGS]
AudioWorkletProcessor.process(inputs, outputs)
dipanggil untuk setiap kuantum render.inputs
akan dikirim ke Input SAB.outputs
akan diisi dengan menggunakan data audio di Output SAB.- Memperbarui States SAB dengan indeks buffer baru yang sesuai.
- Jika Output SAB mendekati batas underflow, Wake Worker untuk merender lebih banyak data audio.
- [DWGS] Pekerja menunggu (tidur) untuk mendapatkan sinyal bangun dari
AudioWorkletProcessor.process()
. Saat bangun:- Mengambil indeks buffer dari States SAB.
- Jalankan fungsi proses dengan data dari Input SAB untuk mengisi Output SAB.
- Memperbarui States SAB dengan indeks buffer yang sesuai.
- 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.