waxflow

package module
v0.0.0-...-192e0e1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 41 Imported by: 0

README

WaxFlow

Self-hosted, pure-Go, on-the-fly audio transcoding: request -> decode -> DSP -> encode -> stream, tuned for time-to-first-audio, sample-exact seeking, and flaky mobile networks.

The codecs (Opus, MP3, AAC-LC, HE-AAC v1, Vorbis, FLAC, ALAC, WavPack, Monkey's Audio, 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.

Status

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_compare metric 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), HE-AAC v1 and v2 (SBR over a half-rate AAC-LC core: transient-aware envelope grids, tonality-driven noise floors and inverse filtering, coupled stereo; v2 adds parametric stereo over a phase-aligned mono downmix, selected explicitly; explicit signalling in M4A, implicit in ADTS), Vorbis (product-lattice VQ residue books, perceptual coupled-stereo classification; ODG-proxy gate green), FLAC (levels 0-8, smaller than flac -5 at level 5), ALAC (bit-exact round trip), WavPack (four compression levels, each a wider search over candidate decorrelation cascades; at parity with libwavpack on the official suite's own audio, and ahead of it at the fast setting), Monkey's Audio (three compression levels; the coded frames are the reference encoder's frames, byte for byte), and WAV/AIFF PCM.
  • Decoders / inputs: FLAC (bit-exact on the IETF suite), WAV, AIFF, MP3, AAC-LC, HE-AAC v1 and v2 (SBR+PS, ffmpeg-differential-verified), and ALAC in MP4/M4A/M4B, ADTS (implicit HE-AAC detected), Opus (all RFC 6716/8251 conformance vectors pass), Vorbis, Ogg, Matroska/WebM, WavPack (bit-exact on the official test suite: 8- to 32-bit integers, the three stereo block modes, APEv2 tags; encodes too), Monkey's Audio (all five compression levels, 8/16/24-bit, APEv2 tags; encodes the first three), and Windows Media Audio 1 and 2 in ASF (mono and stereo, 8-48 kHz; decode only, and encoding it is a non-goal: nothing plays WMA that does not also play a format this tree writes better). Sample-exact seeking everywhere, gapless honored per format (LAME tag, iTunSMPB, edit lists, Ogg pre-skip/end-trim, Matroska CodecDelay). WMA Pro, Lossless and Voice share the container and are different codecs; they are refused by name.
  • 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.mod has an empty require block, so importing codec/flac (or any public package) pulls in nothing. The CLI/daemon binary lives in the nested cli/ module (cobra + waxlabel), the tests that need third-party oracles in oracletest/, and a worked example of extending the CLI in examples/catalogcli/.

Performance

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 every t= 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 honest overloaded envelope).
  • 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 soak runs 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).

Quick start

# 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/caps

Put music under ./library, or point the compose bind mount elsewhere with WAXFLOW_LIBRARY=/path/to/music, and stream: mint a URL with POST /sign, or try the dev demo page (--demo). WAXFLOW_LIBRARY is compose's variable for that mount, not a daemon setting; the daemon reads roots / WAXFLOW_ROOTS. 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/demo

Configuration

Precedence: 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)

Runtime root reload. A daemon configured through the JSON roots (not WAXFLOW_ROOTS) serves POST /roots/reload, which re-reads the config and reconciles its live library roots so a root added at runtime streams without a restart. It reconciles only what the resolver owns (the roots and sourceMaxBytes); every other setting still needs a restart. It is wired only when a reload could do something, a config file is set and WAXFLOW_ROOTS is not pinning roots, so env-only and no-config deployments report delivery.rootsReload: false in /caps and keep serving new roots by direct download. The integration contract for a caller that adds a root (WaxDeck): write the root into the sidecar's JSON config atomically (write a temp file, then rename), then POST /roots/reload; the atomic write keeps a reload from reading a half-written file and 400ing.

