Web-Audio-API für Spiele
Guter Ton verwandelt ein Spiel. Ein stummes Spiel wirkt leblos. Die Web-Audio-API gibt dir Soundeffekte mit geringer Latenz, Musikwiedergabe und sogar räumlichen 3D-Klang, alles im Browser.
Probier es aus (klick einen Button, um synthetisierte Klänge zu hören):
Das Autoplay-Problem
Bevor du irgendetwas machst, musst du die Autoplay-Beschränkungen verstehen. Browser blockieren Audio, bis der Nutzer mit deiner Seite interagiert. Das ist gut so (niemand will, dass Webseiten beim Laden losdröhnen), heißt aber, dass du Audio als Reaktion auf einen Klick, Tipp oder Tastendruck initialisieren musst.
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 })Versuchst du vorher etwas abzuspielen, scheitert es still oder der AudioContext bleibt suspendiert. Das erwischt viele Entwickler kalt.
Sounds laden
Audiodateien müssen vor der Wiedergabe dekodiert werden. Datei holen, dekodieren und den Buffer zur Wiederverwendung behalten:
async function loadSound(url) {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
return audioBuffer
}Das Dekodieren dauert, lade also alle Sounds während eines Ladebildschirms und nicht erst, wenn du sie brauchst.
Sounds abspielen
Zum Abspielen erzeugst du eine Buffer-Quelle, verbindest sie über einen Gain-Knoten zur Lautstärkeregelung und startest sie:
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
}Buffer-Quellen sind Einmalgebrauch. Ist eine Quelle durchgelaufen, kannst du sie nicht neu starten. Erzeuge bei jedem Abspielen eine neue. Das fühlt sich verschwenderisch an, ist aber die Arbeitsweise der API, und Browser optimieren dafür.
Ein Sound-Manager
Für ein echtes Spiel packst du alles in eine Manager-Klasse:
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
}
}Der Master-Gain-Knoten erlaubt dir einen globalen Lautstärkeregler. Alle Sounds laufen durch ihn.
Hintergrundmusik
Musik braucht Schleifen und die Möglichkeit zu stoppen oder auszublenden:
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)
}
}Die Methode linearRampToValueAtTime gibt dir weiche Blenden statt abrupter Schnitte.
Räumlicher Klang
Für 3D-Spiele kannst du Klänge im Raum positionieren. Sie werden leiser, je weiter sie sich vom Hörer entfernen:
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
}Ruf updateListener in jedem Frame mit der Kamera- oder Spielerposition auf. Der Panner passt Lautstärke und Stereopanorama automatisch nach Entfernung und Richtung an.
Schnellfeuer-Sounds
Bei Klängen, die vielfach pro Sekunde spielen (Schüsse, Schritte), sollen sich überlappende Instanzen nicht stapeln. Ein einfaches Pool-Muster hilft:
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
}
}Der Pool begrenzt, wie viele Instanzen desselben Klangs gleichzeitig laufen. Ist das Limit erreicht, wird die älteste ersetzt.
Audioformate
Verschiedene Formate passen zu verschiedenen Situationen:
MP3 läuft überall und eignet sich für Musik. OGG Vorbis liefert bei gleicher Dateigröße bessere Qualität. Safari war lange der Nachzügler, hat native Ogg-Vorbis-Wiedergabe aber in Safari 18.4 ergänzt, alle aktuellen großen Browser unterstützen es also. Einen MP3-Rückfall willst du trotzdem, wenn dir ältere Safari-Versionen wichtig sind. AAC/M4A funktioniert gut auf Apple-Geräten. WAV ist unkomprimiert und groß, dekodiert aber sofort, was für kurze Soundeffekte gut ist, wenn du dir die Dateigröße leisten kannst. WebM/Opus hat das beste Verhältnis von Qualität zu Größe, funktioniert aber nur in modernen Browsern.
Für maximale Kompatibilität bietest du Rückfallwege an:
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'Nützliche Kniffe
Tonhöhenvariation lässt wiederholte Klänge weniger mechanisch wirken:
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)
}Jeder Schuss und jeder Schritt klingt leicht anders.
Ducking senkt die Musiklautstärke bei Dialogen oder wichtigen Klängen:
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)
}Stolperfallen unter iOS
iOS hat strengere Audio-Beschränkungen als andere Plattformen. Der AudioContext muss während einer Nutzergeste erzeugt und fortgesetzt werden. Manche iOS-Versionen verlangen außerdem, dass du während der Geste einen Klang abspielst, selbst einen stummen, und nicht nur den Kontext erzeugst. Wenn Audio auf dem Desktop läuft, unter iOS aber nicht, liegt es wahrscheinlich daran.
Mehr Ressourcen
Ein Web-Spiel ausliefern, das schnell lädt behandelt Asset-Kompression, auch für Audio.
Mobilfreundliche Web-Spiele geht ausführlicher auf iOS-spezifische Probleme ein.
PWA für Offline-Spiele zeigt, wie du Audiodateien fürs Offline-Spielen cachst.
Tone.js für Spiel-Audio behandelt prozedurale Klangerzeugung und Musiksysteme mit einer Bibliothek auf höherer Ebene.
Wo du kostenlose Game-Assets findest listet kostenlose Quellen für Soundeffekte und Musik wie Freesound und Poly Haven.
Externe Ressourcen
- MDN: Web Audio API — vollständige API-Referenz
- MDN: AudioContext — Audio-Kontexte erzeugen und verwalten
- Web Audio API spec — W3C-Spezifikation