Skip to content

Web Audio API untuk Game

Audio yang bagus dapat mengubah sebuah game. Game tanpa suara terasa tidak hidup. Web Audio API menyediakan efek suara berlatensi rendah, pemutaran musik, dan bahkan audio spasial 3D, semuanya langsung di browser.

Cobalah (klik tombol untuk mendengar suara yang disintesis):

Masalah Autoplay

Sebelum melakukan hal lain, Anda perlu memahami pembatasan autoplay. Browser memblokir audio hingga pengguna berinteraksi dengan halaman Anda. Ini merupakan hal yang baik (tidak ada yang ingin situs web tiba-tiba memutar suara saat dimuat), tetapi artinya Anda harus menginisialisasi audio sebagai respons terhadap klik, ketukan, atau penekanan tombol.

js
let audioCtx = null

function initAudio() {
  if (!audioCtx) {
    audioCtx = new AudioContext()
  }
  if (audioCtx.state === 'suspended') {
    audioCtx.resume()
  }
  return audioCtx
}

// Inisialisasi pada interaksi pertama
document.addEventListener('click', () => initAudio(), { once: true })
document.addEventListener('keydown', () => initAudio(), { once: true })

Jika Anda mencoba memutar audio sebelum ini, prosesnya akan gagal tanpa pesan atau AudioContext tetap ditangguhkan. Hal ini sering membuat banyak developer lengah.

Memuat Suara

File audio perlu didekode sebelum diputar. Ambil file, dekode, lalu simpan buffer agar dapat digunakan kembali:

js
async function loadSound(url) {
  const response = await fetch(url)
  const arrayBuffer = await response.arrayBuffer()
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
  return audioBuffer
}

Proses decoding membutuhkan waktu, jadi muat semua suara Anda selama layar pemuatan, bukan ketika suara tersebut perlu diputar.

Memutar Suara

Untuk memutar suara, buat sumber buffer, hubungkan melalui gain node untuk mengontrol volume, lalu mulai pemutarannya:

js
function playSound(buffer, volume = 1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const gainNode = audioCtx.createGain()
  gainNode.gain.value = volume
  
  source.connect(gainNode)
  gainNode.connect(audioCtx.destination)
  
  source.start(0)
  return source
}

Sumber buffer hanya dapat digunakan sekali. Setelah sumber selesai diputar, Anda tidak dapat memulainya kembali. Buat sumber baru setiap kali Anda memutar suara. Ini mungkin terasa boros, tetapi begitulah cara kerja API ini, dan browser telah mengoptimalkannya.

Sound Manager

Untuk game sungguhan, bungkus semuanya dalam sebuah kelas pengelola:

js
class SoundManager {
  constructor() {
    this.ctx = null
    this.sounds = new Map()
    this.masterGain = null
  }
  
  init() {
    this.ctx = new AudioContext()
    this.masterGain = this.ctx.createGain()
    this.masterGain.connect(this.ctx.destination)
  }
  
  async load(name, url) {
    const response = await fetch(url)
    const buffer = await this.ctx.decodeAudioData(await response.arrayBuffer())
    this.sounds.set(name, buffer)
  }
  
  play(name, volume = 1) {
    const buffer = this.sounds.get(name)
    if (!buffer) return
    
    const source = this.ctx.createBufferSource()
    source.buffer = buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.masterGain)
    source.start(0)
    
    return source
  }
  
  setMasterVolume(v) {
    this.masterGain.gain.value = v
  }
}

Master gain node memungkinkan Anda menerapkan slider volume global. Semua suara dirutekan melaluinya.

Musik Latar

Musik memerlukan perulangan serta kemampuan untuk dihentikan atau dipudarkan:

js
class MusicPlayer {
  constructor(ctx, masterGain) {
    this.ctx = ctx
    this.masterGain = masterGain
    this.currentTrack = null
    this.gain = ctx.createGain()
    this.gain.connect(masterGain)
  }
  
  play(buffer, volume = 0.5) {
    this.stop()
    
    this.currentTrack = this.ctx.createBufferSource()
    this.currentTrack.buffer = buffer
    this.currentTrack.loop = true
    this.currentTrack.connect(this.gain)
    this.gain.gain.value = volume
    this.currentTrack.start(0)
  }
  
  stop() {
    if (this.currentTrack) {
      this.currentTrack.stop()
      this.currentTrack = null
    }
  }
  
  fadeOut(duration = 1) {
    this.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + duration)
  }
}

Metode linearRampToValueAtTime menghasilkan transisi volume yang mulus alih-alih terputus secara mendadak.

Audio Spasial

