Skip to content

Tone.js untuk Audio Game

Tone.js berjalan di atas Web Audio API dan membuat audio sintetis lebih praktis. Alih-alih memuat berkas audio, Anda menghasilkan suara secara terprogram. Ini berarti tanpa waktu pemuatan, variasi tak terbatas, dan suara yang dapat bereaksi terhadap gameplay secara real-time.

Cobalah (klik tombol untuk mendengarkan suara game sintetis):

Mengapa Memilih Tone.js daripada Berkas Audio

Berkas audio cocok untuk musik dan suara yang kompleks. Namun, untuk efek suara game, sintesis memiliki keunggulan nyata. Anda dapat memvariasikan nada dan tempo setiap kali suara diputar agar tidak terasa repetitif. Anda dapat menghasilkan suara secara langsung berdasarkan kondisi game. Selain itu, Anda sepenuhnya menghindari permintaan jaringan.

Kekurangannya, audio sintetis menggunakan lebih banyak CPU daripada memutar berkas. Untuk game sederhana, hal ini bukan masalah. Untuk game yang kompleks, Anda dapat menggabungkan kedua pendekatan tersebut.

Penyiapan

Muat Tone.js dari CDN atau npm:

html
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>

Atau dengan npm:

bash
npm install tone
js
import * as Tone from 'tone'

Masalah Autoplay

Browser memblokir audio hingga pengguna berinteraksi. Tone.js menangani hal ini, tetapi Anda perlu memanggil Tone.start() saat pengguna melakukan gestur:

js
document.addEventListener('click', async () => {
  await Tone.start()
  console.log('Audio siap')
}, { once: true })

Tanpa ini, AudioContext akan tetap ditangguhkan dan tidak ada suara yang diputar.

Synth Pertama Anda

Synth menghasilkan suara. Yang paling sederhana adalah Tone.Synth:

js
const synth = new Tone.Synth().toDestination()

// Putar sebuah nada
synth.triggerAttackRelease('C4', '8n')

triggerAttackRelease memainkan nada selama durasi tertentu. 'C4' adalah nada C tengah. '8n' adalah not seperdelapan pada tempo saat ini.

Synth memiliki osilator (sumber suara) dan envelope (cara suara muncul dan menghilang). Anda dapat menyesuaikan keduanya:

js
const synth = new Tone.Synth({
  oscillator: { type: 'triangle' },
  envelope: {
    attack: 0.02,
    decay: 0.2,
    sustain: 0.3,
    release: 0.4,
  },
}).toDestination()

Jenis osilator mencakup sine (murni, lembut), triangle (halus), sawtooth (berdengung), dan square (berongga, retro).

Membuat Sistem Efek Suara

Untuk game, Anda memerlukan beberapa synth untuk berbagai jenis suara. Berikut pola yang bekerja dengan baik:

js
let isInitialized = false
let softSynth = null
let subSynth = null
let noiseSynth = null
let sfxGain = null

function initSFX() {
  if (isInitialized) return
  
  // Kontrol volume utama
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // Synth lembut untuk suara melodis
  softSynth = new Tone.PolySynth(Tone.Synth, {
    maxPolyphony: 6,
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.2,
      sustain: 0.3,
      release: 0.4,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Sub-bass untuk benturan yang dalam
  subSynth = new Tone.Synth({
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.3,
      sustain: 0.1,
      release: 0.3,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Noise untuk tekstur
  noiseSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.02,
      decay: 0.15,
      sustain: 0,
      release: 0.15,
    },
    volume: -20,
  }).connect(sfxGain)
  
  isInitialized = true
}

PolySynth memungkinkan Anda memainkan beberapa nada sekaligus. Synth biasa bersifat monofonik.

Contoh Suara Game

Berikut beberapa suara game umum dan cara membuatnya:

Mengambil koin - rangkaian nada yang naik terasa memuaskan:

js
function coinCollect() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('C4', '8n', now, 0.3)
  softSynth.triggerAttackRelease('E4', '8n', now + 0.1, 0.35)
  softSynth.triggerAttackRelease('G4', '4n', now + 0.2, 0.4)
}

Melompat - sapuan nada naik yang cepat:

js
function jump() {
  const synth = new Tone.Synth({
    oscillator: { type: 'square' },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0, release: 0.1 },
  }).toDestination()
  
  const now = Tone.now()
  synth.frequency.setValueAtTime(150, now)
  synth.frequency.exponentialRampToValueAtTime(400, now + 0.1)
  synth.triggerAttackRelease('C4', '8n', now, 0.2)
}

Terkena serangan/kerusakan - nada menurun dengan noise:

js
function hit() {
  const now = Tone.now()
  subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
  noiseSynth.triggerAttackRelease('4n', now, 0.25)
}

Kematian - nada-nada sedih yang menurun:

js
function death() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('E4', '4n', now, 0.4)
  softSynth.triggerAttackRelease('C4', '4n', now + 0.2, 0.35)
  softSynth.triggerAttackRelease('A3', '4n', now + 0.4, 0.3)
}

Menambahkan Efek

Efek membuat suara menjadi lebih kaya. Rangkai efek di antara synth dan destination:

js
const filter = new Tone.Filter({
  frequency: 3000,
  type: 'lowpass',
  rolloff: -12,
})

const reverb = new Tone.Reverb({
  decay: 1.2,
  wet: 0.25,
})

const gain = new Tone.Gain(0.7)

// Rangkaian: synth -> filter -> reverb -> gain -> destination
filter.connect(reverb)
reverb.connect(gain)
gain.toDestination()

const synth = new Tone.Synth().connect(filter)

Filter lowpass memangkas frekuensi tinggi yang tajam. Reverb menambahkan kesan ruang. Gain mengontrol volume.

Pembatasan Frekuensi Pemutaran

Jika game Anda memicu banyak suara dengan cepat, suara-suara tersebut dapat tumpang tindih dan menjadi kacau. Tambahkan pembatasan frekuensi pemutaran:

js
const MIN_INTERVAL_MS = 35
let lastNoteTime = 0

function canPlayNote() {
  const now = performance.now()
  if (now - lastNoteTime < MIN_INTERVAL_MS) {
    return false
  }
  lastNoteTime = now
  return true
}

function playSound() {
  if (!canPlayNote()) return
  synth.triggerAttackRelease('C4', '8n')
}

Musik Latar Dinamis

Untuk musik yang bereaksi terhadap gameplay, gunakan sistem berbasis ketukan:

js
const BPM = 140
const BEAT_MS = (60 / BPM) * 1000

let bassSynth = null
let kickDrum = null
let beatInterval = null
let beatCount = 0
let energy = 0.3

const PROGRESSION = [
  { root: 'E2', chord: ['E3', 'G3', 'B3'] },
  { root: 'C2', chord: ['C3', 'E3', 'G3'] },
  { root: 'D2', chord: ['D3', 'F#3', 'A3'] },
  { root: 'A1', chord: ['A2', 'C3', 'E3'] },
]

function playBeat() {
  const beatInMeasure = beatCount % 8
  const chordIndex = Math.floor(beatCount / 8) % PROGRESSION.length
  const chord = PROGRESSION[chordIndex]
  const now = Tone.now()
  
  // Bass pada ketukan pertama
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // Kick drum - hanya saat energi tinggi
  if (beatInMeasure === 0 && energy > 0.3) {
    kickDrum.triggerAttackRelease('C1', '8n', now, 0.4)
  }
  
  beatCount++
}

function startMusic() {
  beatInterval = setInterval(playBeat, BEAT_MS)
}

function stopMusic() {
  clearInterval(beatInterval)
}

// Peristiwa dalam game memengaruhi energi
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}

Intensitas musik berubah berdasarkan apa yang terjadi di dalam game.

Drum dengan MembraneSynth dan NoiseSynth

Untuk perkusi, gunakan synth khusus:

js
// Kick drum
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 4,
  envelope: {
    attack: 0.001,
    decay: 0.2,
    sustain: 0,
    release: 0.1,
  },
  volume: -14,
}).toDestination()

kick.triggerAttackRelease('C1', '8n')

// Snare
const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.002,
    decay: 0.15,
    sustain: 0,
    release: 0.1,
  },
  volume: -12,
}).toDestination()

snare.triggerAttackRelease('8n')

// Hi-hat
const hihat = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.001,
    decay: 0.04,
    sustain: 0,
    release: 0.03,
  },
  volume: -22,
}).toDestination()

hihat.triggerAttackRelease('32n')

Pembersihan

Buang synth saat Anda selesai menggunakannya untuk membebaskan sumber daya:

js
function cleanup() {
  if (softSynth) softSynth.dispose()
  if (subSynth) subSynth.dispose()
  if (noiseSynth) noiseSynth.dispose()
  if (sfxGain) sfxGain.dispose()
}

Kesalahan Umum

Tidak menunggu Tone.start() - Audio tidak akan diputar hingga context dimulai melalui interaksi pengguna.

Membuat synth setiap kali suara diputar - Buat synth sekali saat inisialisasi, lalu gunakan kembali. Membuat synth baru setiap kali akan menyebabkan kebocoran memori.

Terlalu banyak suara yang tumpang tindih - Gunakan pembatasan frekuensi pemutaran dan deduplikasi untuk mencegah audio terdengar kacau.

Frekuensi yang tajam - Gunakan filter lowpass dan jenis osilator yang lebih lembut (sine, triangle) agar suara tidak mengganggu pemain.

Referensi Lainnya

Web Audio API untuk game membahas API yang mendasarinya serta cara memuat berkas audio.

Dokumentasi Tone.js menyediakan referensi API lengkap.

Tempat Menemukan Aset Game Gratis mencakup penyedia audio dan efek suara seperti Freesound.

Game web yang ramah perangkat seluler membahas pembatasan audio iOS dan pola sentuh-untuk-membuka-kunci.