The processing-options grammar (API doc §3): parse, validate, normalize.
Options are the API. A variant is fully described by its options segments, and the normalized options string is what the cache key hashes — so this module owns three obligations, in order:
parse/1—/-separatedkey:valuesegments intot/0, with per-key value domains enforced.validate/1— cross-key rules that no single parser can see (brexcludesq,bdis lossless-only, …).parse/1runs it for you.normalize/1— back to a canonical string: defaults materialized, keys sorted, numbers rendered minimally.
Normalization is the determinism boundary. Any two option strings
describing the same variant must produce byte-identical normalized forms,
because AudioProxy.CacheKey hashes that string directly. Two rules keep
it stable: decimals are capped at millisecond precision (three places) at
parse time, and every number is rendered by the same render_number/1.
Failures are AudioProxy.OptionError.t/0 values naming the offending
segment; the HTTP slice maps them to 422.
iex> {:ok, opts} = AudioProxy.Options.parse("br:96/f:opus")
iex> AudioProxy.Options.normalize(opts)
"br:96/f:opus"Known limits
Semantic no-ops keep their own identity: t:0 (a trim from zero to the end),
fade:0:0 and gain:0 render exactly what the bare options render, but
normalize to distinct strings and therefore distinct cache keys. The mapping
from options to key is syntactic on purpose; collapsing no-ops is tracked as
follow-up work rather than done silently here.
Summary
Types
loudnorm targets: integrated LUFS, true peak dBTP, loudness range LU.
A parsed variant description.
Functions
Renders opts as its canonical options string — the cache-key input.
Parses and normalizes in one step.
Parses an options string (or its already-split segments) into t/0.
How many channels a peaks render reduces.
How many min/max pairs opts asks for. Meaningful under f:peaks only.
Which serialization opts asks for. Meaningful under f:peaks only.
Renders a number in the canonical minimal form used across the round-trip.
Checks the cross-key rules that per-key parsing cannot see.
Types
@type bit_depth() :: :bd16 | :bd24 | :bd32f
@type format() :: :mp3 | :opus | :ogg | :aac | :m4a | :flac | :wav | :peaks
loudnorm targets: integrated LUFS, true peak dBTP, loudness range LU.
@type peak_format() :: :json | :dat
@type t() :: %AudioProxy.Options{ bit_depth: bit_depth() | nil, bitrate: pos_integer() | nil, cache_buster: String.t() | nil, channels: 1 | 2 | nil, download: String.t() | nil, fade_in: float() | nil, fade_out: float() | nil, format: format(), gain: float() | nil, norm: norm() | nil, peak_count: pos_integer() | nil, peak_format: peak_format() | nil, quality: float() | nil, sample_rate: pos_integer() | nil, trim_duration: float() | nil, trim_start: float() | nil }
A parsed variant description.
nil means "not given": the renderer falls back to the source's own
property (sample_rate, channels) or skips the stage entirely (gain,
norm, trim_start). trim_duration is nil for an open-ended trim
(t:30 runs to the end of the source).
Functions
@spec keys() :: [String.t()]
The option keys recognized by parse/1, in canonical (sorted) order.
Renders opts as its canonical options string — the cache-key input.
Keys are sorted lexicographically, applicable defaults are materialized
(f, and ch/pts/pk_fmt under f:peaks, and the norm targets when
norm is present), and numbers are rendered minimally (30, not 30.0).
The result always re-parses, and normalizing it again is byte-identical.
iex> {:ok, opts} = AudioProxy.Options.parse("norm:ebu")
iex> AudioProxy.Options.normalize(opts)
"f:mp3/norm:ebu:-16:-1.5:11"
@spec normalize_string(String.t() | [String.t()]) :: {:ok, String.t()} | {:error, AudioProxy.OptionError.t()}
Parses and normalizes in one step.
iex> AudioProxy.Options.normalize_string("br:96/f:opus")
{:ok, "br:96/f:opus"}
@spec parse(String.t() | [String.t()]) :: {:ok, t()} | {:error, AudioProxy.OptionError.t()}
Parses an options string (or its already-split segments) into t/0.
Accepts "f:opus/br:96" or ["f:opus", "br:96"]; an empty string yields
the defaults. Unknown keys, repeated keys, and out-of-domain values fail,
and validate/1 runs on the result so cross-key conflicts fail here too.
iex> {:ok, opts} = AudioProxy.Options.parse("f:opus/t:12.5:30")
iex> {opts.format, opts.trim_start, opts.trim_duration}
{:opus, 12.5, 30.0}
@spec peak_channels(t()) :: 1 | 2
How many channels a peaks render reduces.
Unlike every other format, f:peaks does not follow the source when ch is
absent: it downmixes to mono. A waveform UI draws one shape, and following a
stereo source would double the payload for a picture almost nobody asks for.
ch:2 still gets per-channel peaks.
iex> {:ok, opts} = AudioProxy.Options.parse("f:peaks")
iex> AudioProxy.Options.peak_channels(opts)
1
@spec peak_count(t()) :: pos_integer()
How many min/max pairs opts asks for. Meaningful under f:peaks only.
iex> {:ok, opts} = AudioProxy.Options.parse("f:peaks")
iex> AudioProxy.Options.peak_count(opts)
800
@spec peak_format(t()) :: peak_format()
Which serialization opts asks for. Meaningful under f:peaks only.
iex> {:ok, opts} = AudioProxy.Options.parse("f:peaks")
iex> AudioProxy.Options.peak_format(opts)
:json
Renders a number in the canonical minimal form used across the round-trip.
Whole values lose their fraction entirely (30, not 30.0) and fractions
keep at most the three places parsing allowed. Every number in a normalized
options string goes through here, and so does every number
AudioProxy.Ffmpeg.Command puts in a filter expression — one rendering, so
the options string and the ffmpeg arguments cannot disagree about a value.
iex> AudioProxy.Options.render_number(30.0)
"30"
@spec validate(t()) :: {:ok, t()} | {:error, AudioProxy.OptionError.t()}
Checks the cross-key rules that per-key parsing cannot see.
Run by parse/1; public so a hand-built struct can be checked too. Rules, in
the order they are reported: br excludes q; bd needs a lossless format;
br needs a lossy format, and q needs a format with a quality scale and a
value inside that codec's range; bd needs
a lossless format, and bd:32f needs f:wav; f:peaks refuses encoding and
loudness options; pts/pk_fmt need f:peaks; sr is capped at 48 kHz for
lossy formats; a fade must fit inside a bounded trim, and a fade-out needs
that trim to be bounded at all.