Need Game Art? Edit Game Trailers Launch Your Dev Site Sell Your Merch
Need Game Art? Sell Your Merch

Web Audio API Basics for Games

Updated July 2026
The Web Audio API is a browser-native JavaScript interface that gives game developers a full audio processing pipeline. It handles loading, decoding, mixing, effects, spatial positioning, and precisely timed playback, all without plugins, running on a dedicated audio thread that stays smooth even when your game's frame rate drops.

The AudioContext

Everything in the Web Audio API starts with the AudioContext. This object represents the entire audio system for your page: the processing graph, the clock, the connection to the device's audio hardware. You create one AudioContext and keep it for the life of the game. Creating multiple AudioContexts wastes resources and is not supported well across all platforms.

Creating an AudioContext is straightforward: const ctx = new AudioContext();. However, browsers enforce an autoplay policy that starts the context in a "suspended" state until the user interacts with the page. Your game's "Start" or "Play" button handler should call ctx.resume() to activate it. Without this step, no audio will play.

The AudioContext provides several essential properties. ctx.currentTime returns the audio clock time in seconds, running on the audio thread's high-precision timer. ctx.sampleRate tells you the hardware sample rate, typically 44100 or 48000 Hz. ctx.destination is the final output node representing the speakers. Every sound you play must eventually connect to this destination node.

The AudioContext also exposes the factory methods for creating every type of audio node: createBufferSource(), createGain(), createBiquadFilter(), createPanner(), createConvolver(), and others. Newer code can also use the node constructors directly (e.g., new GainNode(ctx)), which is cleaner and allows passing initial parameters.

Loading and Decoding Audio

Game audio starts with loading sound files from your server. The standard pattern uses the Fetch API to download the file as an ArrayBuffer, then passes it to the AudioContext's decodeAudioData() method for decoding into an AudioBuffer.

The decoded AudioBuffer contains raw PCM sample data that the audio hardware can play instantly. Decoding happens asynchronously and can take a few milliseconds for short effects or several hundred milliseconds for longer music tracks. For this reason, you should decode all audio during your game's loading screen, not during gameplay.

A practical loading function looks like this: fetch the file URL, call .arrayBuffer() on the response, pass the result to ctx.decodeAudioData(), and store the resulting AudioBuffer in a map keyed by sound name. When your game needs to play "jump", it looks up the buffer and creates a source node from it.

Memory management matters for AudioBuffers. A 10-second stereo clip at 44.1kHz occupies about 1.7MB of memory after decoding, regardless of the original file's compressed size. Games with hundreds of sound effects should track their total decoded buffer memory and unload buffers for sounds no longer needed (e.g., sounds from a previous level).

For background music, which can be several minutes long, consider using a MediaElementAudioSourceNode connected to an <audio> element instead. This streams the audio progressively rather than loading the entire track into memory, which is more memory-efficient for files over 1-2 minutes.

The Audio Graph

The Web Audio API uses a directed graph model where audio flows from source nodes through processing nodes to the destination. You build this graph by calling node.connect(otherNode). Each node has inputs and outputs, and you chain them together to create your audio processing pipeline.

A minimal game audio graph has three layers. Source nodes (AudioBufferSourceNode for sound effects, MediaElementAudioSourceNode for music) generate audio. Processing nodes (GainNode for volume, PannerNode for spatial positioning, BiquadFilterNode for equalization) modify the signal. The destination node (ctx.destination) outputs to the speakers.

For a game, you typically create a routing structure with separate sub-mixes. A master GainNode controls overall volume. Branching from it, a music GainNode controls music volume, an SFX GainNode controls sound effects, and a UI GainNode controls interface sounds. Each sound source connects to the appropriate category gain, which feeds into the master gain, which feeds into the destination. This gives you the standard volume slider structure players expect in settings menus.

Nodes can connect to multiple outputs, and multiple nodes can connect to a single input. This fan-out and fan-in capability lets you build complex routing without duplicating audio data. A single ambient rain loop can connect to both the main output and a reverb send simultaneously.

Playing Sounds

To play a sound, you create an AudioBufferSourceNode, assign your pre-decoded AudioBuffer to its buffer property, connect it to the audio graph, and call start(). The sound plays immediately (or at a scheduled future time) and the source node is consumed, meaning you cannot reuse it. Each playback event requires a new source node.

This single-use design seems wasteful at first, but it is intentional. Source nodes are lightweight objects. The expensive part, the decoded audio data, lives in the AudioBuffer which is shared across all source nodes playing that sound. Creating a hundred source nodes for a hundred simultaneous gunshots costs almost no memory because they all reference the same buffer.

The start() method accepts up to three parameters: start(when, offset, duration). The when parameter is an AudioContext time (from ctx.currentTime) specifying exactly when playback should begin. Passing 0 or ctx.currentTime starts immediately. Passing ctx.currentTime + 0.5 schedules it 500ms in the future. The offset parameter starts playback from a specific point in the buffer (useful for audio sprites). The duration parameter limits how long the sound plays.

Stopping a sound uses the stop() method, which also accepts an optional when parameter for scheduled stops. Once a source node has stopped, it cannot be restarted. For sounds you might need to stop and restart (like a looping ambient sound), keep a reference to the source node so you can stop it, then create a new one when you need to restart.

Volume Control with GainNode

The GainNode multiplies the audio signal by a value. A gain of 1.0 is unity (no change), 0.0 is silence, and values above 1.0 amplify the signal. In practice, game volume controls map a slider from 0 to 1 onto the gain value.

You set gain in two ways. Direct assignment (gainNode.gain.value = 0.5) changes the volume immediately, which can cause clicks if the value jumps during active audio. Scheduled changes using gainNode.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.1) smoothly transition the volume over the specified time, eliminating clicks. Always use ramped changes for volume transitions during playback.

The gain property is an AudioParam, which is the Web Audio API's mechanism for parameter automation. AudioParams support several scheduling methods: setValueAtTime() sets a value at a specific time, linearRampToValueAtTime() ramps linearly, exponentialRampToValueAtTime() ramps exponentially (which sounds more natural for volume changes because human hearing is logarithmic), and setTargetAtTime() approaches a target value with an exponential decay constant.

A common pattern for game volume controls: create three GainNodes (masterGain, musicGain, sfxGain), connect them in a chain where music and sfx feed into master, and expose the gain values to your settings UI. When the player adjusts the music slider, ramp musicGain.gain to the new value over 50-100ms. Store the values in localStorage so they persist between sessions.

Timing and Scheduling

The AudioContext clock (ctx.currentTime) is the backbone of precise audio timing. It runs on the audio thread at the hardware sample rate, independent of the main JavaScript thread's requestAnimationFrame or setTimeout timers. This means audio scheduling remains accurate even when the game's frame rate drops or JavaScript execution pauses for garbage collection.

For sound effects, precise timing usually does not matter, you play them as soon as the event fires. For music, timing is critical. If you are scheduling beats, transitioning between tracks at specific musical boundaries, or synchronizing sound effects to a rhythm, you need to use the AudioContext clock exclusively.

The lookahead scheduling pattern is the standard approach for music sequencing. Your game loop checks the current time and schedules any notes or events that should occur within the next 100-200ms. By scheduling ahead of time, you ensure that audio events land precisely on the audio thread's clock even if the game loop runs at inconsistent intervals. This is how browser-based drum machines, sequencers, and rhythm games achieve tight timing.

For crossfading between music tracks, schedule both the fade-out and fade-in on the AudioContext clock. Start the new track and ramp its gain from 0 to 1 over 2 seconds, while simultaneously ramping the old track's gain from 1 to 0. Because both ramps are scheduled on the same clock, they remain perfectly synchronized regardless of main thread activity.

Key Takeaway

The Web Audio API is a graph-based audio system where source nodes flow through processing nodes to the speakers. Create one AudioContext, decode sounds during loading, build a gain-node routing structure for volume categories, and use the AudioContext clock for precise timing. Source nodes are single-use and cheap, buffers are shared and reusable.