Tone.js für Spiel-Audio
Tone.js sitzt auf der Web-Audio-API und macht synthetisiertes Audio praktikabel. Statt Audiodateien zu laden, erzeugst du Klänge programmatisch. Das heißt null Ladezeit, unendliche Variationen und Klänge, die in Echtzeit aufs Gameplay reagieren.
Probier es aus (klick die Buttons, um synthetisierte Spielklänge zu hören):
Warum Tone.js statt Audiodateien
Für Musik und komplexe Klänge funktionieren Audiodateien gut. Bei Spiel-Soundeffekten hat Synthese aber echte Vorteile. Du kannst Tonhöhe und Timing bei jedem Abspielen variieren, damit Klänge nicht repetitiv wirken. Du kannst Klänge spontan aus dem Spielzustand erzeugen. Und du sparst dir die Netzwerkanfragen komplett.
Der Nachteil ist, dass synthetisiertes Audio mehr CPU kostet als das Abspielen einer Datei. Für einfache Spiele ist das kein Problem. Für komplexe Spiele mischst du vielleicht beide Ansätze.
Einrichtung
Lade Tone.js von einem CDN oder über npm:
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>Oder mit npm:
npm install toneimport * as Tone from 'tone'Das Autoplay-Problem
Browser blockieren Audio bis zur Nutzerinteraktion. Tone.js kümmert sich darum, du musst aber Tone.start() bei einer Nutzergeste aufrufen:
document.addEventListener('click', async () => {
await Tone.start()
console.log('Audio ready')
}, { once: true })Ohne das bleibt dein AudioContext suspendiert und nichts spielt.
Dein erster Synth
Ein Synth erzeugt Klang. Der einfachste ist Tone.Synth:
const synth = new Tone.Synth().toDestination()
// Play a note
synth.triggerAttackRelease('C4', '8n')triggerAttackRelease spielt eine Note für eine Dauer. 'C4' ist das mittlere C. '8n' ist eine Achtelnote im aktuellen Tempo.
Der Synth hat einen Oszillator (die Klangquelle) und eine Hüllkurve (wie der Klang ein- und ausblendet). Beides kannst du anpassen:
const synth = new Tone.Synth({
oscillator: { type: 'triangle' },
envelope: {
attack: 0.02,
decay: 0.2,
sustain: 0.3,
release: 0.4,
},
}).toDestination()Oszillatortypen sind unter anderem sine (rein, weich), triangle (sanft), sawtooth (schnarrend) und square (hohl, retro).
Ein System für Soundeffekte bauen
Für ein Spiel willst du mehrere Synths für verschiedene Klangarten. Dieses Muster funktioniert gut:
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 lässt dich mehrere Noten gleichzeitig spielen. Der normale Synth ist monophon.
Beispiele für Spielklänge
Hier ein paar gängige Spielklänge und wie du sie erzeugst:
Münze einsammeln — aufsteigende Noten fühlen sich belohnend an:
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)
}Sprung — schneller Aufwärtsschwung:
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)
}Treffer und Schaden — absteigend mit Rauschen:
function hit() {
const now = Tone.now()
subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
noiseSynth.triggerAttackRelease('4n', now, 0.25)
}Tod — traurige absteigende Noten:
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)
}Effekte ergänzen
Effekte machen Klänge reicher. Häng sie zwischen Synth und Ausgang:
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)Der Tiefpassfilter schneidet harte hohe Frequenzen. Der Hall gibt Raum. Gain regelt die Lautstärke.
Ratenbegrenzung
Löst dein Spiel viele Klänge in schneller Folge aus, können sie sich überlagern und zu Matsch werden. Ergänze eine Ratenbegrenzung:
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')
}Dynamische Hintergrundmusik
Für Musik, die aufs Gameplay reagiert, nimm ein taktbasiertes System:
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)
}Die Intensität der Musik ändert sich je nachdem, was im Spiel passiert.
Schlagzeug mit MembraneSynth und NoiseSynth
Für Perkussion nimmst du spezialisierte Synths:
// 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')Aufräumen
Verwirf Synths, wenn du fertig bist, um Ressourcen freizugeben:
function cleanup() {
if (softSynth) softSynth.dispose()
if (subSynth) subSynth.dispose()
if (noiseSynth) noiseSynth.dispose()
if (sfxGain) sfxGain.dispose()
}Typische Fehler
Nicht auf Tone.start() warten — Audio spielt nicht, bevor der Kontext bei einer Nutzerinteraktion gestartet wurde.
Für jeden Klang neue Synths erzeugen — Erzeuge Synths einmal beim Init und nutz sie wieder. Jedes Mal neue zu erzeugen leckt Speicher.
Zu viele überlappende Klänge — Nutz Ratenbegrenzung und Entdopplung, damit kein Klangmatsch entsteht.
Harte Frequenzen — Nutz Tiefpassfilter und weichere Oszillatortypen (sine, triangle) für Klänge, die Spieler nicht nerven.
Mehr Ressourcen
Web-Audio-API für Spiele behandelt die darunterliegende API und das Laden von Audiodateien.
Die Dokumentation von Tone.js enthält die vollständige API-Referenz.
Wo du kostenlose Game-Assets findest nennt Anbieter für Audio und Soundeffekte wie Freesound.
Mobilfreundliche Web-Spiele behandelt iOS-Audiobeschränkungen und Muster zum Freischalten per Touch.