Mengembangkan audio game dengan Web Audio API

Pengantar

Audio adalah bagian penting yang membuat pengalaman multimedia begitu menarik. Jika pernah mencoba menonton film tanpa suara, Anda mungkin telah memperhatikan hal ini.

Begitu pula dengan game! Kenangan video game terindah saya adalah musik dan efek suara. Sekarang, dalam banyak kasus, hampir dua dekade setelah memainkan game favorit, saya masih tidak bisa melupakan komposisi Zelda Koji Kondo dan soundtrack Diablo yang atmosferik dari Matt Uelmen. Kemudahan diingat yang sama berlaku untuk efek bunyi, seperti respons klik unit yang langsung dikenali dari Warcraft, dan sampel dari game klasik Nintendo.

Audio game menghadirkan beberapa tantangan menarik. Untuk membuat musik game yang meyakinkan, desainer perlu menyesuaikan dengan status game yang berpotensi tidak dapat diprediksi yang dialami pemain. Dalam praktiknya, bagian game dapat berlangsung selama durasi yang tidak diketahui, suara dapat berinteraksi dengan lingkungan dan bercampur dengan cara yang kompleks, seperti efek ruangan dan posisi suara relatif. Terakhir, ada banyak suara yang diputar sekaligus, yang semuanya harus terdengar bagus bersama-sama dan dirender tanpa menyebabkan penurunan performa.

Audio Game di Web

Untuk game sederhana, penggunaan tag <audio> mungkin sudah cukup. Namun, banyak browser yang memberikan implementasi yang buruk, yang menyebabkan gangguan audio dan latensi tinggi. Semoga ini adalah masalah sementara, karena vendor sedang berupaya keras untuk meningkatkan implementasi masing-masing. Untuk melihat sekilas status tag <audio>, tersedia paket pengujian yang bagus di areweplayingyet.org.

Namun, jika melihat lebih dalam spesifikasi tag <audio>, akan jelas bahwa ada banyak hal yang tidak dapat dilakukan dengan tag tersebut, yang tidak mengherankan, karena tag ini dirancang untuk pemutaran media. Beberapa batasan mencakup:

  • Tidak dapat menerapkan filter ke sinyal suara
  • Tidak ada cara untuk mengakses data PCM mentah
  • Tidak ada konsep posisi dan arah sumber dan pemroses
  • Tidak ada pengaturan waktu yang terperinci.

Di sisa artikel ini, saya akan mendalami beberapa topik ini dalam konteks audio game yang ditulis dengan Web Audio API. Untuk pengenalan singkat tentang API ini, lihat tutorial memulai.

Musik latar belakang

Game sering kali memiliki musik latar yang diputar berulang.

Memang sangat menjengkelkan jika loop Anda pendek dan dapat diprediksi. Jika pemain terhenti di area atau level, dan sampel yang sama terus diputar di latar belakang, sebaiknya pudar trek secara bertahap untuk mencegah frustrasi lebih lanjut. Strategi lainnya adalah memiliki campuran berbagai intensitas yang secara bertahap beralih satu sama lain, bergantung pada konteks game.

Misalnya, jika pemain berada di zona dengan pertempuran bos yang epik, Anda mungkin memiliki beberapa campuran yang bervariasi dalam rentang emosional dari atmosferik hingga pertanda hingga intens. Software sintesis musik sering kali memungkinkan Anda mengekspor beberapa campuran (dengan durasi yang sama) berdasarkan bagian dengan memilih kumpulan trek yang akan digunakan dalam ekspor. Dengan begitu, Anda memiliki beberapa konsistensi internal dan menghindari transisi yang tidak wajar saat melakukan cross-fade dari satu trek ke trek lainnya.

Garageband