Untuk game 3D, Anda dapat memosisikan suara di dalam ruang. Suara menjadi lebih pelan saat menjauh dari pendengar:

js
function playSpatialSound(buffer, x, y, z) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const panner = audioCtx.createPanner()
  panner.panningModel = 'HRTF'
  panner.distanceModel = 'inverse'
  panner.refDistance = 1
  panner.maxDistance = 100
  panner.positionX.value = x
  panner.positionY.value = y
  panner.positionZ.value = z
  
  source.connect(panner)
  panner.connect(audioCtx.destination)
  source.start(0)
}

function updateListener(x, y, z, fx, fy, fz) {
  const listener = audioCtx.listener
  listener.positionX.value = x
  listener.positionY.value = y
  listener.positionZ.value = z
  listener.forwardX.value = fx
  listener.forwardY.value = fy
  listener.forwardZ.value = fz
  listener.upX.value = 0
  listener.upY.value = 1
  listener.upZ.value = 0
}

Panggil updateListener pada setiap frame menggunakan posisi kamera atau pemain. Panner secara otomatis menyesuaikan volume dan panning stereo berdasarkan jarak dan arah.

Suara Tembakan Cepat

Untuk suara yang diputar berkali-kali per detik (tembakan, langkah kaki), Anda tentu tidak ingin instance yang saling tumpang tindih terus menumpuk. Pola pool sederhana dapat membantu:

js
class SoundPool {
  constructor(ctx, buffer, size = 8) {
    this.sources = []
    this.index = 0
    this.ctx = ctx
    this.buffer = buffer
    this.size = size
  }
  
  play(volume = 1) {
    const source = this.ctx.createBufferSource()
    source.buffer = this.buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.ctx.destination)
    source.start(0)
    
    this.index = (this.index + 1) % this.size
  }
}

Pool membatasi jumlah instance dari suara yang sama yang dapat diputar secara bersamaan. Ketika batas tercapai, instance tertua akan digantikan.

Format Audio

Format yang berbeda lebih cocok untuk situasi yang berbeda:

MP3 dapat digunakan di mana saja dan cocok untuk musik. OGG Vorbis menawarkan kualitas lebih baik dengan ukuran file yang sama. Safari sebelumnya menjadi browser utama terakhir yang belum mendukungnya, tetapi dukungan pemutaran native Ogg Vorbis telah ditambahkan di Safari 18.4, sehingga kini semua browser utama versi terbaru mendukungnya. Anda tetap sebaiknya menyediakan MP3 sebagai fallback jika ingin mendukung Safari versi lama. AAC/M4A berfungsi dengan baik di perangkat Apple. WAV tidak terkompresi dan berukuran besar, tetapi dapat didekode seketika, sehingga cocok untuk efek suara pendek jika ukuran file bukan masalah. WebM/Opus menawarkan rasio kualitas terhadap ukuran terbaik, tetapi hanya berfungsi di browser modern.

Untuk kompatibilitas maksimal, sediakan fallback:

js
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'

Trik Berguna

Variasi pitch membuat suara yang berulang terasa tidak terlalu robotis:

js
function playWithPitchVariation(buffer, variance = 0.1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  source.playbackRate.value = 1 + (Math.random() - 0.5) * variance
  source.connect(audioCtx.destination)
  source.start(0)
}

Setiap suara tembakan atau langkah kaki akan terdengar sedikit berbeda.

Ducking menurunkan volume musik selama dialog atau saat suara penting diputar:

js
function duckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + duration)
}

function unduckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + duration)
}

Hal yang Perlu Diwaspadai di iOS

iOS memiliki pembatasan audio yang lebih ketat dibandingkan platform lain. AudioContext harus dibuat dan dilanjutkan selama gestur pengguna. Beberapa versi iOS juga mengharuskan Anda memutar suara (bahkan suara hening) selama gestur tersebut, bukan sekadar membuat context. Jika audio berfungsi di desktop tetapi tidak di iOS, kemungkinan besar inilah penyebabnya.

Referensi Lainnya

Rilis game web yang dimuat dengan cepat membahas kompresi aset, termasuk audio.

Game web yang ramah perangkat seluler membahas lebih lanjut masalah khusus iOS.

PWA untuk game offline menunjukkan cara menyimpan file audio dalam cache agar dapat dimainkan secara offline.

Tone.js untuk audio game membahas pembuatan suara prosedural dan sistem musik menggunakan library tingkat tinggi.

Tempat Menemukan Aset Game Gratis mencantumkan sumber efek suara dan musik gratis seperti Freesound dan Poly Haven.

Referensi Eksternal