Guide

Asset formats

The encoding pipeline you want: WebM/Opus first, M4A/AAC fallback for older iOS Safari.

The recommendation, in one line

Ship every sound as two files: a .webm with the Opus codec (primary), and an .m4a with the AAC LC codec (fallback for iOS Safari ≤16). Pass them as an array and zvuk picks the one your browser can decode.

ts ts
await engine.loadSound('coin', [
  '/sfx/coin.webm',  // Opus — Chrome, Firefox, Edge, Safari 17+
  '/sfx/coin.m4a',   // AAC  — iOS Safari ≤16, older macOS Safari
], { bus: 'sfx' });

Why Opus, why AAC, why not just MP3

CodecSize at "good"QualityBrowser support
Opus (in WebM)~50% of MP3ExcellentAll except old iOS Safari
AAC LC (in M4A)~75% of MP3GreatEverywhere
MP3baselineOKEverywhere
OGG Vorbis~70% of MP3GoodNo Safari at all
WAV~10× MP3LosslessEverywhere

For SFX (short, dense, high-frequency), Opus around 64–96 kbps is indistinguishable from source. AAC needs 96–128 kbps to match. Both are dramatically smaller than MP3, and the Opus/AAC pair covers 100% of browsers.

Encoding pipeline (ffmpeg)

From source .wav or .flac:

bash bash
# Music / ambience — stereo, higher bitrate
ffmpeg -i src.wav -c:a libopus -b:a 96k -vn out.webm
ffmpeg -i src.wav -c:a aac     -b:a 128k -vn -movflags +faststart out.m4a

# SFX — mono, lower bitrate
ffmpeg -i src.wav -ac 1 -c:a libopus -b:a 64k -vn out.webm
ffmpeg -i src.wav -ac 1 -c:a aac     -b:a 96k -vn -movflags +faststart out.m4a

The -movflags +faststart flag moves the M4A index to the head of the file so decoding starts before the entire payload is downloaded. Always include it.

Bulk transcode script

bash bash
#!/usr/bin/env bash
# transcode all .wav in raw/ → .webm + .m4a in public/audio/
set -euo pipefail
shopt -s nullglob
for src in raw/*.wav; do
  name="$(basename "${src%.wav}")"
  ffmpeg -y -i "$src" -ac 1 -c:a libopus -b:a 64k -vn \
    "public/audio/${name}.webm" -loglevel error
  ffmpeg -y -i "$src" -ac 1 -c:a aac -b:a 96k -vn -movflags +faststart \
    "public/audio/${name}.m4a" -loglevel error
done

What about MP3?

MP3 is fine, and it ships everywhere, but it is strictly larger than the Opus/AAC pair at equivalent quality. The only reason to use it is licensing paranoia, which expired in 2017. Skip it.

What about FLAC / WAV?

For UI clicks under ~50ms, an uncompressed .wav can be smaller than the codec headers + footers of a compressed file, and the decoder is free. Ship those as .wav.

Picking the right URL yourself

loadSound picks for you, but the helper is exported if you want to drive <audio> tags or your own loader:

ts ts
import { pickSource, canPlay } from '@schmooky/zvuk';

const url = pickSource(['/sfx/coin.webm', '/sfx/coin.m4a']);
// returns the first URL the browser advertises canPlayType > '' for.

if (canPlay('audio/webm; codecs="opus"')) {
  // safe to ship Opus exclusively to this user
}

Pitfalls

Don't ship .ogg as your only source.
Safari does not decode OGG. Many free SFX packs ship .ogg. Transcode to webm and m4a before serving.
Don't forget faststart for M4A.
Without -movflags +faststart, decode waits for the entire file to download. On a slow connection this turns "play on click" into "play in 4 seconds".
Don't ship 320 kbps for a 200ms click.
SFX rarely benefit beyond 64 kbps Opus / 96 kbps AAC. Stereo doubles size for the same perceived quality on a mono click. Use -ac 1.