Self-hosted, pure-Go, on-the-fly audio transcoding for the Wax family
(WaxTap, WaxBin, WaxLabel, WaxSeal): request -> decode -> DSP -> encode ->
stream, tuned for time-to-first-audio, sample-exact seeking, and flaky
mobile networks. No ffmpeg at runtime, ever (CGO_ENABLED=0).
The codecs (Opus, MP3, AAC-LC, Vorbis, FLAC, ALAC, and WAV encoders, plus a
wider decoder set) are written from scratch for Go 1.26 and published as public,
stdlib-only packages under this module, CI-enforced by make depcheck,
so anyone can import them.
v1.0 feature-complete. Everything below is tested, capability-gated
(/caps never advertises what does not work), and held to the pinned
gates in docs/quality-gates.md.
- Encoders (all from scratch, stdlib-only): Opus (full SILK, hybrid,
and CELT with the tonality analyser; at quality parity with libopus
on the reference
opus_comparemetric across music and speech corpora), MP3 (psychoacoustic model, joint stereo, CBR and VBR, LAME gapless tag), AAC-LC (window switching, TNS, M/S, two-loop quantization; at parity with ffmpeg's native encoder on the ODG-proxy gate), Vorbis (product-lattice VQ residue books, perceptual coupled-stereo classification; ODG-proxy gate green), FLAC (levels 0-8, smaller thanflac -5at level 5), ALAC (bit-exact round trip), and WAV/AIFF PCM. - Decoders / inputs: FLAC (bit-exact on the IETF suite), WAV, AIFF, MP3, AAC-LC and ALAC in MP4/M4A/M4B, ADTS, Opus (all RFC 6716/8251 conformance vectors pass), Vorbis, Ogg, Matroska/WebM. Sample-exact seeking everywhere, gapless honored per format (LAME tag, iTunSMPB, edit lists, Ogg pre-skip/end-trim, Matroska CodecDelay).
- DSP: Kaiser windowed-sinc resampling (
hq/fast), BS.775 downmix, gain with true-peak limiting, TPDF and shaped dither, EBU R128 / BS.1770-4 loudness (differential-verified against ffmpeg). - Service: progressive streaming with a direct-play/transmux/
transcode ladder, sample-exact
t=seeks, HMAC-signed URLs pinned to source identity, a write-through cache with read-behind delivery and full ranges on completed entries, HLS (CMAF/fMP4, stateless signed URLs, bitrate ladders, byte-identical segment regeneration), async jobs with restart safety (transcode, analyze, and the gapless merge/split pair: a lossless split rejoins bit for bit, and a split takes a CUE sheet or sample cut points), uploads, loudness analysis with ReplayGain tagging, metadata passthrough (tags, chapters, cover art, lyrics), admission control, Prometheus metrics, named client delivery profiles in/caps, and a full API contract in docs/api.md. - Structure: the root module is the importable, dependency-free
audio library; its
go.modhas an empty require block, so importingcodec/flac(or any public package) pulls in nothing. The CLI/daemon binary lives in the nestedcli/module (cobra + waxlabel), the tests that need third-party oracles inoracletest/, and a worked example of extending the CLI inexamples/catalogcli/.
Measured by the committed harnesses on the reference dev box (12-core
x86-64, Linux, loopback HTTP; server/load_soak_test.go, re-run per
release with make soak):
- Time to first audio (
TestTTFAPercentiles, n=50): cold transcode (also the cost of everyt=seek, since each offset is its own cache entry) p50 0.6 ms / p95 0.9 ms; warm (completed cache entry) p50 0.21 ms / p95 0.24 ms; seek into a 60 s FLAC source (bisection seek + pipeline start) p50 1.7 ms / p95 2.7 ms. Targets: p95 <300 ms warm, <800 ms cold, met with three orders of headroom on this box; network and disk latency dominate in deployment. - HLS seek-to-segment (variant-worker restart): worst 49 ms in the restart-heavy e2e fetch pattern, p95 well under the 1 s target.
- Load (
TestLoadMixedTraffic): 6,350 req/s of mixed live transcodes, seeks, direct play, probes, and HLS over 8 concurrent workers with zero malformed responses (503s under saturation carry the honestoverloadedenvelope). - Streaming soak (
TestStreamingSoak, goroutine/heap leak watch): 918k requests over a sustained run with full reads, mid-body client disconnects, seeks, and HLS fetches; goroutines returned to baseline and heap stayed flat (~1.3 MiB). The nightly job runs a 20-minute soak;make soakruns 30 minutes locally. - Codec throughput (per core,
make bench): every codec clears its pinned floor in docs/quality-gates.md by a wide margin (Opus encode 55-67x realtime, decode 274-514x; MP3 encode 50-69x, decode ~200x; FLAC encode ~200x, decode 490-870x; AAC encode 20-68x, decode ~240x).
# Hardened standalone deployment, port 4418. Publishing a non-loopback
# port requires API keys (the daemon fails closed without them).
WAXFLOW_API_KEYS=$(openssl rand -hex 24) docker compose up -d
curl http://localhost:4418/ping
curl -H "X-API-Key: $WAXFLOW_API_KEYS" http://localhost:4418/capsPut music under ./library (or set WAXFLOW_LIBRARY=/path/to/music)
and stream: mint a URL with POST /sign, or try the dev demo page
(--demo). Or from source (Go 1.26.3):
make build
./bin/waxflow server --demo & # loopback: keyless is allowed
./bin/waxflow ping
open http://127.0.0.1:4418/demoPrecedence: flag > WAXFLOW_* env > JSON config file > default
(config file via --config or WAXFLOW_CONFIG; unknown keys are rejected).
| Key | Env | Default | Purpose |
|---|---|---|---|
addr |
WAXFLOW_ADDR |
127.0.0.1:4418 |
listen address (compose widens to 0.0.0.0) |
logLevel |
WAXFLOW_LOG_LEVEL |
info |
debug|info|warn|error |
roots |
WAXFLOW_ROOTS |
none | named library roots; JSON [{"name","path"}], env name=path,name2=path2; each opened via os.Root (no escape, symlinks confined), files validated regular and size-capped |
catalogDB |
WAXFLOW_CATALOG_DB |
none | WaxBin catalog path for pid:<ULID> source references, read by a build that injects a catalog resolver (see pid: sources). No build here serves it: this server refuses to start with it set (one-shots on plain paths never read config, so they neither honor nor refuse it) |
apiKeys |
WAXFLOW_API_KEYS |
none | control-API keys (comma-separated in env). Fail closed: required on a non-loopback addr unless allowUnauthenticated |
allowUnauthenticated |
WAXFLOW_ALLOW_UNAUTHENTICATED |
false |
explicit opt-in to keyless on non-loopback |
sourceMaxBytes |
WAXFLOW_SOURCE_MAX_BYTES |
4 GiB | per-source open cap |
metricsKey |
WAXFLOW_METRICS_KEY |
none | additionally unlocks GET /metrics |
signingSecret |
WAXFLOW_SIGNING_SECRET |
auto-generated into dataDir (0600) |
HMAC key for signed URLs; kid:hex,kid2:hex rotation list or a literal secret |
allowedOrigins |
WAXFLOW_ALLOWED_ORIGINS |
none | CORS allowlist for playback endpoints |
dataDir / cacheDir |
WAXFLOW_DATA_DIR / WAXFLOW_CACHE_DIR |
platform dirs | daemon state (signing secret, job store) / transcode cache |
scratchDir |
WAXFLOW_SCRATCH_DIR |
temp dir + /waxflow |
upload spool (the hardened container mounts a tmpfs) |
uploadMaxBytes / uploadTTL |
WAXFLOW_UPLOAD_MAX_BYTES / WAXFLOW_UPLOAD_TTL |
2 GiB / 1h |
one upload's size cap / spool eviction after creation (Go duration, 0 never) |
scratchMaxBytes |
WAXFLOW_SCRATCH_MAX_BYTES |
8 GiB | aggregate spool cap (uploadMaxBytes only caps one upload) |
cacheMaxBytes / cacheMaxAge |
WAXFLOW_CACHE_MAX_* |
10 GiB / off | LRU eviction policy (cacheMaxAge is a Go duration) |
liveSlots / jobSlots |
WAXFLOW_*_SLOTS |
NumCPU-1 / 2 | live admission pool (over limit means 503 + Retry-After: 2) / concurrent job workers (jobs queue and also pause while the live pool is saturated) |
defaultGain |
WAXFLOW_DEFAULT_GAIN |
track |
gain mode when gain= absent |
resampleProfile |
WAXFLOW_RESAMPLE_PROFILE |
hq |
hq or fast (constrained hosts) |
tlsCert / tlsKey |
WAXFLOW_TLS_* |
none | native TLS; else put a terminating proxy in front (ADR-0007) |
debugAddr |
WAXFLOW_DEBUG_ADDR |
off | loopback-only pprof listener |
paceBurstSeconds / paceFactor |
WAXFLOW_PACE_* |
30 / 2.0 | read-behind delivery pacing (factor 0 disables) |
demo |
WAXFLOW_DEMO |
false |
serve the browser test page at /demo (dev only) |
waxflow server: run the daemon (--demofor the browser test page)waxflow probe <file>: identify a file and print stream parameters (--jsonfor the schemaVersion'd machine shape, identical toGET /probe;--strictto treat tolerated input damage as errors)waxflow transcode <in> <out>: local one-shot file-to-file transcode through the same engine the daemon uses (--format wav|aiff|flac|mp3|aac|alac|opus,--flac-level,--mp3-bitrate, default from the output extension;--forceto overwrite). Metadata (tags, chapters, cover art, lyrics) passes through onto the output automatically (--no-tagsto skip);--loudness analyzemeasures the source, applies the exact gain to the ReplayGain reference, and writes measured RG tags on the outputwaxflow split <in> <dir>: cut a single-file rip into one output per track, from a CUE sheet (--cue album.cue) or explicit source-sample offsets (--at). Cut points are samples either way: a sheet'sMM:SS:FFtimes are CD frames of 1/75 s, which every CD-family rate divides exactly (44100/75 = 588), so a boundary converts with no rounding, where seconds would land a sample off and click at every join. The default FLAC output at the source's own rate makes the cut bit-exact, so the pieces rejoin into the original.--dry-runprints the pieces and their rangeswaxflow sign --src lib/a.flac: mint a signed playback URL offline (ADR-0003; uses the same secret and roots the daemon holds)waxflow cache stats|gc: inspect or evict a running daemon's cachewaxflow doctor: check the local environment a daemon needs: config resolves, every root opens and reads, the cache/data/scratch dirs accept writes, the WaxBin catalog opens (builds with a catalog resolver), a quick self-bench transcodes faster than realtime, and the absence of ffmpeg is confirmed to be fine (--jsonfor the machine shape)waxflow ping: liveness probe; the container HEALTHCHECKwaxflow version: version and build infowaxflow exit-codes: print the documented exit-code contract (0 ok, 1 internal, 2 invalid, 3 not-found, 4 io, 5 unsupported, 6 canceled, 7 unauthorized, 8 overloaded)
The HTTP surface is documented in docs/api.md.
pid:<ULID> names an item in a WaxBin catalog. No build here resolves
one: WaxFlow ships no catalog code and no database dependency, so every
build in this repo answers pid: with 501 unsupported-source.
A build that wants them injects a catalog resolver through the
cli.Flavor seam (cli/root.go), whose OpenResolver hook wraps the
library roots with extra source schemes; catalogDB is the configuration
field carried across it. examples/catalogcli/ is a working CLI built
that way, and the module to copy from. Once such a build resolves pid:,
every surface that takes a source reference accepts it (/stream,
/probe, /sign, jobs, HLS, plus waxflow probe|transcode|sign), and
/caps reports delivery.pid.
What holds regardless of who resolves them: signed URLs pin bytes, not
locations, so a catalog rename or move does not kill a minted URL, while
replaced content still dies with 410 source-changed.
Video; HE-AAC/SBR/xHE; WMA/APE/WavPack encoding; WMA/APE/WavPack decoding; DASH manifests (the CMAF segments are already DASH-compatible); DRM/HLS-AES; Opus PLC; CD ripping; any database (WaxBin owns cataloging); tag editing (WaxLabel owns it; WaxFlow only maps and passes metadata); Icecast/radio ingest; waveform peaks (WaxBin has them); distributed cache; two-pass loudness on live streams (jobs only).
make check # gofmt + vet + test + test-race + nested modules + depcheck
make test # root-module suite, no race detector (the fast default loop)
make test-race # race detector over the root module (heavy numeric suites self-skip)
make test-cli # the cli module (cobra CLI + waxlabel mapper), race included
make test-oracle # the third-party-oracle tests (waxlabel round trips, go-mp3)
make test-example # examples/catalogcli: the out-of-prefix cli.Flavor canary
make soak # 30m streaming soak + load + TTFA percentiles (nightly-scale)
make client-e2e # browser client-matrix cells via Playwright (gated tooling)
make docker # local image build
make verify-vectors # fetch SHA-256-pinned conformance vectors (CI-cached)
make goldens # regenerate muxer golden files (review the diff)- Architecture invariants live in docs/adr/. Read ADR-0001 (clean-room policy) before touching codec code.
- Encoder/decoder acceptance thresholds are pinned in docs/quality-gates.md; gates only ratchet up.
- ffmpeg is a test oracle only (differential CI job), never a runtime dependency.
- Releases are tag-driven: pushing
vX.Y.Zpublishes binaries + SHA256SUMS and a multi-arch (amd64/arm64) image toghcr.io/colespringer/waxflow.
MIT. Third-party attributions: THIRD-PARTY-NOTICES.md.