Skip to content

Tone.js pour l’audio de jeu

Tone.js s’appuie sur l’API Web Audio et facilite l’utilisation de sons synthétisés. Au lieu de charger des fichiers audio, vous générez les sons par programmation. Vous bénéficiez ainsi d’un temps de chargement nul, de variations infinies et de sons capables de réagir au gameplay en temps réel.

Essayez (cliquez sur les boutons pour écouter des sons de jeu synthétisés) :

Pourquoi utiliser Tone.js plutôt que des fichiers audio

Les fichiers audio conviennent très bien à la musique et aux sons complexes. Mais pour les effets sonores de jeu, la synthèse présente de réels avantages. Vous pouvez faire varier la hauteur et le timing à chaque lecture afin que les sons ne paraissent pas répétitifs. Vous pouvez générer des sons à la volée en fonction de l’état du jeu. Et vous évitez entièrement les requêtes réseau.

L’inconvénient est que l’audio synthétisé sollicite davantage le processeur que la lecture d’un fichier. Ce n’est pas un problème pour les jeux simples. Pour les jeux complexes, vous pouvez combiner les deux approches.

Installation

Chargez Tone.js depuis un CDN ou avec npm :

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

Ou avec npm :

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

Le problème de la lecture automatique

Les navigateurs bloquent l’audio tant que l’utilisateur n’a pas interagi avec la page. Tone.js gère cette contrainte, mais vous devez appeler Tone.start() lors d’une action de l’utilisateur :

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

Sans cela, votre AudioContext reste suspendu et aucun son n’est joué.

Votre premier synthétiseur

Un synthétiseur génère du son. Le plus simple est Tone.Synth :

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

// Play a note
synth.triggerAttackRelease('C4', '8n')

triggerAttackRelease joue une note pendant une durée donnée. 'C4' correspond au do central. '8n' correspond à une croche au tempo actuel.

Le synthétiseur possède un oscillateur (la source sonore) et une enveloppe (la manière dont le son apparaît et disparaît progressivement). Vous pouvez personnaliser les deux :

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

Les types d’oscillateurs comprennent sine (pur, doux), triangle (feutré), sawtooth (bourdonnant) et square (creux, rétro).

Créer un système d’effets sonores

Pour un jeu, vous aurez besoin de plusieurs synthétiseurs adaptés à différents types de sons. Voici une structure qui fonctionne bien :

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

function initSFX() {
  if (isInitialized) return
  
  // Master volume control
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // Soft synth for melodic sounds
  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 for deep impacts
  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 for texture
  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 permet de jouer plusieurs notes simultanément. Un Synth classique est monophonique.

Exemples de sons de jeu

Voici quelques sons courants dans les jeux et la manière de les créer :

Ramassage d’une pièce — des notes ascendantes procurent un sentiment de récompense :

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)
}

Saut — un balayage rapide vers les aigus :

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)
}

Coup/dégâts — une descente accompagnée de bruit :

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

Mort — des notes descendantes et mélancoliques :

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)
}

Ajouter des effets

Les effets enrichissent les sons. Enchaînez-les entre le synthétiseur et la sortie :

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)

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

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

Le filtre passe-bas atténue les hautes fréquences agressives. La réverbération ajoute une sensation d’espace. Le gain contrôle le volume.

Limiter la fréquence de déclenchement

Si votre jeu déclenche rapidement de nombreux sons, ceux-ci peuvent se chevaucher et créer une bouillie sonore. Ajoutez une limitation de fréquence :

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')
}

Musique de fond dynamique

Pour créer une musique qui réagit au gameplay, utilisez un système basé sur le rythme :

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 on beat 1
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // Kick drum - only when energy is high
  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)
}

// Game events affect energy
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}

L’intensité de la musique évolue en fonction de ce qui se passe dans le jeu.

Percussions avec MembraneSynth et NoiseSynth

Pour les percussions, utilisez des synthétiseurs spécialisés :

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')

Nettoyage

Libérez les synthétiseurs lorsque vous n’en avez plus besoin afin de récupérer les ressources :

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

Erreurs courantes

Ne pas attendre Tone.start() — aucun son ne sera joué tant que le contexte n’aura pas été démarré à la suite d’une interaction de l’utilisateur.

Créer des synthétiseurs pour chaque son — créez-les une seule fois lors de l’initialisation, puis réutilisez-les. En créer de nouveaux à chaque fois provoque des fuites de mémoire.

Trop de sons qui se chevauchent — utilisez la limitation de fréquence et la déduplication pour éviter la bouillie sonore.

Fréquences agressives — utilisez des filtres passe-bas et des types d’oscillateurs plus doux (sine, triangle) pour créer des sons qui n’agaceront pas les joueurs.

Ressources supplémentaires

L’API Web Audio pour les jeux présente l’API sous-jacente et le chargement de fichiers audio.

La documentation de Tone.js contient la référence complète de l’API.

Où trouver des ressources de jeu gratuites présente notamment des fournisseurs de sons et d’effets sonores comme Freesound.

Jeux web adaptés aux appareils mobiles aborde les restrictions audio d’iOS et les méthodes de déverrouillage de l’audio par interaction tactile.