Studi Kasus - The Sound of Racer

Pengantar

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

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

Menghubungkan beberapa perangkat bersama-sama adalah sesuatu yang telah kami coba selama beberapa waktu. Kami telah melakukan eksperimen musik dengan suara yang akan dibagi di perangkat yang berbeda atau berpindah-pindah antarperangkat, sehingga kami ingin menerapkan ide tersebut ke Racer.

Lebih khusus lagi, kami ingin menguji apakah kami dapat membuat trek musik di seluruh perangkat seiring semakin banyak orang yang bergabung ke game—mulai dari drum dan bass, lalu menambahkan gitar dan synth, dan seterusnya. Kami melakukan beberapa demo musik dan mempelajari coding. Efek multi-speaker sangat memuaskan. Saat ini kami belum memiliki semua sinkronisasi, tetapi saat mendengar lapisan suara yang tersebar di seluruh perangkat, kami tahu bahwa kami telah menemukan sesuatu yang bagus.

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 asli atau menggunakan library suara. Kami juga tahu bahwa speaker output, dalam sebagian besar kasus, akan berupa speaker ponsel atau tablet yang kecil sehingga suara harus dibatasi dalam spektrum frekuensi untuk menghindari distorsi speaker. Hal ini terbukti cukup menantang. Saat kami menerima draf musik pertama dari Giorgio, kami merasa lega karena komposisinya cocok dengan suara yang telah kami buat.

Suara mesin

Tantangan terbesar dalam memprogram suara adalah menemukan suara mesin terbaik dan membentuk perilakunya. Jalur balap menyerupai jalur F1 atau Nascar, sehingga mobil harus terasa cepat dan meledak-ledak. Pada saat yang sama, mobilnya sangat kecil sehingga suara mesin yang besar tidak akan benar-benar menghubungkan suara ke visual. Kita tidak bisa memutar suara mesin yang menderu-deru di speaker seluler, jadi kita harus mencari solusi lain.

Untuk mendapatkan inspirasi, kami menghubungkan beberapa koleksi synth modular milik teman kami, Jon Ekstrand, dan mulai bermain-main. Kami menyukai apa yang kami dengar. Berikut adalah suaranya dengan dua osilator, beberapa filter bagus, dan LFO.

Peralatan analog telah didesain ulang dengan sukses besar menggunakan Web Audio API sebelumnya, sehingga kami memiliki harapan besar dan mulai membuat synth sederhana di Web Audio. Suara yang dihasilkan akan menjadi yang paling responsif, tetapi akan membebani daya pemrosesan perangkat. Kami harus sangat efisien untuk menghemat semua resource yang dapat kami gunakan agar visual berjalan lancar. Jadi, kita beralih teknik untuk memutar sampel audio.

Sintetizer modular untuk inspirasi suara mesin

Ada beberapa teknik yang dapat digunakan untuk membuat suara mesin dari sampel. Pendekatan paling umum untuk game konsol adalah memiliki lapisan beberapa suara (semakin banyak semakin baik) dari mesin pada RPM yang berbeda (dengan beban), lalu melakukan crossfade dan crosspitch di antara suara tersebut. Kemudian, tambahkan lapisan beberapa suara mesin yang baru saja berputar (tanpa beban) pada RPM yang sama dan crossfade serta crosspitch di antara keduanya. Crossfade di antara lapisan tersebut saat berpindah gigi, jika dilakukan dengan benar, akan terdengar sangat realistis, tetapi hanya jika Anda memiliki file suara dalam jumlah besar. Crosspitching tidak boleh terlalu lebar atau akan terdengar sangat sintetis. Karena kami harus menghindari waktu pemuatan yang lama, opsi ini tidak cocok untuk kami. Kami mencoba dengan lima atau enam file suara untuk setiap lapisan, tetapi suaranya mengecewakan. Kita 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 berakhir dalam loop terprogram pada nada / RPM tertinggi. Web Audio API sangat mahir melakukan loop dengan tepat sehingga kita dapat melakukannya tanpa gangguan atau pop.
  • Satu file suara dengan deselerasi / putaran mesin turun.
  • Terakhir, satu file suara yang memutar suara diam / tidak ada aktivitas dalam loop.

Tampilannya seperti ini

Grafik suara mesin

Untuk peristiwa sentuh / akselerasi pertama, kita akan memutar file pertama dari awal, dan jika pemain melepaskan throttle, kita akan menghitung waktu dari posisi kita dalam file suara saat rilis sehingga saat throttle diaktifkan lagi, file akan melompat ke tempat yang tepat dalam file akselerasi setelah file kedua (putaran turun) 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 dan 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 14islands, kami mulai mempelajari lebih dalam cara membuat perangkat diputar secara sinkron. 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 memiliki konsep waktu yang sama. Mudah, kan? (Sekali lagi, secara teori.)

Menghitung latensi jaringan

Kita dapat mengasumsikan 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 bolak-balik ke server tidak selalu simetris, yaitu permintaan mungkin memerlukan waktu lebih lama daripada respons atau sebaliknya. Makin tinggi latensi jaringan, makin besar dampak asimetri ini—sehingga menyebabkan suara tertunda dan diputar tidak sinkron dengan perangkat lain.

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

