# Asset formats · zvuk

zvuk v1.14.1 · MIT · https://zvuk.schmooky.dev/guides/asset-formats/

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

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

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`:

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`.
