Mengembangkan audio game dengan Web Audio API

Pengantar

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

Tak terkecuali game! Memori video {i>game<i} yang paling saya sukai adalah musik dan efek suara. Sekarang, dalam banyak kasus hampir dua dekade setelah memainkan favorit saya, saya masih belum bisa memahami komposisi Zelda dari Koji Kondo dan soundtrack Diablo dari Matt Uelmen. Daya tarik yang sama berlaku untuk efek suara, 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 diri dengan kondisi game yang berpotensi tidak dapat diprediksi yang dimiliki pemain. Dalam praktiknya, bagian-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, akan ada banyak suara yang diputar sekaligus, yang semuanya harus terdengar bagus jika digunakan bersama dan dirender tanpa membawa penalti performa.

Audio Game di Web

Untuk game sederhana, menggunakan tag <audio> mungkin sudah cukup. Namun, banyak browser menyediakan implementasi yang buruk, yang mengakibatkan gangguan audio dan latensi tinggi. Semoga ini masalah sementara, karena vendor sedang bekerja keras untuk memperbaiki implementasinya masing-masing. Untuk melihat sekilas status tag <audio>, ada rangkaian pengujian yang bagus di areweplayingyet.org.

Namun, jika melihat lebih dalam spesifikasi tag <audio>, sudah jelas bahwa ada banyak hal yang tidak dapat dilakukan dengan tag tersebut, dan tidak mengherankan, karena dirancang untuk pemutaran media. Beberapa batasannya meliputi:

  • Tidak ada kemampuan untuk 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 mendetail.

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

Musik latar belakang

Game sering kali memutar musik latar belakang secara berulang.

Hal ini bisa sangat mengganggu jika loop Anda pendek dan dapat diprediksi. Jika pemain terhenti di suatu area atau level, dan contoh yang sama terus diputar di latar belakang, mungkin ada baiknya untuk memudarkan trek secara bertahap untuk mencegah pemain merasa frustrasi lebih lanjut. Strategi lainnya adalah memiliki campuran intensitas yang secara bertahap memudar menjadi satu sama lain, bergantung pada konteks game.

Misalnya, jika pemain berada di zona pertarungan bos yang epik, Anda mungkin akan memiliki beberapa campuran yang memiliki rentang emosi, mulai dari atmosfer, bayangan, hingga intens. Software sintesis musik sering kali memungkinkan Anda mengekspor beberapa mix (dengan panjang yang sama) berdasarkan suatu lagu dengan memilih set trek yang akan digunakan dalam ekspor. Dengan begitu, Anda memiliki konsistensi internal dan menghindari transisi yang mengganggu saat Anda memudar bersilang dari satu trek ke trek lainnya.

Tali Garasi

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

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

Setelah itu, Anda dapat memutar semua sumber ini secara bersamaan dalam satu loop, dan karena panjangnya sama, Web Audio API akan menjamin sumber tersebut akan tetap selaras. Saat karakter semakin mendekati atau semakin jauh dari pertempuran bos terakhir, game dapat memvariasikan nilai perolehan untuk setiap node yang terkait dalam rantai, menggunakan algoritma jumlah perolehan 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 tersebut sangat cocok untuk konten streaming. Sekarang Anda dapat memindahkan konten dari tag <audio> ke konteks Audio Web.

Teknik ini dapat berguna karena tag <audio> dapat berfungsi dengan konten streaming, yang memungkinkan Anda segera memutar musik latar belakang tanpa harus menunggu semuanya didownload. Dengan menyertakan 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 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. Seperti musik latar belakang, efek suara dapat mengganggu dengan sangat cepat. Untuk menghindari hal ini, penting untuk memiliki kumpulan suara yang mirip tetapi berbeda untuk dimainkan. Hal ini dapat bervariasi mulai dari variasi kecil pada sampel langkah kaki hingga variasi yang drastis, seperti yang terlihat dalam seri Warcraft sebagai respons terhadap klik pada unit.

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

Contoh berikut membuat peluru mesin tembak dari beberapa sampel peluru dengan membuat beberapa sumber suara yang pemutaran dilakukan secara bertahap.

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

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

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

Untuk contoh nyata dari teknik ini dalam tindakan, lihat demo Pool Table, yang menggunakan pengambilan sampel acak dan memvariasikan PlaybackRate untuk menghasilkan suara tabrakan bola yang lebih menarik.

Suara posisi 3D

Game sering kali ditempatkan di dunia yang memiliki beberapa properti geometris, baik dalam 2D maupun 3D. Jika ini terjadi, audio dengan posisi stereo dapat sangat meningkatkan imersif pengalaman. Untungnya, Web Audio API dilengkapi dengan fitur audio posisi dengan akselerasi hardware bawaan yang cukup mudah digunakan. Omong-omong, Anda harus memastikan bahwa Anda memiliki speaker stereo (sebaiknya headphone) untuk contoh berikut agar masuk akal.

Pada contoh di atas, ada pemroses (ikon orang) di tengah kanvas, dan mouse memengaruhi posisi sumber (ikon speaker). Contoh di atas adalah contoh sederhana penggunaan AudioPannerNode untuk mencapai efek seperti ini. Ide dasar dari contoh di atas adalah untuk merespons gerakan mouse dengan menyetel posisi sumber audio, sebagai 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-hal yang perlu diketahui tentang perlakuan spasialisasi Audio Web:

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

Lanjutan: kerucut suara

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

Model posisi

