Z zvuk
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 — 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 — 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:

ts ts
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

Don't ship .ogg as your only source.
Safari does not decode OGG. Many free SFX packs ship .ogg — transcode to webm/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.