गेम्स के लिए Web Audio API
अच्छा ऑडियो एक गेम को बदल देता है। एक खामोश गेम बेजान लगता है। Web Audio API आपको कम-लेटेंसी साउंड इफेक्ट, म्यूज़िक प्लेबैक और यहां तक कि 3D स्पेशियल ऑडियो देता है, वह भी सीधे browser में।
इसे आज़माएं (सिंथेसाइज़्ड आवाज़ें सुनने के लिए किसी बटन पर क्लिक करें):
ऑटोप्ले की समस्या
सबसे पहले, आपको ऑटोप्ले की पाबंदियों को समझना होगा। जब तक यूज़र आपके पेज से इंटरैक्ट न करे, browser ऑडियो को रोक देते हैं। यह अच्छी बात है (कोई नहीं चाहता कि वेबसाइट लोड होते ही आवाज़ें बजने लगें), लेकिन इसका मतलब है कि आपको ऑडियो को किसी क्लिक, टैप या कीप्रेस के जवाब में शुरू करना होगा।
let audioCtx = null
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext()
}
if (audioCtx.state === 'suspended') {
audioCtx.resume()
}
return audioCtx
}
// Initialize on first interaction
document.addEventListener('click', () => initAudio(), { once: true })
document.addEventListener('keydown', () => initAudio(), { once: true })अगर आप इससे पहले ऑडियो चलाने की कोशिश करते हैं, तो यह चुपचाप फेल हो जाता है या AudioContext suspended रह जाता है। यह बहुत सारे डेवलपर्स को चौंका देता है।
आवाज़ें लोड करना
प्लेबैक से पहले ऑडियो फाइलों को डिकोड करना ज़रूरी होता है। फाइल फेच करें, उसे डिकोड करें और दोबारा इस्तेमाल के लिए बफर को संभालकर रखें:
async function loadSound(url) {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
return audioBuffer
}डिकोडिंग में समय लगता है, इसलिए अपनी सारी आवाज़ें लोडिंग स्क्रीन के दौरान लोड करें, न कि तब जब आपको उन्हें बजाना हो।
आवाज़ें बजाना
कोई आवाज़ बजाने के लिए, एक बफर सोर्स बनाएं, उसे वॉल्यूम कंट्रोल के लिए एक gain node से जोड़ें और शुरू करें:
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
}बफर सोर्स वन-शॉट होते हैं। एक बार कोई सोर्स बजना खत्म कर दे, तो आप उसे दोबारा शुरू नहीं कर सकते। हर बार बजाते समय एक नया बनाएं। यह फिज़ूलखर्च लगता है, लेकिन API इसी तरह काम करती है, और browser इसी के लिए ऑप्टिमाइज़ करते हैं।
एक साउंड मैनेजर
असली गेम के लिए, सब कुछ एक मैनेजर क्लास में रैप करें:
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
}
}मास्टर gain node आपको एक ग्लोबल वॉल्यूम स्लाइडर बनाने देता है। सारी आवाज़ें इसी से होकर गुज़रती हैं।
बैकग्राउंड म्यूज़िक
म्यूज़िक को लूपिंग और रुकने या फेड आउट होने की क्षमता चाहिए होती है:
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)
}
}linearRampToValueAtTime मेथड आपको अचानक कट जाने के बजाय स्मूद फेड देता है।
स्पेशियल ऑडियो
3D गेम्स के लिए, आप आवाज़ों को स्पेस में पोज़िशन कर सकते हैं। जैसे-जैसे आवाज़ें लिसनर से दूर जाती हैं, वे धीमी होती जाती हैं:
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
}updateListener को हर फ्रेम पर कैमरा या प्लेयर की पोज़िशन के साथ कॉल करें। panner दूरी और दिशा के आधार पर वॉल्यूम और स्टीरियो पैनिंग को अपने आप एडजस्ट कर देता है।
तेज़ी से बजने वाली आवाज़ें
जो आवाज़ें एक सेकंड में कई बार बजती हैं (गोलीबारी, कदमों की आहट), उनके लिए आप नहीं चाहते कि ओवरलैप होती इंस्टेंस ढेर लगा लें। एक सिंपल पूल पैटर्न मदद करता है:
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
}
}पूल इस बात को सीमित करता है कि एक ही आवाज़ की कितनी इंस्टेंस एक साथ बज सकती हैं। जब आप लिमिट तक पहुंच जाते हैं, तो सबसे पुरानी वाली रिप्लेस हो जाती है।
ऑडियो फॉर्मेट
अलग-अलग हालात में अलग-अलग फॉर्मेट बेहतर काम करते हैं:
MP3 हर जगह काम करता है और म्यूज़िक के लिए अच्छा है। OGG Vorbis उतने ही फाइल साइज़ में बेहतर क्वालिटी देता है। Safari लंबे समय तक पीछे रहा, लेकिन Safari 18.4 में इसने नेटिव Ogg Vorbis प्लेबैक जोड़ दिया, तो अब सभी मौजूदा बड़े browser इसे सपोर्ट करते हैं। अगर आपको पुराने Safari वर्ज़न की परवाह है तो भी आपको एक MP3 fallback रखना चाहिए। AAC/M4A Apple डिवाइस पर अच्छा काम करता है। WAV अनकंप्रेस्ड और बड़ा होता है, लेकिन तुरंत डिकोड हो जाता है, जो छोटे साउंड इफेक्ट के लिए अच्छा है जहां आप फाइल साइज़ झेल सकते हैं। WebM/Opus क्वालिटी-टू-साइज़ का सबसे अच्छा अनुपात देता है लेकिन सिर्फ मॉडर्न browser में चलता है।
ज़्यादा से ज़्यादा कम्पैटिबिलिटी के लिए, fallback दें:
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'काम के तरीके
पिच में बदलाव दोहराई जाने वाली आवाज़ों को कम मशीनी महसूस कराता है:
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)
}हर गोली या कदम की आवाज़ थोड़ी अलग लगती है।
डकिंग डायलॉग या ज़रूरी आवाज़ों के दौरान म्यूज़िक का वॉल्यूम कम कर देती है:
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)
}iOS की दिक्कतें
iOS में दूसरे प्लेटफॉर्म से ज़्यादा सख्त ऑडियो पाबंदियां होती हैं। AudioContext को किसी यूज़र जेस्चर के दौरान ही बनाना और रिज़्यूम करना ज़रूरी है। कुछ iOS वर्ज़न में आपको जेस्चर के दौरान कोई आवाज़ (भले ही खामोश हो) बजाना भी ज़रूरी होता है, सिर्फ कॉन्टेक्स्ट बनाना काफी नहीं। अगर ऑडियो डेस्कटॉप पर चलता है लेकिन iOS पर नहीं, तो शायद यही वजह है।
और संसाधन
ऐसा web game शिप करें जो तेज़ी से लोड हो ऑडियो समेत असेट कंप्रेशन को कवर करता है।
मोबाइल-फ्रेंडली web games में iOS से जुड़ी दिक्कतों पर ज़्यादा जानकारी है।
ऑफलाइन गेम्स के लिए PWA दिखाता है कि ऑफलाइन प्ले के लिए ऑडियो फाइलें कैसे कैश करें।
गेम ऑडियो के लिए Tone.js एक हाई-लेवल लाइब्रेरी से प्रोसीजरल साउंड जेनरेशन और म्यूज़िक सिस्टम को कवर करता है।
मुफ्त गेम असेट कहां मिलें Freesound और Poly Haven जैसे मुफ्त साउंड इफेक्ट और म्यूज़िक के सोर्स की सूची देता है।
बाहरी संसाधन
- MDN: Web Audio API — पूरा API रेफरेंस
- MDN: AudioContext — ऑडियो कॉन्टेक्स्ट बनाना और मैनेज करना
- Web Audio API spec — W3C स्पेसिफिकेशन