Ada satu AudioListener yang dilampirkan ke konteks Web Audio API yang dapat dikonfigurasi dalam ruang melalui posisi dan orientasi. Setiap sumber dapat diteruskan melalui AudioPannerNode yang memisahkan audio input. Node panner memiliki posisi dan orientasi, serta model jarak dan arah.

Model jarak menentukan jumlah penguatan yang bergantung pada kedekatan dengan sumber, sementara model arah dapat dikonfigurasi dengan menentukan kerucut dalam dan luar, yang menentukan jumlah penguatan (biasanya negatif) jika pemroses berada di dalam kerucut dalam, 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 menggeneralisasi dengan mudah ke dimensi ketiga. Untuk contoh suara yang dispasialisasi dalam 3D, lihat sampel posisi ini. Selain posisi, model suara Audio Web juga secara opsional menyertakan kecepatan pergeseran doppler. Contoh ini menunjukkan efek doppler secara lebih mendetail.

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

Efek dan filter ruangan

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

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

Untuk informasi selengkapnya tentang cara membuat respons impuls dari lingkungan tertentu, baca bagian "Penyiapan Rekaman" di bagian Konvolution pada spesifikasi Web Audio API.

Yang lebih penting lagi, untuk tujuan kami, 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 kamar ini di halaman spesifikasi Web Audio API, serta contoh ini yang memberi Anda kontrol atas pencampuran kering (mentah) dan basah (diproses melalui konvolver) dari standar Jazz yang hebat.

Hitung mundur akhir

Anda telah membuat game, mengonfigurasi audio posisi, dan kini Anda memiliki AudioNode dalam jumlah besar dalam grafik, dan semuanya diputar secara bersamaan. Bagus, tetapi masih ada satu hal lagi yang perlu dipertimbangkan:

Karena beberapa suara menumpuk di atas satu sama lain tanpa normalisasi, Anda mungkin berada dalam situasi di mana Anda melebihi batas kemampuan speaker. Seperti gambar yang melebihi batas kanvasnya, suara juga dapat terpotong jika bentuk gelombang melebihi batas maksimumnya, sehingga menghasilkan distorsi yang jelas. Bentuk gelombangnya akan terlihat seperti ini:

Klip

Berikut adalah contoh nyata dari cara kerja penyesuaian nilai. Bentuk gelombangnya terlihat buruk:

Klip

Penting untuk mendengarkan distorsi yang keras seperti di atas, atau sebaliknya, campuran yang terlalu tenang yang memaksa pemroses Anda menaikkan volume. Jika Anda berada dalam situasi ini, Anda benar-benar harus memperbaikinya!

Mendeteksi pengeklipan

Dari perspektif teknis, clipping terjadi ketika 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 terjadi. Agar dapat 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);

Selain itu, penyesuaian nilai dapat dideteksi dalam 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 untuk tidak menggunakan JavaScriptAudioNode secara berlebihan untuk alasan performa. Dalam hal ini, implementasi alternatif pengukuran dapat melakukan polling RealtimeAnalyserNode dalam grafik audio untuk getByteFrequencyData, pada waktu render, seperti yang ditentukan oleh requestAnimationFrame. Pendekatan ini lebih efisien, tetapi meleset sebagian besar sinyal (termasuk tempat yang berpotensi terpotong), karena rendering terjadi maksimal pada 60 kali per detik, sedangkan sinyal audio berubah jauh lebih cepat.

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

Mencegah kliping

Dengan menyesuaikan penguatan pada AudioGainNode master, Anda dapat menundukkan mix Anda ke level yang mencegah kliping. Namun, dalam praktiknya, karena suara yang diputar dalam game mungkin bergantung pada berbagai faktor, mungkin sulit untuk menentukan nilai keuntungan master yang mencegah klip untuk semua status. Secara umum, Anda harus menyesuaikan keuntungan untuk mengantisipasi kasus terburuk, tetapi ini lebih merupakan seni daripada sains.

Tambahkan sedikit gula

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

Menggunakan kompresi dinamika umumnya merupakan ide bagus, terutama dalam setelan game, tempat, seperti yang telah dibahas sebelumnya, Anda tidak tahu persis suara apa yang akan diputar dan kapan. Plink dari lab DinahMoe adalah contoh yang bagus dalam hal ini, karena suara yang diputar kembali sepenuhnya bergantung pada Anda dan peserta lainnya. Kompresor berguna dalam kebanyakan kasus, kecuali di beberapa kasus yang jarang terjadi, di mana Anda harus menangani trek yang telah dikuasai dengan susah payah dan telah disesuaikan agar terdengar "tepat".

Penerapan ini hanya dengan menyertakan DynamicsCompressorNode dalam grafik audio Anda, umumnya 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.

Ringkasnya, perhatikan kliping dan cegah dengan memasukkan node penguatan master. Kemudian mengencangkan seluruh campuran dengan menggunakan node kompresor dinamika. Grafik audio Anda mungkin terlihat seperti ini:

Hasil akhir

Kesimpulan

Itulah yang saya pikir menjadi aspek terpenting dari pengembangan audio game menggunakan Web Audio API. Dengan teknik ini, Anda dapat membangun pengalaman audio yang benar-benar menarik langsung di browser. Sebelum berhenti, izinkan saya memberikan tips khusus browser: pastikan untuk menjeda suara jika tab Anda beralih ke latar belakang menggunakan page visibility API. Jika tidak, Anda akan membuat pengalaman yang mungkin membuat pengguna tidak nyaman.

Untuk informasi tambahan tentang Audio Web, lihat artikel memulai pengantar lainnya, dan jika ada 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 saya undur diri, izinkan saya menyampaikan beberapa penggunaan Web Audio API yang luar biasa dalam game sungguhan hari ini: