Web Audio API pour les jeux
Un bon rendu sonore transforme un jeu. Un jeu silencieux semble sans vie. La Web Audio API permet de produire des effets sonores à faible latence, de lire de la musique et même de créer un son spatial 3D, le tout dans le navigateur.
Essayez-la (cliquez sur un bouton pour écouter des sons synthétisés) :
Le problème de la lecture automatique
Avant toute chose, vous devez comprendre les restrictions liées à la lecture automatique. Les navigateurs bloquent le son jusqu’à ce que l’utilisateur interagisse avec votre page. C’est une bonne chose — personne ne veut qu’un site diffuse soudainement du son au chargement — mais cela signifie que vous devez initialiser l’audio en réponse à un clic, un appui ou une pression sur une touche.
let audioCtx = null
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext()
}
if (audioCtx.state === 'suspended') {
audioCtx.resume()
}
return audioCtx
}
// Initialiser lors de la première interaction
document.addEventListener('click', () => initAudio(), { once: true })
document.addEventListener('keydown', () => initAudio(), { once: true })Si vous essayez de lire du son avant cela, l’opération échoue silencieusement ou l’AudioContext reste suspendu. Ce comportement prend de nombreux développeurs au dépourvu.
Chargement des sons
Les fichiers audio doivent être décodés avant leur lecture. Récupérez le fichier, décodez-le et conservez le tampon pour le réutiliser :
async function loadSound(url) {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
return audioBuffer
}Le décodage prend du temps : chargez donc tous vos sons pendant un écran de chargement, et non au moment où vous devez les lire.
Lecture des sons
Pour lire un son, créez une source de tampon, connectez-la à un nœud de gain pour contrôler le volume, puis lancez-la :
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
}Les sources de tampon sont à usage unique. Une fois la lecture d’une source terminée, vous ne pouvez pas la redémarrer. Créez-en une nouvelle à chaque lecture. Cela peut sembler inefficace, mais c’est ainsi que fonctionne l’API, et les navigateurs sont optimisés pour ce modèle.
Un gestionnaire de sons
Pour un véritable jeu, regroupez toutes ces fonctionnalités dans une classe de gestion :
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
}
}Le nœud de gain principal vous permet d’implémenter un curseur de volume global. Tous les sons passent par ce nœud.
Musique de fond
La musique doit pouvoir être lue en boucle, arrêtée ou atténuée progressivement :
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)
}
}La méthode linearRampToValueAtTime permet d’obtenir des fondus fluides plutôt que des coupures brusques.
Audio spatial
Pour les jeux en 3D, vous pouvez positionner les sons dans l’espace. Leur volume diminue à mesure qu’ils s’éloignent de l’auditeur :
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
}Appelez updateListener à chaque image avec la position de la caméra ou du joueur. Le panoramique ajuste automatiquement le volume et la répartition stéréo en fonction de la distance et de la direction.
Sons joués en rafale
Pour les sons joués plusieurs fois par seconde, comme les tirs ou les bruits de pas, il faut éviter que les instances qui se chevauchent s’accumulent. Un simple système de pool peut vous y aider :
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
}
}Le pool limite le nombre d’instances d’un même son pouvant être lues simultanément. Lorsque la limite est atteinte, l’instance la plus ancienne est remplacée.
Formats audio
Certains formats sont plus adaptés que d’autres selon la situation :
Le MP3 fonctionne partout et convient bien à la musique. OGG Vorbis offre une meilleure qualité à taille de fichier égale. Safari a longtemps été le dernier navigateur à ne pas le prendre en charge, mais la lecture native d’Ogg Vorbis a été ajoutée dans Safari 18.4 : tous les principaux navigateurs actuels le prennent donc désormais en charge. Prévoyez tout de même une solution de repli en MP3 si vous souhaitez prendre en charge les anciennes versions de Safari. AAC/M4A fonctionne bien sur les appareils Apple. WAV est non compressé et volumineux, mais son décodage est instantané, ce qui convient aux effets sonores courts lorsque la taille du fichier n’est pas un problème. WebM/Opus offre le meilleur rapport qualité-taille, mais ne fonctionne que dans les navigateurs modernes.
Pour une compatibilité maximale, prévoyez des formats de repli :
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'Astuces utiles
La variation de hauteur rend les sons répétés moins robotiques :
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)
}Chaque coup de feu ou bruit de pas produit ainsi un son légèrement différent.
Le ducking réduit le volume de la musique pendant les dialogues ou les sons importants :
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)
}Pièges sur iOS
iOS impose des restrictions audio plus strictes que les autres plateformes. L’AudioContext doit être créé et réactivé pendant un geste de l’utilisateur. Certaines versions d’iOS exigent également que vous lisiez un son, même silencieux, pendant ce geste, plutôt que de simplement créer le contexte. Si l’audio fonctionne sur ordinateur, mais pas sur iOS, c’est probablement la raison.
Ressources complémentaires
Publier un jeu web qui se charge rapidement aborde la compression des ressources, y compris des fichiers audio.
Jeux web adaptés aux appareils mobiles présente davantage de détails sur les problèmes propres à iOS.
PWA pour les jeux hors ligne explique comment mettre en cache les fichiers audio pour jouer hors ligne.
Tone.js pour l’audio de jeu traite de la génération procédurale de sons et des systèmes musicaux à l’aide d’une bibliothèque de plus haut niveau.
Où trouver des ressources de jeu gratuites répertorie des sources gratuites d’effets sonores et de musique, comme Freesound et Poly Haven.
Ressources externes
- MDN : Web Audio API — référence complète de l’API
- MDN : AudioContext — création et gestion des contextes audio
- Spécification de la Web Audio API — spécification du W3C