CLI

  • waxflow server: run the daemon (--demo for the browser test page)
  • waxflow probe <file>: identify a file and print stream parameters (--json for the schemaVersion'd machine shape, identical to GET /probe; --strict to 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|he-aac|alac|opus|vorbis|wavpack|ape, --flac-level, --wavpack-level, --ape-level, --mp3-bitrate, default from the output extension; --force to overwrite). An mp4-family output (.m4a, .m4b, alac) is written flat: a file can satisfy the header back-patch that form needs, and it is the shape players and taggers expect. The fragmented (CMAF) form is for delivery, and /stream and HLS keep it. Metadata (tags, chapters, cover art, lyrics) passes through onto the output automatically (--no-tags to skip); --loudness analyze measures the source, applies the exact gain to the ReplayGain reference, and writes measured RG tags on the output. Ogg-FLAC, WavPack, and Monkey's Audio outputs get a projection there rather than a measurement, and say so: their muxer takes its tags before the encode and cannot be patched afterward.
  • waxflow 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's MM:SS:FF times 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-run prints the pieces and their ranges
  • waxflow 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 cache
  • waxflow 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 (--json for the machine shape)
  • waxflow ping: liveness probe; the container HEALTHCHECK
  • waxflow version: version and build info
  • waxflow 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: sources

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.

Development

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.
  • Defects that are understood but deliberately unfixed, with the reason, are in docs/deferred-work.md.
  • ffmpeg is a test oracle only (differential CI job), never a runtime dependency.
  • Releases are tag-driven: pushing vX.Y.Z publishes binaries + SHA256SUMS and a multi-arch (amd64/arm64) image to ghcr.io/colespringer/waxflow.

License

MIT. Third-party attributions: THIRD-PARTY-NOTICES.md.

Documentation

Overview

Package waxflow exposes the transcoding engine facade (New, Probe, Transcode, OpenStream), the library-first entry point to the pure-Go audio pipeline: request -> decode -> DSP -> encode -> stream.

The public, stdlib-only packages live under this module, whose require block is empty by construction (importing any of them pulls in nothing):

waxerr        - error taxonomy: codes, sentinels, exit-code contract
audio         - PCM model (planar buffers, formats, layouts)
dsp/...       - resample, mix, gain, dither, loudness, psy, fft
codec/...     - pcm, flac, alac, mp3, aac, opus, vorbis, wavpack, ape
container/... - riff, aiff, ogg, mp4, mka, adts, mpa, flacn, wv, apen
format        - probe + registry + Open
source        - source-ref model + Resolver interface
server        - HTTP service
client        - Go client for the HTTP service

The codec/DSP tree is stdlib-only, enforced in CI by `make depcheck` and structurally by this module's empty require block; the CLI/daemon binary (cobra + waxlabel) lives in the nested cli/ module, and the third-party-oracle tests in oracletest/. See docs/adr/ for the architecture decision records that pin the invariants this module promises.

Index

Constants

View Source
const (
	DefaultSilenceThresholdDB = -50.0
	DefaultSilenceMinDuration = 500 * time.Millisecond
)

Silence detection defaults, applied to the zero fields of SilenceOptions. The threshold suits studio-quiet content; see SilenceOptions.ThresholdDB for why the right value is a property of the source rather than of the detector.

View Source
const (
	// FLACLevelDefault keeps the encoder's default compression level.
	FLACLevelDefault = 0
	// FLACLevelFastest selects FLAC level 0.
	FLACLevelFastest = -1
)

FLACLevel spellings whose meaning the zero value cannot carry.

View Source
const (
	// WavPackLevelDefault keeps the encoder's default level (normal).
	WavPackLevelDefault = 0
	// WavPackLevelFast is the shallowest cascade: fastest, largest.
	WavPackLevelFast = wavpack.LevelFast
	// WavPackLevelNormal is the default cascade.
	WavPackLevelNormal = wavpack.LevelNormal
	// WavPackLevelHigh spends more passes for a smaller file.
	WavPackLevelHigh = wavpack.LevelHigh
	// WavPackLevelVeryHigh is the deepest cascade: smallest, slowest.
	WavPackLevelVeryHigh = wavpack.LevelVeryHigh
)

WavPackLevel spellings. WavPack's levels are named modes rather than a numeric scale, and they are numbered from one, so the zero value means the default with no sentinel needed.

View Source
const (
	// APELevelDefault keeps the encoder's default level (normal).
	APELevelDefault = 0
	// APELevelFast is the shallowest cascade: no filter at all, fastest,
	// largest.
	APELevelFast = ape.LevelFast
	// APELevelNormal is the default cascade, a 16-tap filter.
	APELevelNormal = ape.LevelNormal
	// APELevelHigh runs a 64-tap filter for a smaller file.
	APELevelHigh = ape.LevelHigh
)

APELevel spellings. Monkey's Audio names its levels in thousands, which is the vocabulary the format itself uses in the file header, so the option carries those numbers rather than a scale of its own; zero is not one of them, so the default needs no sentinel.

View Source
const (
	// OpusComplexityDefault keeps the encoder's default complexity.
	OpusComplexityDefault = 0
	// OpusComplexityLowest selects complexity 0.
	OpusComplexityLowest = -1
)

OpusComplexity spellings whose meaning the zero value cannot carry.

View Source
const (
	ContainerProgressive = "progressive"
	ContainerFragmented  = "fragmented"
	// ContainerOgg is the flac row's Ogg wrapper override.
	ContainerOgg = "ogg"
)

ContainerProgressive and ContainerFragmented are the TranscodeOptions. Container overrides naming the two MP4 box shapes. Progressive is the flat moov+mdat form, "the .m4a most players expect"; it back-patches its header, so it needs a seekable destination and is not live. Fragmented is the CMAF form /stream and HLS deliver, and it is the aac and alac rows' default (the empty override).

Fragmented is spellable even though it is the default because the empty override no longer reaches it everywhere: a file output takes the flat form (FileOutputContainer), which would otherwise leave the delivery form with no name a caller could ask for.

Exported because four packages spell them: the CLI's --container flag, the job request's container field, the /stream query parameter, and this table. They were restated per boundary until the empty-to-progressive rule was reimplemented three times and one of the three was missed, which is what FileOutputContainer below now prevents by construction.

View Source
const CutVersion = "cut-2"

CutVersion identifies the cut rung's own sample-affecting logic for the ADR-0004 cache key.

It rides beside RemuxVersion rather than replacing it, because a cut is a remux with a packet filter in front: everything RemuxVersion covers still applies, and this covers what the filter adds. RemuxVersion's own argument for existing is the one that puts this here too. A cut runs no decoder, no DSP and no encoder, so no revision of any of them can change its bytes; what it does synthesize is a set of trims and a rewritten codec config, and wrong gapless metadata is wrong playback rather than merely older bytes.

cut-2: the HE-AAC decode preroll (aac.HESeekPreroll) grew from 4096 to 24576 samples, and the preroll picks each span's first kept packet, so the same request now cuts different bytes on HE-AAC sources.

View Source
const DefaultSegmentSeconds = 4.0

DefaultSegmentSeconds is the HLS target segment duration when a request names none, per the Apple authoring guidance for low-latency-enough audio streaming without playlist bloat.

View Source
const RemuxVersion = "remux-3"

RemuxVersion identifies the remux rung's own sample-affecting logic for the ADR-0004 cache key.

It is the only version constant this rung adds, and the absence of the others is deliberate rather than an oversight. A remux runs no decoder, no DSP, and no encoder, so no revision of any of them can change its bytes; keying on them would invalidate remuxes for fixes that cannot reach them. The progressive muxers carry no version constants at all, which is equally deliberate: a whole-file cache entry is self-consistent, so a framing change leaves older entries as merely older bytes that still decode identically. (The segmented form inherits mp4.SegmenterVersion for free, which exists because segments must agree with each other and with a restarted worker within one stream.)

The muxer half of that reasoning was wrong, and remux-2 is the correction. A framing change is not always "merely older bytes that still decode identically": the Ogg-Vorbis granulepos fix changed what the muxer writes for the stream's length, so cached remuxes kept reporting a duration ~21 ms long. The encoder-side lever for that fix (vorbis.EncoderVersion) is not in this rung's key at all, by the design above, so this constant is the only one that can invalidate them. Bump it for a muxer change that alters what a file says about itself, not merely how it is framed.

What is left is the gapless trailer this rung synthesizes from the input track, and that is not merely older bytes: a bug there writes a wrong iTunSMPB or a wrong edit list, and wrong gapless metadata is wrong playback. That is the one thing here that needs a version, so it is the one that has one.

remux-3 is the same argument for WavPack. The muxer patches the first block's stream length, a block checksum covers the header that field sits in, and until now the patch left that checksum stale: `wvunpack -v` refuses the files this rung produced whenever a source's length was unknown or a cut changed it. Nothing but this constant can invalidate them.

View Source
const ToEnd = -1

ToEnd is Slice's open-ended upper bound: the span runs to the end of the source.

Variables

This section is empty.

Functions

func Concat

func Concat(members []ConcatSource, opts ConcatOptions) (format.Media, error)

Concat sequences members into one gapless format.Media: a single continuous timeline whose sample len(a) is b's sample 0, exactly, unless opts.Crossfade asks for a blend.

The butt-join is the default and the primitive. It is sample-exact by construction rather than by arithmetic: format.Media already delivers gapless-trimmed PCM, so there is no encoder delay or padding left to reason about at the seam, and concatenation is just reading one stream after another.

A crossfade trades exactly that away, on purpose and only when asked: the seam becomes a zone of Crossfade samples that is both members at once, and the timeline shortens by one zone per seam. See ConcatOptions.Crossfade, which is zero for every caller that does not want it.

Members open on demand and close on advance. That is the design and not an optimization: besides costing one file descriptor for a queue of any length, it makes planning and running symmetric (both are driven by the members' tracks alone) and it removes the rewind problem outright, since a member reached a second time is a member opened a second time, from the top, with no state to have gone stale.

The returned Media owns nothing until it is read and closes whatever it opened on Close. It also satisfies format.Composite, so a consumer keying its own cache can reach the members' tracks rather than only the envelope.

func ConcatTrack

func ConcatTrack(tracks []container.Track, opts ConcatOptions) (container.Track, error)

ConcatTrack computes the synthetic track a Concat of these members presents: the common (envelope) format, the summed normalized length, and no gapless trims. It is a pure function of the headers, so planning and running cannot disagree about the delivered format.

The envelope is the format no member loses information to reach: the maximum rate, the maximum channel count, and the wider sample domain (float if any member is float). Refusing mixed members instead would not push the problem to the caller, it would delete the feature for the normal case: HLS cannot change format mid-variant without an EXT-X-DISCONTINUITY and a second init, which one chain, one init, and one edit list forbid structurally, and a play queue is mixed by nature.

A member whose format already equals the envelope is read straight through, with no chain and no copy, so a uniform timeline (a gapless album, which is one master at one rate) pays nothing for the machinery. That is structural rather than an optimization: it falls out of the envelope being a maximum.

One member at 96 kHz makes every other member resample twice, member to envelope and envelope to output. The cost is real and deliberately unaddressed: collapsing it needs the output format, which is not known here (an output row's adjust hook owns the real rate, which is how Opus forces 48 kHz whatever the caller asked for). Likewise the channel count is a maximum and is not capped at stereo: capping would silently destroy a surround member, and it looks cheaper only because the output is usually stereo, which is the same output-aware knowledge this function does not have. The common mixed-channel case is a mono track in a stereo queue, where the maximum is exact and free.

Delay and Padding are zero, and that is load-bearing rather than incidental: format.Media delivers already-trimmed PCM, so both trims happened inside each member before Concat saw a sample. That is exactly why concatenation is sample-exact, and a nonzero trim here would make a downstream consumer trim a second time.

It takes the options because concatLayout is the single funnel and a crossfade changes the length: opts.Crossfade shortens the total by X per seam, and every refusal a crossfade needs lives in concatLayout so that planning a timeline and running one refuse the same requests for the same reasons.

func CrossfadeSamples

func CrossfadeSamples(tracks []container.Track, seconds float64) (int64, error)

CrossfadeSamples converts a crossfade expressed in seconds into the envelope samples ConcatOptions.Crossfade carries. The wire spells a crossfade in seconds because a caller cannot know the envelope rate (the maximum member rate) before the members are measured, and so cannot express the blend in the samples the option wants; this is where the two meet.

The rate is read from the same concatLayout ConcatTrack, Concat, and ConcatBoundaries read, so a crossfade converted here is measured on exactly the rate the run blends on. That is what lets a plan and a run convert one signed number the same way and never come to disagree about how long the seam is: the envelope is a pure function of the members' formats, which are pinned, so the count is deterministic for a given set of members. The result is rounded to the nearest sample.

A non-positive (or NaN) seconds is a butt-join, the zero the default every timeline that does not ask for a blend gets. An absurdly large seconds is clamped to the int64 ceiling rather than wrapped, so it reaches checkCrossfade as the refusal it is ("more than this timeline can blend") instead of a silently wrapped small value: the fit and memory bounds are checkCrossfade's to enforce inside ConcatTrack, not this converter's.

func Cut

func Cut(demux container.Demuxer, track container.Track, spans []Span, grid int) (container.Demuxer, error)

Cut returns a view of demux holding only track's packets that fall in spans, retimed to be contiguous: the packet-domain sibling of Slice, and the input side of a cut.

It is a wrapper rather than a TranscodeOptions field for the reason Slice is, and for one more that is this rung's own. The remux rung shares TranscodeOptions deliberately: remuxable derives its rule by comparing option projections instead of hand-listing fields, which is what keeps it from drifting as options land, and a parallel options struct would destroy that derivation. The codebase has already answered "how do I express a span" twice, and both answers say the same thing: a span is applied by wrapping, never through the options.

The returned Demuxer implements neither container.Seeker nor Warner nor Chapterer, which embedding the interface gives for free: a method set outside container.Demuxer is not promoted. That is the wanted answer rather than a gap. A segmented cut would need a Seeker, and one that seeked the source's timeline while the packets ran on the cut's would be wrong in a way no error would surface, so it fails loudly at the type assert instead.

The caller owns demux. Packets stay borrowed exactly as they are through a plain remux: this delegates ReadPacket inward and mutates only PTS, so copyPackets' borrow contract holds verbatim and no copy is added.

func CutFormats

func CutFormats() []string

CutFormats lists the output formats the cut rung serves without re-encoding, in table order. A format qualifies only where the cut is reachable on every surface it may be asked for: its codec is in the cut allowlist WITHOUT the head-only restriction (a head-anchored codec cuts, but advertising it would promise arbitrary spans it silently re-encodes), it has a live progressive form (/stream can serve it), and it has a segmented (HLS) form. So the one flat list is honest on both surfaces. A client names one of these (with a from/to span) to reach the cut; format=auto never does. Today: opus and aac (he-aac cuts head-anchored spans when asked, unadvertised).

func Cuttable

func Cuttable(track container.Track) bool

Cuttable reports whether track's codec is one whose packets survive being moved within a stream, which is the premise the cut rung rests on. It is the exported form of the cutCodecs membership test, so a caller can skip the packet-grid walk for a codec no cut could ever serve.

It answers only the codec question, which is the cheap one: a true here does not promise the cut will be taken, since a sub-grid gap, an unanswerable tail, or a destination that cannot signal the trims still declines inside PlanCut. It is the fast negative, not a guarantee of the positive. See cutCodecs for why the set is an allowlist rather than a lossless rule.

func DefaultLiveFormat

func DefaultLiveFormat() string

DefaultLiveFormat returns the output format that format=auto resolves to when a transcode is required: the first registered output with a streaming form.

func FileOutputContainer

func FileOutputContainer(requested string, plan *TranscodePlan) string

FileOutputContainer resolves the container a file output should be written with: the caller's explicit choice, or the flat MP4 form when the plan says this is an mp4-family output and the caller expressed no preference.

A file can satisfy the back-patch the flat header needs, streaming buys it nothing, and it is the only form that carries a QuickTime chapter track or that a tag rewriter can edit afterwards. Delivery keeps the fragmented default, which is where it belongs.

It lives here rather than at each boundary because every writer of a file needs it: `waxflow transcode`, `waxflow split`, and all of the transcode, split and merge job types. Pass the plan taken from the caller's own options and re-plan when this changes the answer, so the plan and the run agree about what was asked for.

func LossyFormat

func LossyFormat(name string) (lossy, known bool)

LossyFormat reports whether the named output format is lossy (accepts bitrate/q), and whether it is a registered format at all. An unregistered name returns (false, false) so callers defer to the format-existence error rather than mislabeling it as lossless.

func OutputContainerForExt

func OutputContainerForExt(ext string) (format, container string, ok bool)

OutputContainerForExt maps a container-selecting output extension (one that names a container form rather than a top-level format) to the format and container override it implies. MKA and WebM are reached through a Container override on a codec row, not their own output rows, so the extension alone does not resolve through OutputFormatForExt; this fills that gap for the CLI. A ".mka"/".mkv" defaults to lossless FLAC-in-Matroska (matching a lossless source), ".webm" to Opus-in-WebM. ok is false for any other extension.

func OutputEmbedsTags

func OutputEmbedsTags(format, container string) bool

OutputEmbedsTags reports whether the muxer for a (format, container) pair writes the track's tags itself, so a tagging post-pass must skip that output rather than write a second, conflicting set over the top.

Four do. The MP4 muxers embed an ilst in moov, which is also the only way the fragmented form gets tags at all: the mapper reads that shape but refuses to rewrite it. The Ogg muxer embeds the comment header at Begin. The WavPack and Monkey's Audio muxers write the APEv2 block after the audio, which is likewise the only way a .wv or a .ape gets tags, since waxlabel cannot identify either format and a post-pass on one fails rather than adding anything. Every other output, incl. Matroska (.mka/.webm), defers to the post-pass: the mka muxer accepts Tags but does not emit them (see container/mka.MuxerOptions), so if it ever starts writing them at Begin, add it here.

Exported for the same reason the container names above are: three callers need it (the CLI's transcode and split, the job runner), each had spelled it out separately, and the job runner's spelling had fallen a format behind. WavPack and Monkey's Audio are keyed on the format because their rows have no container of their own, which is what made a container-keyed predicate silently never fire.

func OutputExt

func OutputExt(format, container string) string

OutputExt is the file extension for an output written as format in container (empty for the format's default form), without the leading dot.

It is the write direction of OutputFormatForExt and a separate function rather than that one inverted, because the two are not inverses: an extension resolves to at most one format, so of the two formats that write .m4a only aac claims it, and yet both write it. A caller naming a download needs the name the file should carry, which is the question the read direction cannot answer. See output.writeExt.

A container override names a wrapper, not a box shape, and only a different wrapper renames the file: adts, mka, webm, and ogg each write a different kind of file than the row's default, while progressive is the aac/alac row's own MP4 with its boxes flattened and stays an m4a. That distinction is what this exists for. A container name used as an extension yields foo.progressive, which is not a file Apple Books will open.

An unregistered format falls back to bin. The caller is naming a download, so "" is not an answer; the format's own name is the confident lie this removes (foo.alac), and a format with no row has no file form to name at all, which is exactly what bin says.

format and container are taken as a pair some plan already accepted: this names the result, it does not re-validate the request.

func OutputFormatForExt

func OutputFormatForExt(ext string) string

OutputFormatForExt maps a file extension (with or without the leading dot, any case) to the output format name that writes it, or "" when no registered output claims the extension.

func OutputFormats

func OutputFormats() []string

OutputFormats lists the registered output format names, in table order, omitting remux-only rows like Outputs does.

func SampleTime

func SampleTime(n int64, rate int) time.Duration

SampleTime is sample n's position on a stream's clock at rate. It is the shared overflow-safe sample-to-duration converter: Slice's chapter retiming here and a merge's chapter offsets (internal/jobs) both place samples on a clock, and both must round the same way.

The division is split so the whole-second part stays exact at any stream length: the direct n*time.Second/rate overflows an int64 past about 53 hours at 48 kHz, and a long file is exactly the kind that carries chapters. What is left rounds toward zero, below the nanosecond the Duration itself resolves.

func SegmentedFormats

func SegmentedFormats() []string

SegmentedFormats lists the output formats with a segmented (HLS) form, in table order. Remux-only rows are omitted (their hls column serves the segmented remux of their sources, not a requestable transcode).

func Slice

func Slice(med format.Media, from, to int64) (format.Media, error)

Slice bounds med to the sample range [from, to) of its own timeline, as a Media whose sample 0 is med's sample from and whose length is to-from. to is exclusive; ToEnd means to the end. The returned Media owns med and closes it.

It is the primitive behind three things that looked like three features: a split job's cut points, an end trim (a span with a from of 0), and a virtual track streamed over an offset range of one file. All three are "bound this stream to a sample range", so they land once.

A wrapper rather than a TranscodeOptions field, deliberately. An end bound as an option would need about six branches in the most invariant-dense function in the library, permanently: a clamp in the length math, a refusal in the segmented plan, the right interaction with the projected output length that feeds both the muxer's declared length and the edit list (get that wrong and every M4B lies about its duration), the progress total, and both canonical cache-key strings. A Media that is already the bounded stream needs none of them, because every one of those reads the length off the track and the track is already right.

It composes, which is the clinching part. A sliced Media is the shifted stream, addressing from 0, so it hands the segmented path a start offset that PlanSegments refuses to take as an option ("segments address time"); and "start the album at track 3" is Concat(members[2:]) with no new option at all.

Exactness is conditional, and the condition is worth stating

A slice hands the chain a stream starting at sample from, and the chain starts fresh there, so any stateful node primes from nothing exactly as it does after a seek.

  • A cut with no rate change is exact, and that is the case that matters. A CUE split to FLAC at the source rate builds a chain with no resampler and no limiter, so there is no state to prime and each piece's sample 0 is the source's sample from, bit for bit. TestSliceSplitRoundTrip proves precisely that: a transient would make a bit-exact rejoin fail.
  • A resampled span would carry a short transient at sample 0, because the resampler's FIR window starts zero-filled, and that is what Headroom exists to remove: a span is a window onto a longer stream, so unlike a file it genuinely has audio before its own sample 0 to prime with. The segmented run uses it, so a virtual track's first sample is the same audio a continuous run of the whole source delivers there. That is what lets consecutive virtual tracks of one rip play gaplessly.

Cut points are not assumed frame- or packet-aligned. Slice sits downstream of decode, so it cuts at any sample, which is the whole reason it is sample-exact where a packet-level cut would not be.

func SpanTrack

func SpanTrack(track container.Track, from, to int64) (container.Track, error)

SpanTrack computes the track a Slice of track to [from, to) presents: the same format, the window's length, and no gapless trims. It is a pure function of the header, so planning a span and running it cannot disagree about what gets delivered.

It is the single funnel, the discipline ConcatTrack applies to a timeline: Slice resolves its track through this at open, and a caller planning a span resolves through it too, from the probed track alone and without opening anything. Without that, a plan's length and the slice's actual delivery drift, and the drift is invisible until a cache entry holds segments for a track that is not the one being served.

to is exclusive; ToEnd means to the end of track.

Types

type AnalyzeOptions

type AnalyzeOptions struct {
	// Channels, when non-zero, measures the loudness after mixing the
	// source down to this channel count (1 or 2, matching a later
	// TranscodeOptions.Channels), so a two-pass gain is computed on the
	// audio the encode will meter. 0 keeps the source layout. The fold is
	// the same one the encode applies (dsp/mix), but with no limiter, gain,
	// or dither: a measurement observes the raw fold, so TruePeakDB stays
	// honest where the encode's overshoot limiter would flatten it. This is
	// the substantive difference from TranscodeOptions.Channels.
	Channels int
	// Progress, when non-nil, is called after each decoded chunk with the
	// samples measured so far and the projected total (-1 unknown). It
	// runs on the analyzing goroutine, so blocking it pauses the
	// analysis; the job runner's yield-to-live-streams check rides on
	// exactly that.
	Progress func(done, total int64)
	// Silence, when non-nil, maps the source's silent spans alongside the
	// loudness measurement, from the same decode. Nil omits the map
	// entirely, so an analysis that does not ask for it is unchanged.
	Silence *SilenceOptions
	// Tap, when non-nil, is called with each decoded chunk's planar channel
	// slices at the source's own rate and layout: chans[c][i] is sample i of
	// channel c, all channel slices the same length, values nominal full
	// scale +-1.0. It is the seam for an analyzer WaxFlow does not own,
	// riding the same decode as the meter rather than paying for a second
	// one, which is what AnalyzeOptions.Silence already does for one WaxFlow
	// does own.
	//
	// It runs on the analyzing goroutine, so blocking it pauses the
	// analysis; the same contract Progress carries. An error from it fails
	// the analysis, as an error from the meter does.
	//
	// The slices are borrowed: they alias the pooled chunk buffer and are
	// valid only for the duration of the call, and the next chunk reuses
	// them. A tap that keeps the samples must copy them. It must also not
	// write to them, which is why it runs after the analyzers this engine
	// owns rather than before: their measurements are already taken, so a
	// tap that breaks the rule breaks only its own result.
	Tap func(chans [][]float32) error
}

AnalyzeOptions configures Engine.Analyze.

type AnalyzeResult

type AnalyzeResult struct {
	// Format is the PCM format the measurement ran on: the source rate in
	// the float domain, and the source channel layout unless
	// AnalyzeOptions.Channels asked for a downmix, in which case it is the
	// folded layout (the rate stays the source rate either way). When a
	// downmix was asked for, every measured field below (IntegratedLUFS,
	// LoudnessRange, TruePeakDB, SamplePeakDB) is on that downmix basis,
	// since all come off one meter fed the folded channels: a 5.1 source
	// measured at Channels 2 reports a stereo loudness, range, and true
	// peak, which is what makes the two-pass gain correct.
	Format audio.Format
	// Samples is the number of frames measured.
	Samples int64
	// IntegratedLUFS is the gated integrated loudness. Silence that
	// never passes the absolute gate reports math.Inf(-1).
	IntegratedLUFS float64
	// LoudnessRange is the EBU Tech 3342 loudness range in LU.
	LoudnessRange float64
	// TruePeakDB is the maximum oversampled true peak in dBTP,
	// math.Inf(-1) for silence.
	TruePeakDB float64
	// SamplePeakDB is the maximum sample magnitude in dBFS, math.Inf(-1)
	// for silence.
	SamplePeakDB float64
	// Silence is the silence map, non-nil exactly when AnalyzeOptions
	// asked for one.
	Silence *SilenceResult
}

AnalyzeResult is a full-stream loudness measurement of the decoded audio per ITU-R BS.1770-4 and EBU R128.

type ConcatOptions

type ConcatOptions struct {
	// Profile selects the resampler quality profile for normalizing members
	// whose rate is not the envelope's; empty means resample.HQ.
	//
	// It must be the profile the transcode's own TranscodeOptions carry. See
	// the convention above.
	Profile resample.Profile

	// Crossfade is how many samples of each seam are a blend of the two
	// members meeting there, on the envelope's timeline. Zero, the default, is
	// a butt-join: sample len(a) is b's sample 0, exactly, which is what every
	// existing caller gets and what ADR-0009's primitive is.
	//
	// There is no nonzero default and there will not be one. A gapless album
	// must never blend, because the seam it would smear is the artifact this
	// primitive exists to deliver intact. A crossfade is a thing a caller asks
	// for on material that wants it (a declick between two independently
	// recorded takes, a play queue of unrelated tracks), never something the
	// library decides on their behalf.
	//
	// Each seam costs X samples of total length: N members crossfaded by X
	// deliver sum(len) - (N-1)*X. Member i's tail zone and member i+1's head
	// zone are the same region of the timeline, which is what the overlap is.
	// The blend is equal-power (cos/sin), so uncorrelated material holds its
	// level across the zone where a linear fade would dip 3 dB.
	//
	// Bounded twice, both refused at ConcatTrack so a plan and a run refuse
	// identically: every member must be long enough for the zones it carries
	// (head plus tail, so the edge members need only one), and a zone must fit
	// maxCrossfadeBytes.
	Crossfade int64
}

ConcatOptions configures a Concat.

Hand the same options to the plan and to the run

PlanSegmentsTimeline(tracks, copts, ...) and Concat(members, copts) are two calls taking two separately constructed ConcatOptions, and nothing checks that they match. Both fields below make a mismatch a silent wrong answer rather than an error, so the convention is: build one ConcatOptions and pass it to both.

For Profile a mismatch is a wrong cache key: the plan names one profile in its Versions and the run resamples through another, so the cached bytes describe processing that did not happen.

For Crossfade it is worse, and it is why this paragraph exists rather than the field being left to speak for itself. A crossfade changes the timeline's length, so a plan built with one and a run built without it disagree about how many samples exist: the plan promises the sum less (N-1)*Crossfade against a run delivering the full sum. That is the prefix-sum desync and the tail 404 that ADR-0009's advisory-length section exists to prevent, arriving by a different door.

type ConcatSource

type ConcatSource struct {
	// Track describes the member from its headers. Concat holds the member
	// to this declaration: one that opens in a different format, or delivers
	// a different number of samples, fails the run rather than silently
	// desyncing every position after it.
	Track container.Track
	// Open opens the member's decodable media. Concat calls it when the
	// timeline reaches this member and closes the result on advance, so a
	// 500-track queue costs one file descriptor rather than 500.
	//
	// Any context this closure binds must be the engine's own, never a
	// request's. Open fires lazily, mid-stream, long after the call that
	// built the Concat returned: live pipelines resolve under the server's
	// base context by design, so read-behind can finish an encode after the
	// client has left, and a request context captured here would instead
	// kill a member's first read at a track boundary minutes into playback.
	// container.Contextual exists for exactly this handoff.
	Open func() (format.Media, error)
}

ConcatSource is one member of a timeline: its track, as Probe reported it, so a timeline can be planned without opening anything, and a function that opens it on demand.

type CutPlan

type CutPlan struct {
	RemuxPlan
	// Landed is where the requested spans actually fell, one for one, on the
	// source's own track timeline.
	//
	// The head of the first span and the tail of the last land exactly where
	// they were asked for, because their snap slop is expressed as the
	// synthesized gapless trims rather than delivered. Every interior splice
	// snaps outward to the packet grid and says so here: there is no per-splice
	// trim to hide it in, so a caller that needs to know where its cut points
	// really landed reads them off this.
	Landed []Span
}

CutPlan describes what a cut would produce, computed from the source track's headers alone.

It embeds RemuxPlan for the reason RemuxPlan embeds TranscodePlan: consumers read one shape whichever rung answered. Track is the cut's synthesized track, carrying the trims and the rewritten codec config the cut computed, and Samples is its landed length.

type CutSegmentPlan

type CutSegmentPlan struct {
	RemuxSegmentPlan
	// Landed is where the requested spans fell, one for one, on the source's own
	// track timeline, exactly as CutPlan.Landed reports for the progressive cut.
	Landed []Span
	// Grid and SourceSamples are the source's packet grid and its exact length,
	// threaded to CutSegments so the run cuts on the same boundaries and
	// synthesizes the same track the plan (and so the cache key) was computed
	// from. SourceSamples is the source's own length even when the run reopens a
	// container that declares none (ADTS AAC-LC reports -1 from its headers), so
	// the run does not compute a different cut than the plan promised. It is -1
	// only when the plan itself was handed a lengthless track.
	Grid          int
	SourceSamples int64
}

CutSegmentPlan describes the segmented (CMAF) form of a cut, as CutPlan describes its progressive form. It embeds RemuxSegmentPlan for the reason CutPlan embeds RemuxPlan: the delivery layer reads the same segment facts off a plan whichever rung answered, and a cut is a segmented remux with a packet filter in front.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the library-first entry point to the transcoding pipeline. The CLI and the HTTP server are both thin layers over it.

func New

func New(opts ...Option) *Engine

New returns an Engine. Without WithLogger, logs are discarded.

func (*Engine) Analyze

func (e *Engine) Analyze(ctx context.Context, src container.Source, hint string, opts AnalyzeOptions) (*AnalyzeResult, error)

Analyze decodes src end to end and measures its loudness: integrated LUFS, loudness range, true peak, and sample peak. It powers the type:analyze job and the loudness:analyze two-pass transcode (the R128 half of the loudness design: live streams stay tag-based, exact measurement belongs to jobs, where a second pass is affordable).

AnalyzeOptions.Silence adds the silence map to the same pass. Both analyzers want the identical chain for the identical reason (the source's own rate and layout, in the float domain), so they share one decode rather than paying for two: the decode is the expensive half, and a library-wide sweep runs this over everything.

func (*Engine) AnalyzeMedia

func (e *Engine) AnalyzeMedia(ctx context.Context, med format.Media, opts AnalyzeOptions) (*AnalyzeResult, error)

AnalyzeMedia analyzes an already-opened Media, the same measurement as Analyze without the source-open step. It is the entry point for inputs that are not a single sniffable Source: the HLS client assembles a presentation from many fetched resources and exposes it as a format.Media, which flows through here exactly like a local file. The caller owns med and closes it.

func (*Engine) CutInitSegment

func (e *Engine) CutInitSegment(plan *CutSegmentPlan) ([]byte, error)

CutInitSegment builds the CMAF init header for a planned segmented cut. It is RemuxInitSegment over the embedded plan with no new box code: the cut track already carries the rewritten OpusHead and the synthesized delay, so its sample entry and edit list are correct, and the edit list instructs the player to skip the head pre-skip priming exactly as a remux's would. That the plan's Delay is the cut's and not the source's zero is what PlanRemuxSegments guarantees, since it sets SegmentPlan.Delay from the track it is handed, and it is handed the cut track.

func (*Engine) CutSegments

func (e *Engine) CutSegments(ctx context.Context, src container.Source, hint string, opts TranscodeOptions,
	spans []Span, grid int, samples int64, segOpts SegmentedOptions, emit func(mp4.Segment) error) (*SegmentedResult, error)

CutSegments emits numbered CMAF media segments from a span of src's own packets: the run half of the segmented cut rung, and the segmented sibling of CutStream. No decode, no DSP, no encode, so no generation loss; each segment holds the kept access units byte for byte.

opts, spans, grid, and samples must be the pair PlanCutSegments accepted, for the reason CutStream gives: a caller reaching this directly has chosen the rung, and the fallback it did not ask for would be the wrong help. samples is the source's exact length, threaded from the plan and patched over the reopened header's, so an undeclared-length source (ADTS AAC-LC) cuts against the length the plan measured rather than the -1 its headers report. Pass a negative samples to take the header's own length, which a source that declares one already has.

It opens the source, wraps it in a seekable Cut view, and hands that to the same segmentWalk RemuxSegments uses, so a mid-stream restart reproduces a continuous run's segment bytes for bytes. There is no demux.Close, mirroring RemuxSegments: the source.File owns the handle.

func (*Engine) CutStream

func (e *Engine) CutStream(ctx context.Context, src container.Source, hint string, dst io.Writer,
	opts TranscodeOptions, spans []Span, grid int, samples int64) (*TranscodeResult, error)

CutStream cuts src's existing packets to spans and rewrites the container around them, opening the source itself: the run half of the cut rung, and the packet-move sibling of Remux. No decode, no DSP, no encode, so no generation loss; the output holds the kept access units byte for byte, retimed to be contiguous.

opts and spans must be a pair PlanCut accepted; a request it declines is an error here rather than a silent re-encode, for the reason Remux gives about PlanRemux: a caller reaching this directly has already chosen the rung, and a fallback it did not ask for would be the wrong kind of help. The ladder calls PlanCut first and falls through on its own.

grid is the source's packet duration from Engine.PacketGrid, and samples is the source's exact length, both threaded in from the plan rather than re-measured here, so the bytes this delivers are the ones the plan and the cache key were computed against. It is the assembly recipe above, minus the plan step, in one call: the engine owns the open-and-assemble exactly as it does for Remux.

samples is the one thing a fresh header open cannot know and the plan can. An undeclared-length source (AAC-LC in ADTS) reports Samples -1 from its headers, while the plan measured the true length off the same source. That length is not cosmetic: the muxer's init segment encodes it (an fMP4 moov duration), so running from the header's -1 would write a stream whose own duration disagrees with the plan's advertised one. Everything else on the track (codec, config, trims, track ID) a fresh open reads identically, so only the measured length is patched over it. Pass a negative samples to take the header's length as-is, which is what a source that declares its own length already has.

func (*Engine) InitSegment

func (e *Engine) InitSegment(plan *SegmentPlan, opts TranscodeOptions) ([]byte, error)

InitSegment builds the CMAF init header for a planned segmented transcode: the ftyp+moov all the plan's media segments share. opts must be the options the plan was computed from. Deterministic: regenerating after cache eviction yields identical bytes.

func (*Engine) OpenStream

func (e *Engine) OpenStream(src container.Source, hint string) (format.Media, error)

OpenStream opens src for decoded, sample-exact PCM access. With an IndexCache configured, a saved source index (the MP3 frame table) is restored into the demuxer before the first read, and a grown one is saved back when the media closes.

func (*Engine) PacketGrid

func (e *Engine) PacketGrid(src container.Source, hint string) (int, error)

PacketGrid reports the decode duration every packet of src's default track shares: the grid a segmented remux must lay its segment boundaries on. It returns 0 when the durations vary, which is a fact about the source rather than an error, and the caller's cue to take a rung that has its own grid.

This is a demuxer walk, not a decode, in the same cost class as the exact-length measure (sub-millisecond for a three-minute file). It costs the progressive rung nothing, which needs no grid at all; only the segmented one asks.

The final packet is excluded on purpose. A stream's last packet is routinely short (an encoder's tail flush), and that is no obstacle to segmenting, since the last segment is short anyway. Every other packet must agree, and the silent failure it prevents is worth naming: mp4.Segmenter emits once it holds SegmentSamples of packets and stamps tfdt = index * SegmentSamples, so a short packet in the middle desynchronizes the real decode time from that arithmetic and every later segment carries a tfdt that is a lie. A packet that straddles a boundary errors loudly; a short one in the middle would not.

func (*Engine) PlanCut

func (e *Engine) PlanCut(track container.Track, opts TranscodeOptions, spans []Span, grid int) (*CutPlan, error)

PlanCut reports whether opts can be served by cutting track's existing packets to spans and rewriting the container around them, and how.

It is the cut's entry to the ladder, and it keeps the ladder's published contract: a request this rung cannot serve is not an error. PlanCut returns (nil, nil) and the caller falls through to a transcode, which cuts sample-exactly in the decode domain. An error means the request is wrong for every rung, and a transcode of it would fail identically.

grid is the source's packet duration from Engine.PacketGrid, exactly as PlanRemuxSegments takes one. A varying grid (0) declines the cut as it declines the segmented remux.

Why the declines say nothing to the caller

A decline's reason is not actionable: the caller's answer to every one of them is the same re-encode, so the reason is a debugging aid rather than a control-flow input, and a bare nil is the shape the ladder is built on. But this rung declines for seven distinct reasons and a caller asking why an Opus cut is re-encoding has no other signal, so each is logged at Debug on its way out. The prose lives on the error path, where RemuxDemuxer names it.

The seven: a codec off the allowlist, no grid, a sub-grid gap, an unanswerable tail, a source whose Delay or grid is outside the timeline this rung computes in, an HE-AAC span that does not keep the stream head, and a codec config the reprime cannot rewrite. The last is worth naming because it looks like it should be an error and is not: a config this rung cannot parse is one the demuxer built and the decoder still can, so the honest answer is to hand the request to a rung that decodes rather than to refuse it on everyone's behalf. Its own error code says CodeUnsupportedFormat for the same reason every malformed-input path here does, and that is precisely what a decline is made of.

func (*Engine) PlanCutSegments

func (e *Engine) PlanCutSegments(track container.Track, opts TranscodeOptions, spans []Span,
	grid int, segSeconds float64) (*CutSegmentPlan, error)

PlanCutSegments plans the segmented form of a cut: the HLS spelling of the cut rung, and the mirror of PlanRemuxSegments over a synthesized cut track. It runs CutTrack for the cut's own track, plans that track's segmented remux, and then applies the destination-trim gate PlanCut applies, so a segmented cut declines in exactly the cases the progressive one does.

grid is the source's packet duration from PacketGrid, as PlanRemuxSegments takes one. A varying grid (0) declines, as does anything PlanRemuxSegments declines, and all of these return (nil, nil): the caller falls through to a transcode. An error means the request is wrong for every rung, exactly the error/decline seam PlanCut documents.

func (*Engine) PlanRemux

func (e *Engine) PlanRemux(track container.Track, opts TranscodeOptions) (*RemuxPlan, error)

PlanRemux reports whether opts can be served by rewriting track's container around its existing packets, and how. It is the ladder's middle rung, between serving the original bytes and a full re-encode: the codec must survive unchanged, so the output row's codec must match the track's and no option may transform samples.

A request this rung cannot serve is not an error. PlanRemux returns (nil, nil) and the caller falls through to a transcode, which is what makes this a rung rather than a gate. An error means the request is wrong for every rung (an unsupported format, a container the format cannot produce), and a transcode of it would fail identically.

The rule falls out of the existing output table with nothing invented: format=opus already means "Ogg-Opus progressive, fMP4-Opus segmented" via the row's hls column, so Opus-in-WebM to Opus-in-fMP4 is just this rung with format=opus on the segmented path. Remux connects outputs that already exist.

func (*Engine) PlanRemuxSegments

func (e *Engine) PlanRemuxSegments(track container.Track, opts TranscodeOptions, segSeconds float64, grid int) (*RemuxSegmentPlan, error)

PlanRemuxSegments plans the segmented form of a remux: the rung that carries WaxTap's motivating case, since format=opus already means "Ogg-Opus progressive, fMP4-Opus segmented" and so Opus-in-WebM to Opus-in-fMP4 is this with nothing invented.

grid is the source's packet duration from PacketGrid. A zero grid declines, as does anything PlanRemux declines, and both return (nil, nil).

The alignment rule is to snap, not to refuse, and that is a correction to this milestone's plan worth stating where the code is. The plan feared that a 60 ms-frame Opus source has no whole-packet boundary in a 4 s segment (192000/2880 = 66.67) and concluded such a request must fall to rung 3. But segment length is not fixed at the request's ask: PlanSegments already snaps it to a whole number of encoder frames, which is exactly why a transcode's grid "is ours by construction". Handing it the packet duration as the frame size makes the same snap produce 67 packets of 2880 (a 4.02 s segment, aligned) with no new rule at all. A FLAC transcode already rounds 4 s to 4.01 s this way, so the behavior is not even new.

What genuinely cannot be served is a source whose packet durations vary, since there is then no grid to snap to. That is the real decline, and it is the one PacketGrid reports.

func (*Engine) PlanSegments

func (e *Engine) PlanSegments(track container.Track, opts TranscodeOptions, segSeconds float64) (*SegmentPlan, error)

PlanSegments plans the segmented form of a transcode of track. opts is the per-variant output selection (FromSample must be zero: segments own the timeline); segSeconds is the target segment duration, 0 for the default. A plan that succeeds guarantees TranscodeSegments and InitSegment accept the same options.

func (*Engine) PlanSegmentsTimeline

func (e *Engine) PlanSegmentsTimeline(tracks []container.Track, copts ConcatOptions,
	opts TranscodeOptions, segSeconds float64) (*SegmentPlan, error)

PlanSegmentsTimeline plans the segmented form of a concatenated timeline from its members' tracks alone (no decode, no open), exactly as PlanSegments does for one track: it is the same plan, over the synthetic track ConcatTrack computes, with the versions the synthetic track cannot name prepended.

opts and segSeconds mean what they mean for PlanSegments. copts must be the options the matching Concat is built with, both fields of them: see ConcatOptions, whose convention paragraph is what keeps this plan and that run describing the same audio.

func (*Engine) PlanTranscode

func (e *Engine) PlanTranscode(track container.Track, opts TranscodeOptions) (*TranscodePlan, error)

PlanTranscode plans a transcode of the given source track without opening a pipeline. The same validation as Transcode applies, so a plan that succeeds will not fail chain assembly later.

func (*Engine) Probe

func (e *Engine) Probe(src container.Source, hint string, opts *ProbeOptions) (*format.Info, error)

Probe identifies src and returns its parsed headers. The hint is an optional file extension used only when no magic bytes match.

func (*Engine) Remux

func (e *Engine) Remux(ctx context.Context, src container.Source, hint string, dst io.Writer, opts TranscodeOptions) (*TranscodeResult, error)

Remux rewrites src's container around its existing packets: the ladder's middle rung, and the one that makes "direct play, transmux, transcode" true. No decode, no DSP, no encode, so no generation loss; the output holds the source's own access units byte for byte.

opts must be one PlanRemux accepts; a request it declines is an error here rather than a silent re-encode, because a caller reaching this directly has already decided which rung it wants and a fallback it did not ask for would be the wrong kind of help. The ladder calls PlanRemux first and falls through on its own.

func (*Engine) RemuxDemuxer

func (e *Engine) RemuxDemuxer(ctx context.Context, demux container.Demuxer, track container.Track,
	dst io.Writer, opts TranscodeOptions) (*TranscodeResult, error)

RemuxDemuxer remuxes an already-opened demuxer to dst, the same packet copy as Remux without the source-open step. It is the entry point for packets that are not a single sniffable Source, and the packet-domain sibling of TranscodeMedia: the suffix says which domain the caller is opening into, since this one takes a container.Demuxer (packets) rather than a format.Media (decoded samples). Cut is the caller it exists for, handing in a filtered, retimed view of another demuxer.

track is the demuxer's own track, not a plan's. The distinction is not stylistic: the packet walk filters on track.ID while the muxer is opened with PlanRemux's ID-0 normalization of it, so handing a plan's Track here would filter out every packet of a source whose track ID is not 0 and write an empty file. The caller owns demux.

func (*Engine) RemuxInitSegment

func (e *Engine) RemuxInitSegment(plan *RemuxSegmentPlan) ([]byte, error)

RemuxInitSegment builds the CMAF init header for a planned segmented remux: the source's own sample entry, from the codec config it already carries, so the packets the segments hold and the header that describes them come from one place. Deterministic, like InitSegment.

func (*Engine) RemuxSegments

func (e *Engine) RemuxSegments(ctx context.Context, src container.Source, hint string, opts TranscodeOptions,
	segOpts SegmentedOptions, emit func(mp4.Segment) error) (*SegmentedResult, error)

RemuxSegments emits numbered CMAF media segments from src's own packets: the segmented form of the middle rung, and the back end of an HLS variant that needs no encoder.

A run starting mid-stream needs no priming at all, which is the one way this is simpler than its transcode sibling rather than harder. Priming exists to settle a resampler's window and an encoder's cross-frame state, and this rung has neither: the packets are the source's, already independently decodable, so segment n from a restarted worker is byte-identical to a continuous run's because it is built from the same bytes.

func (*Engine) Transcode

func (e *Engine) Transcode(ctx context.Context, src container.Source, hint string, dst io.Writer, opts TranscodeOptions) (*TranscodeResult, error)

Transcode decodes src and writes it to dst in the requested output format: decode -> DSP -> encode -> mux, checking ctx between chunks. The DSP chain (convert, resample, mix, gain, dither, in that fixed order) is assembled only from the options that differ from the source, so zero options add no stage at all and the decoder's samples reach the encoder unaltered. Against a lossless source and a lossless output that makes the transcode a bit-exact container rewrite. A lossy source is still decoded and re-encoded, which is a new generation rather than a rewrite of its packets; moving packets through untouched is a separate rung this engine does not yet have. A positive FromSample seeks sample-exact before the first chunk (the HTTP t= parameter, converted at the boundary). Output formats whose muxer needs to back-patch headers (AIFF, exact WAV sizes) want a dst that can really seek, which is probed rather than read off the method set: an *os.File on a pipe carries io.WriteSeeker and cannot seek. WAV falls back to a compliant streaming form on one that cannot; AIFF refuses.

func (*Engine) TranscodeMedia

func (e *Engine) TranscodeMedia(ctx context.Context, med format.Media, dst io.Writer, opts TranscodeOptions) (*TranscodeResult, error)

TranscodeMedia transcodes an already-opened Media to dst, the same decode -> DSP -> encode -> mux pipeline as Transcode without the source-open step. It is the entry point for inputs that are not a single sniffable Source: the HLS client assembles a presentation from many fetched resources and exposes it as a format.Media, which flows through here exactly like a local file. The caller owns med and closes it.

func (*Engine) TranscodeSegments

func (e *Engine) TranscodeSegments(ctx context.Context, src container.Source, hint string, opts TranscodeOptions,
	segOpts SegmentedOptions, emit func(mp4.Segment) error) (*SegmentedResult, error)

TranscodeSegments decodes src and emits numbered CMAF media segments: the variant-worker back end of HLS delivery. A run starting mid-stream (StartSegment > 0) seeks the source sample-exact and primes both sides: the decode chain warms its resampler history and the encoder settles its cross-frame state on ~100 ms of pre-target audio, whose packets are discarded on an exact frame boundary, so the kept packets sit at the same decode positions a continuous run would put them. Segments arrive in order starting at StartSegment; ctx is checked between chunks.

func (*Engine) TranscodeSegmentsMedia

func (e *Engine) TranscodeSegmentsMedia(ctx context.Context, med format.Media, opts TranscodeOptions,
	segOpts SegmentedOptions, emit func(mp4.Segment) error) (*SegmentedResult, error)

TranscodeSegmentsMedia emits numbered CMAF media segments from an already-opened Media: the seam TranscodeSegments is built on, for inputs that are not a single sniffable Source (a concatenated album timeline). The caller owns med and closes it.

type Headroomer

type Headroomer interface {
	// Headroom is how many samples of real audio lie before sample 0, so a
	// caller knows how far back it may seek. Zero means none.
	Headroom() int64
}

Headroomer is implemented by a Media that has real audio before its own sample 0, as a window onto a longer stream does. It is an optional capability in the same idiom as container.Indexer, container.Warner, and dsp.Settler: a Media opened from a file has nothing before its first sample and does not implement it, so the assertion is an honest gate.

It exists because priming a chain and starting a stream are different questions, and only a span can answer the first one for its own sample 0. A stateful node primes from nothing at a stream's start, which is correct for a file (there is nothing earlier) and wrong for a span (there is). A consumer that wants a span's sample 0 to hold the same audio a continuous run of the whole source delivers there reads Headroom, seeks to a negative position, and discards the output it fed through.

Positions below 0 are the whole point of the interface and are legal only on a Media that implements it. They stay within [-Headroom(), 0): the samples are real, they are simply upstream of the window this Media presents.

type IndexCache

type IndexCache interface {
	// Load returns the saved index blob for src, or nil.
	Load(src container.Source) []byte
	// Save persists a fresh snapshot for src. Best effort: failures are
	// the implementation's to swallow (a lost sidecar only costs a
	// rebuild).
	Save(src container.Source, blob []byte)
	// Drop removes src's saved blob. The engine calls it when a demuxer
	// rejects a loaded blob, so an invalid one stops being served (and
	// LRU-refreshed) forever.
	Drop(src container.Source)
}

IndexCache persists demuxer-built source indexes across sessions (the cacheDir/idx sidecar): MP3 frame tables today, seek tables for later formats. The engine restores a cached index when it opens a source whose demuxer can use one, and saves fresh snapshots on close. Keying blobs by source identity is the implementation's job (the server keys by ref plus size plus mtime); the engine stays identity-agnostic.

type MemberBoundary

type MemberBoundary struct {
	OffsetSamples   int64 `json:"offsetSamples"`
	DurationSamples int64 `json:"durationSamples"`
}

MemberBoundary is one member's place on a concatenated timeline. Both fields are in samples at the envelope rate (the timeline's normalized rate, reported alongside as the envelope rate). OffsetSamples is the member's actual start on the timeline; DurationSamples is its own raw normalized length.

Under a crossfade of X, consecutive members OVERLAP: member i occupies [OffsetSamples, OffsetSamples+DurationSamples), which runs X past where member i+1 begins, so OffsetSamples+DurationSamples can exceed the next member's OffsetSamples and sum(DurationSamples) is total + (N-1)X, not total. Only at X=0 do the members tile without overlap.

func ConcatBoundaries

func ConcatBoundaries(tracks []container.Track, opts ConcatOptions) ([]MemberBoundary, audio.Format, error)

ConcatBoundaries reports where each member lands on the concatenated timeline and how long it is, from the members' headers alone (no decode, no open), plus the envelope format the offsets are measured on. It reads the same concatLayout ConcatTrack and Concat read, so a boundary reported here is the position the run actually plays.

The offsets are actual timeline positions and overlap under a crossfade; see MemberBoundary for the contract, which is pinned from the first release so a consumer does not build on a meaning that changes when a crossfade is threaded to the wire.

type Option

type Option func(*Engine)

Option configures an Engine.

func WithIndexCache

func WithIndexCache(c IndexCache) Option

WithIndexCache wires an index sidecar cache into the Engine.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the Engine's logger. Nil (and the default) discards.

type OutputInfo

type OutputInfo struct {
	Name string
	Exts []string
	// Live reports a streaming form exists (plain io.Writer suffices).
	Live bool
}

OutputInfo describes one entry of the writer-side capability table.

func Outputs

func Outputs() []OutputInfo

Outputs lists the registered output formats, in table order. Remux-only rows are omitted: they advertise nothing the engine can encode.

type ProbeOptions

type ProbeOptions struct {
	// Strict turns tolerated input damage into errors.
	Strict bool
}

ProbeOptions configures Engine.Probe.

type RemuxPlan

type RemuxPlan struct {
	TranscodePlan
	// Track is what the muxer is opened with and what the trailer is
	// synthesized from: the source's own track, carrying its codec config and
	// its gapless trims across unchanged.
	Track container.Track
}

RemuxPlan describes what a remux would produce, computed from the source track's headers alone.

It embeds TranscodePlan because everything downstream of the ladder reads the same facts off a plan whichever rung answered, so the rungs hand their consumers one shape. The embedded plan is built from the source track and never from a chain, which is what keeps it honest: Format is the source's own, since nothing on this rung touches samples; Versions names RemuxVersion and no codec revision; and BitRate and EstimatedBytes stay unknown, because they are. Reporting an encoder's projected bit rate for packets no encoder produced would be exactly the plausible-looking wrong answer this rung must not give.

type RemuxSegmentPlan

type RemuxSegmentPlan struct {
	SegmentPlan
	// Track is the source's track, which the init segment is built from and the
	// trailer synthesized against.
	Track container.Track
}

RemuxSegmentPlan describes the segmented (CMAF) form of a remux, as SegmentPlan describes a transcode's. It embeds SegmentPlan for the reason RemuxPlan embeds TranscodePlan: the delivery layer reads the same facts off a plan whichever rung answered.

type SegmentPlan

type SegmentPlan struct {
	TranscodePlan
	// SegmentSamples is the decode duration of every segment but the last,
	// in output samples: the requested duration snapped to a whole number
	// of encoder frames so boundaries land exactly between packets.
	SegmentSamples int
	// Delay is the encoder delay the init segment's edit list carries.
	Delay int64
	// Codecs is the RFC 6381 CODECS attribute for master playlists.
	Codecs string
	// Bandwidth is a peak-bit-rate bound for master playlists: the exact
	// rate for CBR encoders, the PCM wire rate for VBR lossless (whose
	// compressed peak can only be below it), plus segmentation overhead.
	Bandwidth int
	// TotalDecodeSamples is the whole stream's decode duration: the
	// trimmed output length plus the delay and padding frames the encoder
	// flushes, rounded as the codec rounds. -1 when the source length is
	// unknown (VOD playlists then need the length measured first).
	TotalDecodeSamples int64
	// Segments is the exact segment count a VOD playlist promises, -1
	// when the source length is unknown.
	Segments int64
}

SegmentPlan describes the segmented CMAF (HLS) form of a transcode, computed from headers alone like TranscodePlan (which it embeds: the embedded Versions already carry the segmenter revision, so an HLS cache key derives from it directly).

func (*SegmentPlan) PresentationDuration

func (p *SegmentPlan) PresentationDuration(n int64) int64

PresentationDuration returns segment n's playable duration in samples: its decode span intersected with the presentation window the init segment's edit list declares, [Delay, Delay+Samples). The durations then sum to exactly Samples, which is what a player deriving a total duration from EXTINF should see.

It is a second method rather than a change to SegmentDuration, which stays decode-timeline truth: the segment count, the workers, and the segmenter's tfdt all measure the decode timeline, and the segments themselves do not move. Only the playlist text does.

A format whose row declares no encoder delay (FLAC, ALAC) takes totalDecodeSamples' delay == 0 early return, so TotalDecodeSamples equals Samples and the intersection is the identity. All three rungs read the same two fields: a transcode plan takes Delay from its row, a remux plan from the source's own pre-skip, and the cut rung embeds a remux plan.

func (*SegmentPlan) SegmentDuration

func (p *SegmentPlan) SegmentDuration(n int64) int64

SegmentDuration returns segment n's decode duration in samples, or -1 when n is out of range or the total is unknown.

type SegmentedOptions

type SegmentedOptions struct {
	// SegmentSamples is the segment length in output samples, from the
	// plan (it must be a positive multiple of the encoder frame).
	SegmentSamples int
	// StartSegment is the first segment to emit; the run continues from
	// there to the end of the stream. Zero encodes the whole sequence.
	StartSegment int64
}

SegmentedOptions selects which slice of the segment sequence a TranscodeSegments run produces.

type SegmentedResult

type SegmentedResult struct {
	// Samples is the stream's decode-timeline length as this run measured
	// it: the priming start position plus everything the encoder consumed
	// and flushed.
	Samples int64
	// Segments is the number of segments this run emitted.
	Segments int64
	// ClippedSamples counts samples the integer output could not carry; see
	// TranscodeResult.ClippedSamples. It spans everything this run fed the
	// chain, the discarded priming included, so a worker starting mid-stream
	// counts pre-roll that belongs to the previous segment: sum the counts
	// of a variant's workers and the overlap is counted twice.
	ClippedSamples int64
	// TruePeak is the output's true-peak level, linear, 1.0 = full scale;
	// see TranscodeResult.TruePeak. It spans the priming feed like the
	// count, but workers' maxima combine by max without double counting.
	TruePeak float64
}

SegmentedResult reports what a TranscodeSegments run produced.

type SilenceOptions

type SilenceOptions struct {
	// ThresholdDB is the silence threshold in dBFS; 0 means
	// DefaultSilenceThresholdDB. It must be negative and finite, which the
	// detector enforces; tighter policy clamps live at the API boundary,
	// not here, exactly as they do for TranscodeOptions.GainDB.
	//
	// The right value is a property of the content, so there is no default
	// that suits everything. See dsp/silence.New for the guidance, and
	// SilenceResult.DroppedSamples for what a wrong one looks like: it does
	// not fail cleanly, it reports no silence at all.
	ThresholdDB float64
	// MinDuration is the shortest span worth reporting; 0 means
	// DefaultSilenceMinDuration. It must be positive, which the detector
	// enforces.
	MinDuration time.Duration
}

SilenceOptions configures the silence map. Both fields are raw parameters rather than a closed vocabulary, which is the opposite of the choice gain= and dynamics= make, and deliberately: a closed vocabulary belongs where a value enters a cache key or a validated signal path, where it must mean the same thing forever. These values do neither. They shape a report, nothing is keyed by them, and the caller genuinely knows better than the daemon does.

type SilenceResult

type SilenceResult struct {
	// Version is the detector revision (ADR-0004 style). WaxFlow keys
	// nothing by it, but a caller caching the map needs it to know when
	// the map went stale.
	Version string
	// ThresholdDB and MinDuration are the resolved parameters, defaults
	// applied.
	ThresholdDB float64
	MinDuration time.Duration
	// Spans are the detected silences, in stream order.
	Spans []SilenceSpan
	// Dropped counts runs discarded for falling short of MinDuration.
	// Read it with DroppedSamples, never alone: ordinary audio dips under
	// any threshold at every zero crossing, so this is large even for a
	// source with clean silences.
	Dropped int
	// DroppedSamples is the summed length of those runs, and it is the
	// diagnostic. Against Samples it says how much of the source sat
	// below the threshold without ever staying there long enough to
	// report: near zero for a healthy source however large Dropped grows,
	// and a sizeable share of the stream when the threshold is wrong for
	// this source (see SilenceOptions.ThresholdDB).
	DroppedSamples int64
	// TotalSamples is the summed length of Spans, which is what a
	// "time saved by trimming" figure reads.
	TotalSamples int64
}

SilenceResult is the silence map: the spans plus the parameters they were found with, so a caller that stores the map can tell what it means.

type SilenceSpan

type SilenceSpan struct {
	From int64
	To   int64
}

SilenceSpan is one silent span of the analyzed source, in frames on its own timeline (ADR-0006). To is exclusive.

type Span

type Span struct{ From, To int64 }

Span is a kept sample range [From, To) of a source's own track timeline. ToEnd means to the end of the track.

The timeline is the track's, which is the gapless-trimmed one: sample 0 is the first sample a player hears, not the first sample the decoder emits. That is the timeline every other span-shaped thing in this library speaks (Slice, SpanTrack, the HTTP t= parameter), and the cut converts to the decode domain internally rather than making a caller do it.

func CutTrack

func CutTrack(track container.Track, spans []Span, grid int) (container.Track, []Span, error)

CutTrack synthesizes the track a cut of track to spans would produce: its trims, its length, its rewritten codec config, and where the spans landed.

grid is the source's packet duration from Engine.PacketGrid. Snapping to it is packet-aligned by definition, since it is measured as the decode duration every packet of the source shares rather than chosen by a caller.

It is a track-level computation for the reason SpanTrack is: a plan must be able to state the output's length without opening anything, and a plan's length and the run's actual delivery must not be free to drift.

Errors and declines

This returns errors throughout, including for the four conditions that are really declines, because its signature has nowhere to put a (nil, nil). PlanCut is what maps them back onto the ladder's published contract: CodeUnsupportedFormat here becomes a decline there, and CodeInvalidRequest propagates as an error. That split is the seam between "this rung cannot serve this" and "no rung can": an invalid span is one rung 3 would refuse identically, and a codec off the allowlist is one rung 3 serves happily.

type TranscodeOptions

type TranscodeOptions struct {
	// Format is the output format name: "wav", "aiff", "flac", "mp3",
	// "alac", "aac", "opus", or "vorbis".
	Format string
	// Container overrides the format's default container where the
	// format defines an alternative; empty selects the default. Today
	// only aac has one: "adts" replaces the progressive fragmented MP4
	// with the raw ADTS elementary stream, a legacy opt-out that
	// sacrifices gapless signaling (ADTS has none).
	Container string
	// Rate resamples to this sample rate in Hz; 0 keeps the source rate.
	Rate int
	// Channels converts the channel count (downmix to 1 or 2, or mono
	// duplication to stereo); 0 keeps the source layout.
	Channels int
	// BitDepth forces integer output at this depth, dithered when
	// reducing; 0 keeps the source domain and depth.
	BitDepth int
	// GainDB applies a scalar gain, finite within +-120 dB. Positive
	// gain engages the true-peak limiter; tighter policy clamps (the
	// HTTP +12 dB bound) live at the API boundary, not here.
	GainDB float64
	// Dynamics applies a dynamics-processing preset to the post-gain
	// signal: gain.PresetOff (the zero value) applies none, gain.PresetVoice
	// the spoken-word leveller. It is a closed vocabulary rather than raw
	// compressor parameters; see gain.Preset for why.
	//
	// It composes with GainDB rather than replacing it, and the order is
	// load-bearing: the preset's curve has a fixed threshold, so the caller
	// levels the signal to a known point with GainDB first and the preset
	// then shapes it. A caller with a measured loudness (an analyze job's)
	// sends the exact dB alongside the preset. WaxFlow cannot measure a
	// live stream, so it cannot do this for the caller: two-pass is
	// jobs-only.
	//
	// A preset always engages the true-peak limiter.
	Dynamics gain.Preset
	// FromSample starts output at this source-timeline sample, seeking
	// sample-exact before the first chunk. Seconds convert to samples at
	// the API boundary (ADR-0006); 0 starts at the beginning.
	FromSample int64
	// FLACLevel selects the FLAC compression level for flac output: 1
	// through 8 literally, FLACLevelDefault (the zero value) for the
	// encoder default, and FLACLevelFastest for level 0, which needs a
	// sentinel because the zero value cannot mean it without stealing
	// the default. Levels trade encode speed for size and never affect
	// decoded audio.
	FLACLevel int
	// WavPackLevel selects the WavPack compression level for wavpack
	// output: WavPackLevelFast through WavPackLevelVeryHigh literally, and
	// WavPackLevelDefault (the zero value) for the encoder default, which
	// is normal. Levels choose how deep a decorrelation cascade each block
	// runs: they trade encode speed for size and never affect decoded
	// audio.
	WavPackLevel int
	// APELevel selects the Monkey's Audio compression level for ape
	// output: APELevelFast, APELevelNormal, or APELevelHigh literally, and
	// APELevelDefault (the zero value) for the encoder default, which is
	// normal. Levels choose the filter cascade each frame runs through:
	// they trade encode and decode speed for size and never affect decoded
	// audio. The format's two deeper levels decode here but are not
	// written; see ape.MaxEncodeLevel.
	APELevel int
	// MP3Bitrate selects the constant bit rate in bits per second for mp3
	// output; the zero value uses the encoder default (128000). It must be
	// a legal Layer III CBR rate for the output sample rate. Under MP3VBR
	// it anchors the quality level instead.
	MP3Bitrate int
	// MP3VBR selects variable bit rate for mp3 output: each frame carries
	// the smallest legal bit-rate index that holds its psychoacoustic
	// demand, anchored at MP3Bitrate. The zero value is constant bit rate.
	MP3VBR bool
	// OpusBitrate selects the target bit rate in bits per second for opus
	// output; the zero value uses the encoder default (96000).
	OpusBitrate int
	// AACBitrate selects the target bit rate in bits per second for aac
	// output; the zero value uses the encoder default (128000). AAC
	// frames are variable-size, so the encoder holds the long-term mean
	// at the target with a bit reservoir.
	AACBitrate int
	// HEAACv2 selects HE-AAC v2 (parametric stereo over a mono SBR core,
	// an AOT-29 stream) for he-aac output, and drops the zero-AACBitrate
	// default to 32000. Selection is explicit rather than
	// bitrate-automatic: an auto threshold would silently switch stereo
	// coding technology across a bitrate boundary. Stereo sources only;
	// a mono source is refused at plan time (encode v1 instead). Other
	// formats ignore it.
	HEAACv2 bool
	// OpusComplexity gates the Opus encoder's analysis depth: 1 through 10
	// literally, OpusComplexityDefault (the zero value) for the encoder
	// default (5), and OpusComplexityLowest for complexity 0, which needs a
	// sentinel because the zero value cannot mean it without stealing the
	// default. Higher is slower and higher quality.
	OpusComplexity int
	// OpusVBR selects variable bit rate for opus output, sizing each frame to its
	// content around OpusBitrate. The zero value is constant bit rate.
	OpusVBR bool
	// OpusSignal hints the opus encoder about the content type: "voice"
	// biases the speech/music mode decision toward SILK/hybrid (audiobooks,
	// podcasts), "music" toward CELT. The zero value ("" or "auto") lets the
	// encoder's analyser decide per frame.
	OpusSignal string
	// VorbisQuality selects VBR quality for vorbis output on libvorbis's -q
	// scale (-1..10); higher is larger and better. The zero value uses the
	// encoder default (3.0). Vorbis is natively quality-driven, so this is the
	// primary knob; a small nonzero value near 0 reaches the lowest qualities
	// (the zero value cannot, matching the "0 means default" idiom).
	VorbisQuality float64
	// VorbisBitrate is a reserved ABR target in bits per second for vorbis
	// output. ABR rate control is not implemented, so a nonzero value is
	// rejected at plan time rather than silently ignored; leave it 0 for
	// quality-driven VBR.
	VorbisBitrate int
	// Shaping selects the dither strategy for quantization; the default
	// is flat TPDF.
	Shaping dither.Shaping
	// ResampleProfile selects resampler quality; empty means resample.HQ.
	ResampleProfile resample.Profile
	// Tags embeds canonical metadata fields (TITLE, ARTIST, ...) in the
	// output where the muxer can represent them in its stream form: Ogg
	// OpusTags, a FLAC VORBIS_COMMENT block, an MP3 ID3v2 tag, MP4 ilst
	// atoms. Formats without stream-form tagging (WAV, AIFF, ADTS)
	// ignore them; a finished file gets full metadata from the mapping
	// post-pass instead. Tags never change the plan: callers keying
	// cached bytes must fold the tag values into their own key.
	Tags []container.Tag
	// Chapters embeds chapter markers. Only the MP4 muxer represents
	// them (Nero chpl); the mapping post-pass covers finished files of
	// the other formats.
	Chapters []container.Chapter
	// Art embeds cover art. Only the MP4 muxer represents it (the ilst
	// covr atom); art inflates the pre-audio init header, so live
	// streams should leave it nil.
	Art *container.Picture
	// Progress, when non-nil, is called after each encoded chunk with
	// the encoder-input samples consumed so far and the projected total
	// (-1 unknown). It runs on the transcoding goroutine, so blocking it
	// pauses the pipeline; the job runner's yield-to-live-streams check
	// rides on exactly that.
	Progress func(done, total int64)
}

TranscodeOptions selects the Transcode output, with the DSP chain (resample, mix, gain, dither) between decode and encode. Zero values keep the source's properties, so the zero options add no DSP stage and the decoder's samples reach the encoder unaltered: a bit-exact container rewrite for a lossless source to a lossless output. A lossy source is decoded and re-encoded even so, which costs a generation.

Remux is what removes that generation, and the options it accepts are exactly the ones described above: zero everywhere but Format and Container. It moves the source's own packets rather than decoding them, so it is a bit-exact container rewrite for a lossy source too, but only where the codec survives the trip (the output format's codec must already be the source's). PlanRemux answers whether a given request is one of those, and the server's ladder asks it before reaching for a transcode.

type TranscodePlan

type TranscodePlan struct {
	// Format is the output PCM format.
	Format audio.Format
	// Container is the output container name.
	Container string
	// MediaType is the output's HTTP media type.
	MediaType string
	// Live reports whether the container has a streaming form (a muxer
	// that does not need a seekable destination).
	Live bool
	// Versions are the version constants of every sample-affecting node,
	// source decoder, then DSP chain, then encoder, for the cache key:
	// a decoder revision must invalidate cached transcodes of
	// that codec's sources just as an encoder revision invalidates its
	// outputs.
	Versions []string
	// Samples is the projected output length from FromSample to the end,
	// -1 when the source length is unknown.
	Samples int64
	// BytesPerFrame is the output wire size of one frame across channels.
	BytesPerFrame int
	// FrameSize is the encoder-native frame length in output samples (the
	// chain framer's chunk), 0 for formats that accept any chunk length.
	// Segmented (HLS) outputs snap their boundaries to it.
	FrameSize int
	// BitRate is the projected output bit rate in bits per second, 0 when
	// unknown. PCM outputs derive it from the wire format; lossy encoders
	// will report their target rate here.
	BitRate int
	// EstimatedBytes is the projected total output size including the
	// nominal container header, -1 when the source length is unknown. A
	// hint for players, not a promise.
	EstimatedBytes int64
}

TranscodePlan describes what a transcode would produce, computed from headers alone: no decoding, no output. The HTTP layer plans before it runs, because the ADR-0004 cache key (node versions) and the response headers (duration, size estimate) must exist before any pipeline does.

type TranscodeResult

type TranscodeResult struct {
	// Samples is the number of frames written.
	Samples int64
	// Format is the PCM format of the output track.
	Format audio.Format
	// Container is the output container name.
	Container string
	// ClippedSamples counts samples that reached the quantizer beyond full
	// scale: channel samples, not frames, against Samples*Format.Channels.
	// Gain and resampler overshoot land here too (see dsp.Chain.Clipped).
	// Zero for float outputs and copy rungs; the concat timeline's own
	// clamps sit upstream and go uncounted.
	ClippedSamples int64
	// TruePeak is the output's true-peak level, linear, 1.0 = full scale;
	// AnalyzeResult.TruePeakDB is 20*log10 of it. Lossy outputs measure the
	// encoder's input. Zero for copy rungs, pure integer passes, silence.
	TruePeak float64
	// Quantized reports whether a quantizer ran (float cut to integer).
	// It gates the true-peak note; see LevelNote.
	Quantized bool
}

TranscodeResult reports what Transcode produced.

func (*TranscodeResult) LevelNote

func (r *TranscodeResult) LevelNote() string

LevelNote returns the one warning line the level fields warrant, or "": the clip count, else the true peak when a quantizer ran and only the waveform between samples is over. Copy and float paths stay silent, so the note cannot nag on every pass. Callers may append remedies.

Directories

Path Synopsis
Package audio defines WaxFlow's PCM model: sample formats, channel layouts, and the planar dual-domain Buffer that every decoder, DSP node, and encoder exchanges.
Package audio defines WaxFlow's PCM model: sample formats, channel layouts, and the planar dual-domain Buffer that every decoder, DSP node, and encoder exchanges.
cli module
Package client is the Go API client for a WaxFlow daemon (WaxSeal client/ precedent): thin typed wrappers over the HTTP surface (control, playback, timelines, and the jobs lifecycle) plus an offline signed-URL mint helper, so the users and CLI never reimplement canonicalization or envelope decoding.
Package client is the Go API client for a WaxFlow daemon (WaxSeal client/ precedent): thin typed wrappers over the HTTP surface (control, playback, timelines, and the jobs lifecycle) plus an offline signed-URL mint helper, so the users and CLI never reimplement canonicalization or envelope decoding.
Package codec defines the compressed-domain types and the Decoder and Encoder interfaces every WaxFlow codec implements (ADR-0005).
Package codec defines the compressed-domain types and the Decoder and Encoder interfaces every WaxFlow codec implements (ADR-0005).
aac
Package aac implements AAC-LC, HE-AAC v1, and HE-AAC v2 decoders and encoders (ISO/IEC 14496-3), written from the specification and Bosi/Goldberg (clean-room: AAC reference codecs were behavioral references only, never opened while implementing; the QMF and SBR/PS parameter tables are spec data).
Package aac implements AAC-LC, HE-AAC v1, and HE-AAC v2 decoders and encoders (ISO/IEC 14496-3), written from the specification and Bosi/Goldberg (clean-room: AAC reference codecs were behavioral references only, never opened while implementing; the QMF and SBR/PS parameter tables are spec data).
alac
Package alac implements an Apple Lossless (ALAC) decoder.
Package alac implements an Apple Lossless (ALAC) decoder.
ape
Package ape implements a Monkey's Audio (APE) decoder and encoder, ported from the reference Monkey's Audio SDK (see THIRD-PARTY-NOTICES.md): the range coder and its two models, the cascaded neural filters, and the adaptive predictor, so decodes are bit-exact and encodes come out as the reference's own bytes.
Package ape implements a Monkey's Audio (APE) decoder and encoder, ported from the reference Monkey's Audio SDK (see THIRD-PARTY-NOTICES.md): the range coder and its two models, the cascaded neural filters, and the adaptive predictor, so decodes are bit-exact and encodes come out as the reference's own bytes.
flac
Package flac implements a FLAC decoder (RFC 9639), written from the specification.
Package flac implements a FLAC decoder (RFC 9639), written from the specification.
mp3
Package mp3 implements an MPEG-1/2/2.5 Layer III audio decoder (ISO/IEC 11172-3 and 13818-3) in pure Go.
Package mp3 implements an MPEG-1/2/2.5 Layer III audio decoder (ISO/IEC 11172-3 and 13818-3) in pure Go.
opus
Package opus decodes Opus audio (RFC 6716, with the RFC 8251 errata) carried in Ogg (RFC 7845).
Package opus decodes Opus audio (RFC 6716, with the RFC 8251 errata) carried in Ogg (RFC 7845).
pcm
Package pcm implements the PCM "codec": the bridge between raw interleaved wire bytes inside containers (WAV, AIFF, and later MP4 and Matroska PCM tracks) and the pipeline's planar audio.Buffer domain.
Package pcm implements the PCM "codec": the bridge between raw interleaved wire bytes inside containers (WAV, AIFF, and later MP4 and Matroska PCM tracks) and the pipeline's planar audio.Buffer domain.
vorbis
Package vorbis decodes Vorbis I audio (the Xiph "Vorbis I specification").
Package vorbis decodes Vorbis I audio (the Xiph "Vorbis I specification").
wavpack
Package wavpack implements a WavPack decoder.
Package wavpack implements a WavPack decoder.
wma
Package wma decodes Windows Media Audio v1 (wFormatTag 0x0160) and v2 (0x0161), the two codecs carried by ASF files that container/asf reads.
Package wma decodes Windows Media Audio v1 (wFormatTag 0x0160) and v2 (0x0161), the two codecs carried by ASF files that container/asf reads.
Package container defines the demuxer, muxer, and seeker interfaces and the track-routed packet model that wrap codec-level packets (ADR-0005).
Package container defines the demuxer, muxer, and seeker interfaces and the track-routed packet model that wrap codec-level packets (ADR-0005).
adts
Package adts demuxes the ADTS elementary stream framing for AAC (ISO/IEC 14496-3 1.A).
Package adts demuxes the ADTS elementary stream framing for AAC (ISO/IEC 14496-3 1.A).
aiff
Package aiff reads and writes AIFF and AIFF-C, the Apple/SGI audio container.
Package aiff reads and writes AIFF and AIFF-C, the Apple/SGI audio container.
apen
Package apen reads and writes native Monkey's Audio framing: the descriptor, format header, and mandatory seek table a .ape file opens with, the run of frames behind them, and the APEv2 tag most files carry after the audio.
Package apen reads and writes native Monkey's Audio framing: the descriptor, format header, and mandatory seek table a .ape file opens with, the run of frames behind them, and the APEv2 tag most files carry after the audio.
asf
Package asf demuxes the Advanced Systems Format container, the .wma/.asf file Windows Media Audio ships in: a flat tree of GUID-tagged objects (header, data, index) whose Data Object holds a run of fixed-size packets, each carrying one or more payload fragments that reassemble into media objects.
Package asf demuxes the Advanced Systems Format container, the .wma/.asf file Windows Media Audio ships in: a flat tree of GUID-tagged objects (header, data, index) whose Data Object holds a run of fixed-size packets, each carrying one or more payload fragments that reassemble into media objects.
flacn
Package flacn demuxes native FLAC framing (RFC 9639): the fLaC marker, metadata blocks, and the self-framing audio stream.
Package flacn demuxes native FLAC framing (RFC 9639): the fLaC marker, metadata blocks, and the self-framing audio stream.
internal/apev2
Package apev2 parses APEv2 tags: the key/value block WavPack and Monkey's Audio files carry, almost always appended after the audio.
Package apev2 parses APEv2 tags: the key/value block WavPack and Monkey's Audio files carry, almost always appended after the audio.
internal/id3
Package id3 parses the byte length of an ID3v2 tag, from its header at the front of a stream or from the footer of one appended to the back.
Package id3 parses the byte length of an ID3v2 tag, from its header at the front of a stream or from the footer of one appended to the back.
internal/srcwin
Package srcwin is the shared read-ahead window demuxers scan through: byte access over a container.Source with chunked read-ahead, forward extension, rebasing, and a sticky I/O error that the owner surfaces on its packet and seek paths.
Package srcwin is the shared read-ahead window demuxers scan through: byte access over a container.Source with chunked read-ahead, forward extension, rebasing, and a sticky I/O error that the owner surfaces on its packet and seek paths.
internal/trailer
Package trailer peels the non-audio structures taggers bolt onto the end of a file: an APEv2 tag, an ID3v1 tag, an ID3v2 tag appended after the audio, and NUL padding, stacked in any order.
Package trailer peels the non-audio structures taggers bolt onto the end of a file: an APEv2 tag, an ID3v1 tag, an ID3v2 tag appended after the audio, and NUL padding, stacked in any order.
mka
Package mka demuxes Matroska and WebM (ISO/IEC 14496 EBML) audio: the .mka/.mkv/.webm family carrying Opus, Vorbis, FLAC, AAC-LC, or PCM.
Package mka demuxes Matroska and WebM (ISO/IEC 14496 EBML) audio: the .mka/.mkv/.webm family carrying Opus, Vorbis, FLAC, AAC-LC, or PCM.
mp4
Package mp4 demuxes ISO base media files (ISO/IEC 14496-12) and their QuickTime kin: the .m4a/.m4b/.mp4 family carrying AAC-LC or ALAC audio.
Package mp4 demuxes ISO base media files (ISO/IEC 14496-12) and their QuickTime kin: the .m4a/.m4b/.mp4 family carrying AAC-LC or ALAC audio.
mpa
Package mpa demuxes the MP3 elementary stream: a bare sequence of Layer III frames, usually wrapped in ID3 tags, often led by a Xing, Info, or VBRI metadata frame.
Package mpa demuxes the MP3 elementary stream: a bare sequence of Layer III frames, usually wrapped in ID3 tags, often led by a Xing, Info, or VBRI metadata frame.
ogg
Package ogg demuxes Ogg streams (RFC 3533): page parsing with CRC verification, packet reassembly across pages, and per-mapping content handling.
Package ogg demuxes Ogg streams (RFC 3533): page parsing with CRC verification, packet reassembly across pages, and per-mapping content handling.
riff
Package riff reads and writes RIFF/WAVE, the WAV container, including the RF64/BW64 64-bit extension: RF64 is always read, and the muxer switches to it automatically when output projects past RIFF's 4 GiB size fields (24-bit/96 kHz audiobooks overflow plain WAV at about two hours; decided here, not discovered in production).
Package riff reads and writes RIFF/WAVE, the WAV container, including the RF64/BW64 64-bit extension: RF64 is always read, and the muxer switches to it automatically when output projects past RIFF's 4 GiB size fields (24-bit/96 kHz audiobooks overflow plain WAV at about two hours; decided here, not discovered in production).
wv
Package wv demuxes native WavPack framing: the self-delimiting run of "wvpk" blocks a .wv file is, plus the APEv2 tag most of them carry after the audio.
Package wv demuxes native WavPack framing: the self-delimiting run of "wvpk" blocks a .wv file is, plus the APEv2 tag most of them carry after the audio.
dsp
Package dsp assembles the transcode pipeline's PCM processing chain:
Package dsp assembles the transcode pipeline's PCM processing chain:
dither
Package dither requantizes float PCM to integer bit depths.
Package dither requantizes float PCM to integer bit depths.
fft
Package fft provides the shared forward complex FFT kernel the codec transforms build on (the CELT MDCT today; any future MDCT/DFT consumer).
Package fft provides the shared forward complex FFT kernel the codec transforms build on (the CELT MDCT today; any future MDCT/DFT consumer).
gain
Package gain scales PCM level: a plain scalar gain kernel, plus a look-ahead true-peak limiter the chain inserts whenever the level path can clip (net positive gain, or a downmix whose worst-case matrix gain exceeds unity).
Package gain scales PCM level: a plain scalar gain kernel, plus a look-ahead true-peak limiter the chain inserts whenever the level path can clip (net positive gain, or a downmix whose worst-case matrix gain exceeds unity).
internal/firwin
Package firwin holds the windowed-sinc design primitives shared by the DSP kernels that build FIR filters at runtime (the resampler's polyphase banks, the limiter's true-peak interpolator).
Package firwin holds the windowed-sinc design primitives shared by the DSP kernels that build FIR filters at runtime (the resampler's polyphase banks, the limiter's true-peak interpolator).
loudness
Package loudness implements the ITU-R BS.1770-4 / EBU R128 loudness meter behind the engine's analysis jobs: gated integrated loudness, loudness range per EBU Tech 3342, and oversampled true peak.
Package loudness implements the ITU-R BS.1770-4 / EBU R128 loudness meter behind the engine's analysis jobs: gated integrated loudness, loudness range per EBU Tech 3342, and oversampled true peak.
mix
Package mix converts channel layouts with per-stream gain matrices.
Package mix converts channel layouts with per-stream gain matrices.
psy
Package psy is the shared psychoacoustic model behind the lossy encoders.
Package psy is the shared psychoacoustic model behind the lossy encoders.
resample
Package resample converts PCM sample rates with a streaming Kaiser windowed-sinc polyphase filter.
Package resample converts PCM sample rates with a streaming Kaiser windowed-sinc polyphase filter.
silence
Package silence maps the near-silent spans of a stream: the detector behind the analyze job's silence half, which a library manager reads to trim leading and trailing pauses or to propose track boundaries.
Package silence maps the near-silent spans of a stream: the detector behind the analyze job's silence half, which a library manager reads to trim leading and trailing pauses or to propose track boundaries.
Package format identifies containers and opens them as decodable media: a bounded magic-byte sniff over an ordered driver table (extension hints only break ties), then demuxer plus decoder wired into a Media that reads planar PCM chunks and seeks sample-exact.
Package format identifies containers and opens them as decodable media: a bounded magic-byte sniff over an ordered driver table (extension hints only break ties), then demuxer plus decoder wired into a Media that reads planar PCM chunks and seeks sample-exact.
internal
admission
Package admission bounds concurrent live pipeline work: one slot per interactive stream or sync one-shot (default max(1, NumCPU-1)).
Package admission bounds concurrent live pipeline work: one slot per interactive stream or sync one-shot (default max(1, NumCPU-1)).
cache
Package cache is the transcode cache and the delivery model built on it.
Package cache is the transcode cache and the delivery model built on it.
config
Package config loads WaxFlow configuration with the Wax-family precedence: flag > WAXFLOW_* environment variable > JSON config file > built-in default.
Package config loads WaxFlow configuration with the Wax-family precedence: flag > WAXFLOW_* environment variable > JSON config file > built-in default.
cue
Package cue parses CUE sheets, the sidecar index that pairs a single-file CD rip with the track boundaries the disc itself had.
Package cue parses CUE sheets, the sidecar index that pairs a single-file CD rip with the track boundaries the disc itself had.
flight
Package flight provides keyed call deduplication: concurrent calls with the same key share one execution and its result.
Package flight provides keyed call deduplication: concurrent calls with the same key share one execution and its result.
hls
Package hls is the service plumbing behind the HLS surface: the v= descriptor every HLS URL carries, the playlist writers, and the variant worker manager.
Package hls is the service plumbing behind the HLS surface: the v= descriptor every HLS URL carries, the playlist writers, and the variant worker manager.
jobs
Package jobs is the async job store and runner: full-file transcodes and loudness analyses that outlive any request, with restart-safe file-backed state under dataDir/jobs.
Package jobs is the async job store and runner: full-file transcodes and loudness analyses that outlive any request, with restart-safe file-backed state under dataDir/jobs.
meta
Package meta defines WaxFlow's metadata model and the Mapper seam between it and the tag library.
Package meta defines WaxFlow's metadata model and the Mapper seam between it and the tag library.
metrics
Package metrics is WaxFlow's hand-rolled Prometheus surface (plan section 9): a fixed set of counters, gauges, and one histogram, exposed in text format by GET /metrics.
Package metrics is WaxFlow's hand-rolled Prometheus surface (plan section 9): a fixed set of counters, gauges, and one histogram, exposed in text format by GET /metrics.
muxseek
Package muxseek holds the muxers' shared back-patch convention.
Package muxseek holds the muxers' shared back-patch convention.
posixfs
Package posixfs gives file publishes and the handles around them POSIX semantics on every platform.
Package posixfs gives file publishes and the handles around them POSIX semantics on every platform.
sign
Package sign implements ADR-0003 signed playback URLs: exp + kid + sig query parameters carrying a base64url HMAC-SHA256 over the canonical string
Package sign implements ADR-0003 signed playback URLs: exp + kid + sig query parameters carrying a base64url HMAC-SHA256 over the canonical string
testutil
Package testutil is the shared test harness: the ffmpeg/ffprobe differential oracle, PCM comparison helpers, deterministic signal synthesis, and the SHA-256-pinned conformance-vector fetcher.
Package testutil is the shared test harness: the ffmpeg/ffprobe differential oracle, PCM comparison helpers, deterministic signal synthesis, and the SHA-256-pinned conformance-vector fetcher.
testutil/cmd/vectorfetch command
Command vectorfetch downloads the SHA-256-pinned conformance vectors into testdata/vectors.
Command vectorfetch downloads the SHA-256-pinned conformance vectors into testdata/vectors.
timeline
Package timeline is the content-addressed store behind multi-source HLS timelines: the {src, id} member lists that an HLS descriptor's tl digest names.
Package timeline is the content-addressed store behind multi-source HLS timelines: the {src, id} member lists that an HLS descriptor's tl digest names.
ulid
Package ulid mints ULIDs: 26-character Crockford base32 identifiers packing a 48-bit millisecond timestamp ahead of 80 bits of entropy, so ids sort lexicographically in creation order.
Package ulid mints ULIDs: 26-character Crockford base32 identifiers packing a 48-bit millisecond timestamp ahead of 80 bits of entropy, so ids sort lexicographically in creation order.
uploads
Package uploads is the spool for one-shot upload sources: a client POSTs bytes, receives a ULID, and references it as src=upload:<id>.
Package uploads is the spool for one-shot upload sources: a client POSTs bytes, receives a ULID, and references it as src=upload:<id>.
Package server is WaxFlow's HTTP service: the progressive streaming surface over the engine.
Package server is WaxFlow's HTTP service: the progressive streaming surface over the engine.
Package source resolves source references onto opened, validated files.
Package source resolves source references onto opened, validated files.
Package waxerr defines WaxFlow's error taxonomy: machine-readable kebab-case codes shared across the HTTP boundary, sentinel errors for errors.Is classification, a boundary error type for errors.AsType extraction, and the documented CLI exit-code contract printed by `waxflow exit-codes`.
Package waxerr defines WaxFlow's error taxonomy: machine-readable kebab-case codes shared across the HTTP boundary, sentinel errors for errors.Is classification, a boundary error type for errors.AsType extraction, and the documented CLI exit-code contract printed by `waxflow exit-codes`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL