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 — zvuk
picks the one your browser can decode.
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
| Codec | Size at "good" | Quality | Browser support |
|---|---|---|---|
| Opus (in WebM) | ~50% of MP3 | Excellent | All except old iOS Safari |
| AAC LC (in M4A) | ~75% of MP3 | Great | Everywhere |
| MP3 | baseline | OK | Everywhere |
| OGG Vorbis | ~70% of MP3 | Good | No Safari at all |
| WAV | ~10× MP3 | Lossless | Everywhere |
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:
# 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
#!/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 — it ships everywhere — but it's 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:
import { pickSource, canPlay } from '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
-movflags +faststart, decode waits for the entire file to download.
On a slow connection this turns "play on click" into "play in 4 seconds".
-ac 1.