technique

Exporting game audio

Which file formats, sample rates, and compression settings to use so game audio is small, loads fast, and loops cleanly.

Before this

This page assumes you are comfortable with:

Why you need this

A finished loop that clicks after export, or a web game that waits on nearly 13 MB of sound before the menu appears, undoes all the shaping work of stage 4. Export is stage 5 of the pipeline: choosing the format each file ships in so it is small, decodes in every browser your players use, and still loops cleanly.

The idea

Keep a lossless master, ship a copy. The master is the edited, level-set file, saved without any information loss: WAV (raw samples) or FLAC (losslessly compressed samples). Every shipped file is made from the master. If you later change format or bitrate, you go back to the master, never re-encode a shipped copy, because each lossy pass removes more detail.

Uncompressed size is a product. With 1 MB = 1,000,000 bytes:

bytes=sample rate×bytes per sample×channels×seconds\text{bytes} = \text{sample rate} \times \text{bytes per sample} \times \text{channels} \times \text{seconds}

At 48 kHz (48,000 samples per second), 16-bit (2 bytes per sample), stereo (2 channels), one minute is 48,000×2×2×60=11,520,00048{,}000 \times 2 \times 2 \times 60 = 11{,}520{,}000 bytes, 11.52 MB.

Lossy formats throw away detail people hear least and describe the result with a bitrate, in kilobits per second (kbps). Size is then roughly bitrate times length: one minute at 128 kbps is 128,000×60/8=960,000128{,}000 \times 60 / 8 = 960{,}000 bytes, 0.96 MB. Many encoders use a variable bitrate, so treat the formula as an estimate and check the real files.

The formats a web game chooses between.

Format Kind Browser support (MDN's audio codec guide, at the time of writing) Loops cleanly?
WAV Uncompressed All major browsers Yes
OGG Vorbis Lossy Chrome, Edge, Firefox; Safari listed as not supported Yes, when the file records its exact length
Opus (in Ogg or WebM) Lossy Chrome, Edge, Firefox; Safari with caveats about the container Yes, when the file records its exact length
AAC (in an MP4 container, often .m4a) Lossy Broad; Firefox relies on the operating system's decoder Depends on the encoder and the player; test it
MP3 Lossy All major browsers Often adds a gap at the start and end

Browser support changes, so check the current table before you choose. A safe pattern is two encodings of each file, for example Ogg and AAC, with the game asking the browser which one it can play:

const a = new Audio();
const ogg = a.canPlayType('audio/ogg; codecs="vorbis"');   // "", "maybe", or "probably"
const aac = a.canPlayType('audio/mp4; codecs="mp4a.40.2"');
console.log({ ogg, aac }, ogg ? "load .ogg" : "load .m4a");

Why MP3 breaks loops. An MP3 encoder works in fixed blocks called frames; in the common MPEG-1 form, each frame holds 1,152 samples. Two things add silence. First, the encoder and decoder each add a short delay, so the decoded audio starts with extra samples; the LAME project's technical FAQ, for example, describes a decoder delay of 528 samples. Second, the end is padded with silence to fill the last frame. A loop of exact length gets a few milliseconds of silence at the start and the end, and a looping player plays that silence every pass: a gap, or a click. Some encoders write the delay and padding into a header so a "gapless" player can trim them, but whether a given browser or engine does so varies. Formats in an Ogg container record the exact number of samples in the stream, which is why they loop cleanly when the decoder honors it. Whatever you choose, load the exported loop into the game and listen to the seam.

Mono for most effects. A card slap or a click has no meaningful left-right difference. Mono halves the size. Keep stereo for music and for ambience that should feel wide.

One sample rate. Pick 44.1 kHz or 48 kHz for the whole game and convert every master to it once. Mixed rates force the player's device to resample on the fly, which costs time and can add artifacts.

A sound sprite is many short effects stored back to back in one file, with a table of start and end times. A web game then makes one request instead of dozens. Leave a short silence between effects so a slightly late stop does not play the start of the next one.

Names and folders. Lower case, no spaces, sortable, with the type first: music/lobby-loop.ogg, music/stinger-win.ogg, sfx/card-play.ogg. A small manifest (a JSON file listing each sound's file, loop points in samples, tempo, and default volume) keeps the code from hard-coding any of them.

Worked example

The durations are the Lumen Clash set as built on the sibling pages, which are illustrations. Masters are 48 kHz, 16-bit. Music is stereo at a nominal 128 kbps; effects are mono at a nominal 64 kbps. Sizes use 1 MB = 1,000,000 bytes.

File Length Channels WAV bytes Lossy bytes (estimate)
Lobby loop (4 s intro + 16 s loop) 20 s 2 3,840,000 320,000
In-match loop (24 bars at 150 BPM) 38.4 s 2 7,372,800 614,400
Win stinger 4.7 s 2 902,400 75,200
Lose stinger 3.1 s 2 595,200 49,600
Card draw 250 ms 1 24,000 2,000
Card play 500 ms 1 48,000 4,000
Button click 60 ms 1 5,760 480
Total 12,788,160 (12.79 MB) 1,065,680 (1.07 MB)

The compressed set is about one twelfth the size. Music is almost all of it: the two loops alone are 11.21 MB of the 12.79 MB uncompressed.

The MP3 gap, in numbers. The lobby file is 960,000 samples (20 s at 48 kHz). That is 960,000/1,152=833.33960{,}000 / 1{,}152 = 833.33 frames, so the encoder needs 834 frames and pads the last with 834×1,152−960,000=768834 \times 1{,}152 - 960{,}000 = 768 samples: 16 ms of silence at the end. Add a decoder delay of 528 samples, 11 ms at 48 kHz, at the start. Every sample of music in the decoded file now sits 528 samples later than in the master, so loop points written in samples for the master land 11 ms early in the beat. A player that loops the whole file plays about 27 ms of silence every pass, which is easy to hear in a steady beat.

The effects as a sprite. With 100 ms of silence between them: click from 0 to 60 ms, draw from 160 to 410 ms, play from 510 to 1,010 ms, one mono file 1.01 s long.

In a game's audio pipeline

Export is the first step of stage 5. It reads the masters that Loudness for games set, with the loop points from Seamless music loops and the cue lengths from Stingers and transitions. Run the conversion as one batch with fixed settings, so every file is made the same way and the batch can be rerun. Then Licensing and provenance: add the export format and settings to each file's provenance record before it goes into a release.

Common mistakes

  • Looping an MP3. Symptom: a short dropout or click at the seam that was not in the master.
  • Only one format. Symptom: silence on one browser, often Safari with Ogg Vorbis, with no error the player sees.
  • Re-encoding a lossy file. Symptom: a swishy, watery high end that gets worse each round.
  • Stereo effects. Symptom: the download is twice the size it needs to be, and nothing sounds different.
  • Mixed sample rates. Symptom: a stinger plays slightly sharp or flat, or with faint artifacts, on some devices.
  • Bitrate too low on music. Symptom: cymbals and reverb tails turn into a hissy smear.

Cost

Encoding is fast: a batch of a few minutes of audio converts in seconds on any recent machine, in time proportional to total length. Disk for masters is the uncompressed figure, 11.52 MB per minute of 48 kHz 16-bit stereo; keep them. The cost that matters is what players download: here 1.07 MB instead of 12.79 MB, which is the difference between a menu that loads at once and one that waits on a slow phone connection. Two formats per file doubles your storage on the server, not the player's download, since each player fetches one. The maker's time goes to testing loops after export and on each browser.

Going further

  • The LAME project's technical FAQ, on encoder delay and padding.
  • MDN's audio codec guide and media container guide, for current browser support.
  • Gapless playback metadata in MP3 and AAC, and why players differ in reading it.
  • The Web Audio API's decodeAudioData, which decodes a whole file into samples so loops are sample-exact.

Back to Local text-to-audio for games