Studi Kasus - The Sound of Racer

Pengantar

Racer adalah Eksperimen Chrome multi-perangkat dan multi-perangkat. Game mobil slot bergaya retro yang dimainkan di semua layar. Di ponsel atau tablet, Android atau iOS. Siapa saja dapat bergabung. Tidak ada aplikasi Tanpa download. Hanya web seluler.

Plan8 bersama teman-teman kami di 14islands menciptakan musik dan pengalaman suara yang dinamis berdasarkan komposisi asli oleh Giorgio Moroder. Racer menampilkan suara mesin yang responsif, efek suara balapan, tetapi yang lebih penting, mix musik dinamis yang tersebar di beberapa perangkat saat pembalap bergabung. Speaker ini adalah instalasi multi-speaker yang terdiri dari smartphone.

Menghubungkan beberapa perangkat bersama-sama adalah sesuatu yang telah lama kami mainkan. Kami telah melakukan eksperimen musik dengan suara yang terbagi di perangkat berbeda atau antarperangkat, jadi kami ingin menerapkan ide itu ke Racer.

Lebih spesifik lagi, kami ingin menguji apakah kami dapat membuat trek musik di seluruh perangkat seiring semakin banyaknya orang yang bergabung dalam game ini—mulai dengan drum dan bass, lalu menambahkan gitar dan synthesizer, dan sebagainya. Kami melakukan beberapa demo musik dan mendalami coding. Efek multi speaker sangat bermanfaat. Kami tidak memiliki semua sinkronisasi dengan tepat pada saat ini, tetapi ketika kami mendengar lapisan suara tersebar di seluruh perangkat, kami tahu bahwa kami sedang membeli sesuatu yang baik.

Membuat suara

Google Creative Lab telah menguraikan arah kreatif untuk suara dan musik. Kami ingin menggunakan synthesizer analog untuk membuat efek suara, bukan merekam suara sebenarnya atau menggunakan koleksi suara. Kami juga tahu bahwa dalam kebanyakan kasus, speaker output akan berupa speaker ponsel atau tablet kecil sehingga suara harus dibatasi dalam spektrum frekuensi agar speaker tidak terdistorsi. Ini terbukti cukup sulit. Ketika kami menerima draf musik pertama dari Giorgio, kami merasa lega karena komposisinya bekerja sempurna dengan suara yang kami ciptakan.

Suara mesin

Tantangan terbesar dalam memprogram suara adalah menemukan suara mesin terbaik dan membentuk perilakunya. Lintasan balap menyerupai lintasan F1 atau Nascar, sehingga mobil harus terasa cepat dan meledak. Pada saat yang sama, mobil-mobilnya sangat kecil sehingga suara mesin yang besar tidak akan benar-benar menghubungkan suara dengan visual. Lagi pula, tidak mungkin mesin bergemuruh yang diputar di speaker seluler, jadi kami harus mencari cara lain.

Untuk mendapatkan inspirasi, kami menghubungkan beberapa koleksi synthesizer modular teman kami Jon Ekstrand dan mulai berkreasi. Kami menyukai apa yang kami dengar. Inilah yang terdengar dengan dua osilator, beberapa filter bagus dan LFO.

Perlengkapan analog telah direnovasi dengan sukses besar menggunakan Web Audio API, jadi kami memiliki harapan yang tinggi dan mulai membuat synth sederhana dalam Audio Web. Suara yang dihasilkan akan menjadi suara yang paling responsif, tetapi akan membebani daya pemrosesan perangkat. Kami harus sangat efisien untuk menyimpan semua sumber daya yang kami miliki agar visualisasi berjalan lancar. Jadi, kami mengganti teknik untuk memutar sampel audio.

Sintesis modular untuk inspirasi suara mesin

Ada beberapa teknik yang dapat digunakan untuk membuat suara mesin dari sampel. Pendekatan yang paling umum untuk game konsol adalah memiliki lapisan berisi beberapa suara (semakin baik) mesin pada RPM yang berbeda (dengan beban), lalu crossfade dan crosspitch di antara suara tersebut. Kemudian tambahkan lapisan beberapa suara mesin yang berputar (tanpa beban) pada RPM yang sama serta crossfade dan crosspitch. Crossfading di antara lapisan-lapisan tersebut saat menggeser persneling, jika dilakukan dengan benar, akan terdengar sangat realistis, tetapi hanya jika Anda memiliki banyak file suara. Bidik tidak boleh terlalu lebar atau akan terdengar sangat sintetis. Karena kami harus menghindari waktu pemuatan yang lama, opsi ini tidak bagus untuk kami. Kami mencoba dengan lima atau enam file suara untuk setiap lapisan, tetapi hasilnya mengecewakan. Kami harus menemukan cara dengan lebih sedikit file.

Solusi yang paling efektif terbukti adalah:

  • Satu file suara dengan akselerasi dan perpindahan gigi yang disinkronkan dengan akselerasi visual mobil yang diakhiri dengan loop terprogram pada pitch / RPM tertinggi. Web Audio API sangat baik dalam melakukan loop secara tepat sehingga kami dapat melakukannya tanpa gangguan atau pop-up.
  • Satu file suara dengan perlambatan / mesin berputar ke bawah.
  • Terakhir, ada satu file suara yang memutar suara diam / tidak ada aktivitas dalam satu loop.

Terlihat seperti ini

Gambar suara mesin

Untuk peristiwa sentuh / akselerasi pertama, kita akan memutar file pertama dari awal, dan jika pemutar melepaskan throttle, kita akan menghitung waktu dari tempat kita berada dalam file suara saat rilis sehingga saat throttle muncul lagi, file tersebut akan melompat ke tempat yang tepat dalam file akselerasi setelah file kedua (diputar ke bawah) diputar.

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

Cobalah

Nyalakan mesin lalu tekan tombol "Throttle".

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

Jadi dengan hanya tiga file suara kecil dan mesin yang terdengar bagus, kami memutuskan untuk melanjutkan ke tantangan berikutnya.

Mendapatkan sinkronisasi

Bersama David Lindkvist dari 14 pulau, kami mulai mempelajari lebih dalam untuk membuat perangkat berfungsi dengan sinkronisasi yang sempurna. Teori dasarnya sederhana. Perangkat meminta waktu server, memperhitungkan latensi jaringan, lalu menghitung offset jam lokal.

syncOffset = localTime - serverTime - networkLatency

Dengan offset ini, setiap perangkat yang terhubung berbagi konsep waktu yang sama. Mudah, kan? (Sekali lagi, secara teori.)

Menghitung latensi jaringan

Kita mungkin berasumsi bahwa latensi adalah setengah dari waktu yang diperlukan untuk meminta dan menerima respons dari server:

networkLatency = (receivedTime - sentTime) × 0.5

Masalah dengan asumsi ini adalah perjalanan dua arah ke server tidak selalu simetris, artinya permintaan mungkin memerlukan waktu lebih lama daripada respons atau sebaliknya. Semakin tinggi latensi jaringan, semakin besar dampak dari asimetri ini—yang menyebabkan suara tertunda dan diputar tidak sinkron dengan perangkat lain.

Untungnya otak kita terhubung untuk tidak menyadari jika suara agak tertunda. Penelitian telah menunjukkan bahwa dibutuhkan penundaan 20 hingga 30 milidetik (md) sebelum otak kita menganggap suara sebagai terpisah. Namun, sekitar 12 hingga 15 md, Anda akan mulai “merasakan” efek sinyal yang tertunda meskipun Anda tidak dapat sepenuhnya “mempersepsinya”. Kami telah menyelidiki beberapa protokol sinkronisasi waktu yang mapan, alternatif yang lebih sederhana, dan mencoba menerapkan beberapa di antaranya dalam praktik. Pada akhirnya, berkat infrastruktur latensi rendah Google, kami hanya dapat mengambil sampel burst permintaan dan menggunakan sampel dengan latensi terendah sebagai referensi.

Mengatasi penyimpangan jam

Berhasil! Ada lebih dari 5 perangkat yang memutar musik dalam sinkronisasi yang sempurna—tapi hanya untuk sementara. Setelah diputar selama beberapa menit, perangkat akan terpisah meskipun kami menjadwalkan suara menggunakan waktu konteks Web Audio API yang sangat presisi. Jeda terakumulasi secara lambat, hanya dalam beberapa milidetik pada satu waktu dan pada awalnya tidak terdeteksi, tetapi menyebabkan lapisan musik benar-benar tidak sinkron setelah diputar dalam waktu yang lebih lama. Halo, penyimpangan jam.

Solusinya adalah menyinkronkan ulang setiap beberapa detik, menghitung offset jam baru, dan memasukkannya dengan lancar ke penjadwal audio. Untuk mengurangi risiko perubahan penting pada musik karena jeda jaringan, kami memutuskan untuk memperhalus perubahan tersebut dengan menyimpan histori offset sinkronisasi terbaru dan menghitung rata-rata.

Menjadwalkan lagu dan beralih aransemen

Membuat pengalaman suara interaktif berarti Anda tidak lagi dapat mengontrol kapan bagian lagu akan diputar, karena Anda bergantung pada tindakan pengguna untuk mengubah status saat ini. Kami harus memastikan bahwa kami dapat berganti-ganti aransemen lagu secara tepat waktu, yang berarti penjadwal harus dapat menghitung jumlah yang tersisa dari batang yang sedang diputar sebelum beralih ke aransemen berikutnya. Algoritma kami akan terlihat seperti ini:

  • Client(1) memulai lagu.
  • Client(n) menanyakan klien pertama saat lagu dimulai.
  • Client(n) menghitung titik referensi saat lagu mulai menggunakan konteks Audio Web-nya, dengan mempertimbangkan syncOffset, dan waktu yang telah berlalu sejak konteks audionya dibuat.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) menghitung berapa lama lagu telah diputar menggunakan playDelta. Penjadwal lagu menggunakan ini untuk mengetahui bilah mana dalam aransemen saat ini yang harus diputar berikutnya.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Demi kesehatan, aransemen kami dibatasi agar selalu delapan batang dan memiliki tempo yang sama (detak per menit).

Lihat ke depan

Selalu penting untuk menjadwalkan terlebih dahulu saat menggunakan setTimeout atau setInterval di JavaScript. Hal ini karena jam JavaScript tidak terlalu tepat dan callback terjadwal bisa dengan mudah didistorsi oleh puluhan milidetik atau lebih berdasarkan tata letak, rendering, pembersihan sampah memori, dan XMLHTTPRequests. Dalam kasus ini, kami juga harus memperhitungkan waktu yang diperlukan semua klien untuk menerima peristiwa yang sama melalui jaringan.

Sprite audio

Menggabungkan suara ke dalam satu file merupakan cara terbaik untuk mengurangi permintaan HTTP, baik untuk Audio HTML maupun Web Audio API. Fitur ini juga menjadi cara terbaik untuk memutar suara secara responsif menggunakan objek Audio, karena objek audio baru tidak perlu dimuat sebelum diputar. Sudah ada beberapa penerapan yang baik di luar sana yang kami gunakan sebagai titik awal. Kami telah memperluas sprite kami agar berfungsi dengan andal di iOS dan Android serta menangani beberapa kasus aneh yang menyebabkan perangkat tertidur.

Di Android, elemen Audio akan terus diputar meskipun perangkat Anda disetel ke mode tidur. Dalam mode tidur, eksekusi JavaScript dibatasi untuk menghemat baterai dan Anda tidak dapat mengandalkan requestAnimationFrame, setInterval, atau setTimeout untuk mengaktifkan callback. Ini menjadi masalah karena sprite audio mengandalkan JavaScript untuk terus memeriksa apakah pemutaran harus dihentikan. Lebih parah lagi, dalam beberapa kasus, currentTime elemen Audio tidak diupdate meskipun audio masih diputar.

Lihat implementasi AudioSprite yang kami gunakan di Chrome Racer sebagai penggantian Audio non-Web.

Elemen audio

Saat kami mulai mengerjakan Racer, Chrome untuk Android belum mendukung Web Audio API. Logika penggunaan Audio HTML untuk beberapa perangkat, atau Web Audio API untuk perangkat lainnya, digabungkan dengan output audio lanjutan yang ingin dicapai dibuat untuk beberapa tantangan menarik. Untungnya, ini semua adalah sejarah. Web Audio API diterapkan di Android M28 beta.

  • Masalah penundaan/waktu. Elemen Audio tidak selalu diputar persis saat Anda menyuruhnya diputar. Karena JavaScript merupakan thread tunggal, browser mungkin akan sibuk, sehingga menyebabkan penundaan pemutaran hingga dua detik.
  • Penundaan pemutaran berarti pengulangan yang lancar tidak selalu dapat dilakukan. Di desktop, Anda dapat menggunakan buffering ganda untuk mencapai loop tanpa jeda, tetapi di perangkat seluler hal ini tidak dapat dilakukan karena:
    • Sebagian besar perangkat seluler tidak akan memutar lebih dari satu elemen Audio pada satu waktu.
    • Volume tetap. Android atau iOS tidak mengizinkan Anda mengubah volume objek Audio.
  • Tidak ada pramuat. Pada perangkat seluler, elemen Audio tidak akan mulai memuat sumbernya kecuali pemutaran dimulai di pengendali touchStart.
  • Mencari masalah. Mendapatkan duration atau menyetel currentTime akan gagal kecuali jika server Anda mendukung HTTP Byte-Range. Hati-hati dengan yang satu ini jika Anda sedang membuat sprite audio seperti yang kami lakukan.
  • Basic Auth pada MP3 gagal. Beberapa perangkat gagal memuat file MP3 yang dilindungi oleh Basic Auth, apa pun browser yang Anda gunakan.

Kesimpulan

Kita telah jauh melangkah sejak menekan tombol bisukan sebagai opsi terbaik untuk menangani suara di web, tetapi ini hanyalah permulaan dan audio web akan menjadi pilihan terbaik. Kami hanya sedikit membahas hal yang dapat dilakukan terkait sinkronisasi beberapa perangkat. Kami tidak memiliki kemampuan pemrosesan di ponsel dan tablet untuk mempelajari pemrosesan sinyal dan efek (seperti gema), tetapi seiring meningkatnya performa perangkat, game berbasis web juga akan memanfaatkan fitur tersebut. Ini adalah saat yang menyenangkan untuk terus mendorong kemungkinan suara.