Memerangi penyimpangan clock

Berhasil! Kami memiliki lebih dari 5 perangkat yang memutar pulsa dalam sinkronisasi yang sempurna—tetapi hanya untuk sementara. Setelah diputar selama beberapa menit, perangkat akan menyimpang meskipun kita menjadwalkan suara menggunakan waktu konteks Web Audio API yang sangat akurat. Jeda terakumulasi secara perlahan, hanya beberapa milidetik pada satu waktu dan tidak dapat dideteksi pada awalnya, tetapi menyebabkan lapisan musik benar-benar tidak sinkron setelah diputar dalam jangka waktu yang lebih lama. Halo, clock drift.

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

Menjadwalkan lagu dan beralih pengaturan

Membuat pengalaman suara interaktif berarti Anda tidak lagi mengontrol kapan bagian lagu akan diputar, karena Anda bergantung pada tindakan pengguna untuk mengubah status saat ini. Kita harus memastikan bahwa kita dapat beralih di antara pengaturan dalam lagu secara tepat waktu, yang berarti penjadwal kita harus dapat menghitung berapa banyak bar yang sedang diputar sebelum beralih ke pengaturan berikutnya. Algoritma kita akhirnya terlihat seperti ini:

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

Untuk menjaga kewarasan, kami membatasi pengaturan agar selalu berdurasi delapan bar dan memiliki tempo yang sama (beat per menit).

Melihat ke depan

Anda harus selalu menjadwalkan terlebih dahulu saat menggunakan setTimeout atau setInterval di JavaScript. Hal ini karena jam JavaScript tidak terlalu akurat dan callback terjadwal dapat dengan mudah terdistorsi hingga puluhan milidetik atau lebih oleh tata letak, rendering, pembersihan sampah, dan XMLHTTPRequest. Dalam kasus kami, kami juga harus memperhitungkan waktu yang diperlukan semua klien untuk menerima peristiwa yang sama melalui jaringan.

Sprite audio

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

Di Android, elemen Audio akan terus diputar meskipun Anda menempatkan perangkat ke mode tidur. Dalam mode tidur, eksekusi JavaScript dibatasi untuk menghemat baterai dan Anda tidak dapat mengandalkan requestAnimationFrame, setInterval, atau setTimeout untuk mengaktifkan callback. Hal ini menjadi masalah karena sprite audio mengandalkan JavaScript untuk terus memeriksa apakah pemutaran harus dihentikan. Yang memperburuk masalah, dalam beberapa kasus, currentTime elemen Audio tidak diperbarui meskipun audio masih diputar.

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

Elemen audio

Saat kami mulai mengerjakan Racer, Chrome untuk Android belum mendukung Web Audio API. Logika penggunaan Audio HTML untuk beberapa perangkat, Web Audio API untuk perangkat lainnya, yang dikombinasikan dengan output audio lanjutan yang ingin kami capai, menimbulkan beberapa tantangan yang menarik. Untungnya, sekarang semua sudah berlalu. Web Audio API diimplementasikan di Android M28 beta.

  • Masalah penundaan/waktu. Elemen Audio tidak selalu diputar tepat saat Anda memintanya untuk diputar. Karena JavaScript adalah thread tunggal, browser mungkin sedang sibuk, sehingga menyebabkan penundaan pemutaran hingga dua detik.
  • Penundaan pemutaran berarti pemutaran berulang yang lancar tidak selalu dapat dilakukan. Di desktop, Anda dapat menggunakan buffer ganda untuk mencapai loop yang hampir tanpa celah, tetapi di perangkat seluler, opsi ini tidak tersedia, karena:
    • Sebagian besar perangkat seluler tidak akan memutar lebih dari satu elemen Audio dalam satu waktu.
    • Volume tetap. Android maupun iOS tidak mengizinkan Anda mengubah volume objek Audio.
  • Tidak ada pramuat. Di perangkat seluler, elemen Audio tidak akan mulai memuat sumbernya kecuali jika pemutaran dimulai di pengendali touchStart.
  • Mencari masalah. Mendapatkan duration atau menetapkan currentTime akan gagal kecuali jika server Anda mendukung Rentang Byte HTTP. Perhatikan hal ini jika Anda membuat sprite audio seperti yang kita lakukan.
  • Autentikasi Dasar di MP3 gagal. Beberapa perangkat gagal memuat file MP3 yang dilindungi oleh Basic Auth, apa pun browser yang Anda gunakan.

Kesimpulan

Kita telah melakukan banyak hal sejak menekan tombol bisukan sebagai opsi terbaik untuk menangani suara di web, tetapi ini hanyalah permulaan dan audio web akan segera menjadi sangat populer. Kita baru membahas permukaan dari apa yang dapat dilakukan terkait sinkronisasi beberapa perangkat. Kami tidak memiliki daya pemrosesan di ponsel dan tablet untuk mempelajari pemrosesan sinyal dan efek (seperti reverb), tetapi seiring peningkatan performa perangkat, game berbasis web juga akan memanfaatkan fitur tersebut. Ini adalah waktu yang tepat untuk terus mendorong kemungkinan suara.