Skip to content

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.

js
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:

js
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:

js
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:

js
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:

js
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:

js
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:

js
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:

js
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'

Nützliche Kniffe

Tonhöhenvariation lässt wiederholte Klänge weniger mechanisch wirken:

js
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:

js
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