Kemudian, menggunakan Web Audio API, Anda dapat mengimpor semua sampel ini menggunakan sesuatu seperti class BufferLoader melalui XHR (ini dibahas secara mendalam dalam artikel pengantar Web Audio API. Pemuatan suara memerlukan waktu, sehingga aset yang digunakan dalam game harus dimuat saat pemuatan halaman, di awal level, atau mungkin secara bertahap saat pemain bermain.

Selanjutnya, Anda membuat sumber untuk setiap node, dan node gain untuk setiap sumber, lalu menghubungkan grafik.

Setelah melakukannya, Anda dapat memutar semua sumber ini secara bersamaan dalam loop, dan karena semuanya memiliki durasi yang sama, Web Audio API akan menjamin bahwa sumber tersebut akan tetap selaras. Saat karakter semakin dekat atau semakin jauh dari pertempuran bos terakhir, game dapat memvariasikan nilai perolehan untuk setiap node masing-masing dalam rantai, menggunakan algoritma jumlah pertambahan seperti berikut:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

Dalam pendekatan di atas, dua sumber diputar sekaligus, dan kita melakukan crossfade di antara keduanya menggunakan kurva daya yang sama (seperti yang dijelaskan dalam pengantar).

Banyak developer game saat ini menggunakan tag <audio> untuk musik latar belakang mereka, karena tag ini sangat cocok untuk konten streaming. Sekarang Anda dapat memasukkan konten dari tag <audio> ke dalam konteks Audio Web.

Teknik ini dapat berguna karena tag <audio> dapat berfungsi dengan konten streaming, yang memungkinkan Anda langsung memutar musik latar belakang tanpa harus menunggu semuanya didownload. Dengan memasukkan streaming ke Web Audio API, Anda dapat memanipulasi atau menganalisis streaming. Contoh berikut menerapkan filter low pass ke musik yang diputar melalui tag <audio>:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

Untuk diskusi yang lebih lengkap tentang cara mengintegrasikan tag <audio> dengan Web Audio API, lihat artikel singkat ini.

Efek suara

Game sering memutar efek suara sebagai respons terhadap input pengguna atau perubahan status game. Namun, seperti musik latar, efek suara dapat menjadi sangat menjengkelkan dengan cepat. Untuk menghindari hal ini, sebaiknya Anda memiliki kumpulan suara yang serupa tetapi berbeda untuk diputar. Hal ini dapat bervariasi dari variasi ringan sampel langkah kaki, hingga variasi drastis, seperti yang terlihat di serial Warcraft sebagai respons terhadap mengklik unit.

Fitur utama lain dari efek suara dalam game adalah dapat ada banyak efek suara secara bersamaan. Bayangkan Anda berada di tengah baku tembak dengan beberapa aktor yang menembakkan senapan mesin. Setiap senapan mesin menembak beberapa kali per detik, sehingga menyebabkan puluhan efek suara diputar secara bersamaan. Memutar suara dari beberapa sumber yang waktunya tepat secara bersamaan adalah salah satu tempat Web Audio API benar-benar bersinar.

Contoh berikut membuat peluru senapan mesin dari beberapa sampel peluru individual dengan membuat beberapa sumber suara yang pemutarannya diselang-seling dalam waktu.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Nah, jika semua senapan mesin dalam game Anda terdengar persis seperti ini, itu akan sangat membosankan. Tentu saja, suara akan bervariasi menurut suara berdasarkan jarak dari target dan posisi relatif (selengkapnya nanti), tetapi hal itu mungkin saja tidak cukup. Untungnya, Web Audio API menyediakan cara untuk menyesuaikan contoh di atas dengan mudah dengan dua cara:

  1. Dengan perubahan waktu yang halus di antara tembakan peluru
  2. Dengan mengubah playbackRate setiap sampel (juga mengubah pitch) untuk menyimulasikan keacakan dunia nyata dengan lebih baik.

Untuk contoh nyata penggunaan teknik ini, lihat demo Tabel Kolam Renang, yang menggunakan pengambilan sampel acak dan memvariasikan playbackRate untuk suara tabrakan bola yang lebih menarik.

Suara posisi 3D

Game sering kali ditetapkan di dunia dengan beberapa properti geometris, baik dalam 2D maupun 3D. Jika demikian, audio yang diposisikan stereo dapat sangat meningkatkan pengalaman imersif. Untungnya, Web Audio API dilengkapi dengan fitur audio posisional yang dipercepat hardware bawaan yang cukup mudah digunakan. Selain itu, Anda harus memastikan bahwa Anda memiliki speaker stereo (sebaiknya headphone) agar contoh berikut dapat dipahami.

Pada contoh di atas, ada pendengar (ikon orang) di tengah kanvas, dan mouse memengaruhi posisi sumber (ikon speaker). Di atas adalah contoh sederhana penggunaan AudioPannerNode untuk mencapai efek semacam ini. Ide dasar dari contoh di atas adalah merespons gerakan mouse dengan menyetel posisi sumber audio, seperti berikut:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Hal yang perlu diketahui tentang perlakuan Web Audio terhadap spasialisasi:

  • Pemroses berada di origin (0, 0, 0) secara default.
  • API posisi Web Audio tidak memiliki satuan, jadi saya memperkenalkan pengganda untuk membuat demo terdengar lebih baik.
  • Web Audio menggunakan koordinat Kartesius y-is-up (kebalikan dari sebagian besar sistem grafis komputer). Itulah sebabnya saya menukar sumbu y dalam cuplikan di atas

Lanjutan: kerucut suara

Model posisi sangat canggih dan cukup maju, sebagian besar didasarkan pada OpenAL. Untuk detail selengkapnya, lihat bagian 3 dan 4 dari spesifikasi yang ditautkan di atas.

Model posisi

Ada satu AudioListener yang terkait dengan konteks Web Audio API yang dapat dikonfigurasi dalam ruang melalui posisi dan orientasi. Setiap sumber dapat diteruskan melalui AudioPannerNode, yang membuat audio input menjadi spasial. Node panner memiliki posisi dan orientasi, serta model jarak dan arah.

Model jarak menentukan jumlah penguatan, bergantung pada kedekatan dengan sumber, sedangkan model direksional dapat dikonfigurasi dengan menentukan kerucut dalam dan luar, yang menentukan jumlah penguatan (biasanya negatif) jika pemroses berada di dalam kerucut bagian dalam, di antara kerucut dalam dan luar, atau di luar kerucut luar.

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

Meskipun contoh saya dalam 2D, model ini mudah digeneralisasi ke dimensi ketiga. Untuk contoh suara yang di-spatialisasi dalam 3D, lihat contoh posisional ini. Selain posisi, model suara Web Audio juga secara opsional menyertakan kecepatan untuk pergeseran doppler. Contoh ini menunjukkan efek doppler secara lebih mendetail.

Untuk informasi selengkapnya tentang topik ini, baca tutorial mendetail tentang [menggabungkan audio posisional dan WebGL][webgl].

Efek dan filter ruangan

Pada kenyataannya, cara suara dirasakan sangat bergantung pada ruangan tempat suara tersebut didengar. Pintu berderit yang sama akan terdengar sangat berbeda di ruang bawah tanah, dibandingkan dengan aula terbuka yang besar. Game dengan nilai produksi yang tinggi ingin meniru efek ini, karena membuat kumpulan sampel terpisah untuk setiap lingkungan sangat mahal, dan akan menghasilkan lebih banyak aset, serta jumlah data game yang lebih besar.

Secara umum, istilah audio untuk perbedaan antara suara mentah dan suara yang terdengar dalam kenyataan adalah respons impuls. Respons impuls ini dapat direkam dengan cermat, dan sebenarnya ada situs yang menghosting banyak file respons impuls yang telah direkam sebelumnya (disimpan sebagai audio) untuk memudahkan Anda.

Untuk mengetahui informasi selengkapnya tentang cara membuat respons impuls dari lingkungan tertentu, baca bagian "Penyiapan Perekaman" di bagian Konvolusi dari spesifikasi Web Audio API.

Yang lebih penting untuk tujuan kita, Web Audio API menyediakan cara mudah untuk menerapkan respons impuls ini ke suara kita menggunakan ConvolverNode.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

Lihat juga demo efek ruangan di halaman spesifikasi Web Audio API, serta contoh ini yang memberi Anda kontrol atas pencampuran standar Jazz kering dan basah (diproses melalui konvolver).

Hitung mundur akhir

Jadi, Anda telah mem-build game, mengonfigurasi audio posisional, dan sekarang Anda memiliki sejumlah besar AudioNode dalam grafik, yang semuanya diputar secara bersamaan. Bagus, tetapi masih ada satu hal lagi yang perlu dipertimbangkan:

Karena beberapa suara hanya ditumpuk satu sama lain tanpa normalisasi, Anda mungkin berada dalam situasi saat melampaui batas kemampuan speaker. Seperti gambar yang melampaui batas kanvasnya, suara juga dapat terpotong jika bentuk gelombang melampaui nilai minimum maksimumnya, sehingga menghasilkan distorsi yang berbeda. Gelombang terlihat seperti ini:

Pemangkasan

Berikut adalah contoh nyata dari pemotongan. Bentuk gelombangnya terlihat buruk:

Pemangkasan

Penting untuk mendengarkan distorsi yang keras seperti yang di atas, atau sebaliknya, campuran yang terlalu redup yang memaksa pendengar untuk menaikkan volume. Jika Anda mengalami situasi ini, Anda benar-benar harus memperbaikinya.

Mendeteksi pemangkasan

Dari perspektif teknis, kliping terjadi saat nilai sinyal di saluran mana pun melebihi rentang yang valid, yaitu antara -1 dan 1. Setelah hal ini terdeteksi, sebaiknya berikan masukan visual bahwa hal ini sedang terjadi. Untuk melakukannya dengan andal, masukkan JavaScriptAudioNode ke dalam grafik Anda. Grafik audio akan disiapkan sebagai berikut:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

Dan pemangkasan dapat dideteksi di pengendali processAudio berikut:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

Secara umum, berhati-hatilah agar tidak terlalu sering menggunakan JavaScriptAudioNode karena alasan performa. Dalam hal ini, implementasi alternatif pengukuran dapat mengkueri RealtimeAnalyserNode dalam grafik audio untuk getByteFrequencyData, pada waktu render, seperti yang ditentukan oleh requestAnimationFrame. Pendekatan ini lebih efisien, tetapi melewatkan sebagian besar sinyal (termasuk tempat yang berpotensi terpotong), karena rendering terjadi paling banyak 60 kali per detik, sedangkan sinyal audio berubah jauh lebih cepat.

Karena deteksi klip sangat penting, kemungkinan kita akan melihat node MeterNode Web Audio API bawaan pada masa mendatang.

Mencegah pemangkasan

Dengan menyesuaikan gain pada AudioGainNode master, Anda dapat meredam campuran ke tingkat yang mencegah pemotongan. Namun, dalam praktiknya, karena suara yang diputar dalam game Anda mungkin bergantung pada berbagai faktor, mungkin sulit untuk menentukan nilai gain master yang mencegah pemangkasan untuk semua status. Secara umum, Anda harus menyesuaikan keuntungan untuk mengantisipasi kasus terburuk, tetapi ini lebih merupakan seni daripada sains.

Menambahkan sedikit gula

Kompresor biasa digunakan dalam produksi musik dan game untuk memperlancar sinyal dan mengontrol lonjakan pada sinyal keseluruhan. Fungsi ini tersedia di dunia Web Audio melalui DynamicsCompressorNode, yang dapat disisipkan dalam grafik audio Anda untuk memberikan suara yang lebih keras, lebih kaya, dan lebih penuh, serta membantu pemotongan. Dengan mengutip spesifikasi secara langsung, node ini

Menggunakan kompresi dinamika umumnya merupakan ide yang bagus, terutama dalam setelan game, yang, seperti yang telah dibahas sebelumnya, Anda tidak tahu persis suara apa yang akan diputar dan kapan. Plink dari lab DinahMoe adalah contoh yang bagus untuk hal ini, karena suara yang diputar sepenuhnya bergantung pada Anda dan peserta lain. Kompresor berguna dalam sebagian besar kasus, kecuali beberapa yang jarang terjadi, saat Anda berurusan dengan trek yang dikuasai dengan susah payah dan telah disesuaikan agar terdengar "benar".

Untuk menerapkannya, cukup sertakan DynamicsCompressorNode dalam grafik audio Anda, biasanya sebagai node terakhir sebelum tujuan.:

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Untuk mengetahui detail selengkapnya tentang kompresi dinamika, artikel Wikipedia ini sangat informatif.

Untuk meringkas, dengarkan dengan cermat untuk mendeteksi pemotongan dan mencegahnya dengan menyisipkan node gain master. Kemudian, kencangkan seluruh campuran menggunakan node compressor dinamis. Grafik audio Anda mungkin terlihat seperti ini:

Hasil akhir

Kesimpulan

Hal ini mencakup aspek yang menurut saya paling penting dalam pengembangan audio game menggunakan Web Audio API. Dengan teknik ini, Anda dapat membuat pengalaman audio yang benar-benar menarik langsung di browser. Sebelum mengakhiri, izinkan saya memberi Anda tips khusus browser: pastikan untuk menjeda suara jika tab Anda beralih ke latar belakang menggunakan page visibility API. Jika tidak, Anda akan memberikan pengalaman yang berpotensi menjengkelkan bagi pengguna.

Untuk informasi tambahan tentang Audio Web, lihat artikel memulai pengantar lainnya, dan jika Anda memiliki pertanyaan, lihat apakah pertanyaan tersebut sudah terjawab di FAQ audio web. Terakhir, jika ada pertanyaan tambahan, ajukan pertanyaan di Stack Overflow menggunakan tag web-audio.

Sebelum mengakhiri, izinkan saya memberikan beberapa penggunaan Web Audio API yang luar biasa dalam game sungguhan saat ini: