Simple standard codecs and containers for oxideav (PCM, WAV, ...)
Part of the oxideav framework — a pure-Rust media transcoding and streaming stack. Codec, container, and filter crates are implemented from the spec (no C codec libraries linked or wrapped, no *-sys crates). Optional hardware-engine crates (oxideav-videotoolbox / -audiotoolbox / -vaapi / -vdpau / -nvidia / -vulkan-video) bridge to OS APIs via runtime libloading; pass --no-hwaccel (or omit the hwaccel feature) to opt out.
- PCM codecs:
pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_f64le. - WAV container: RIFF/WAVE (plus EBU Tech 3306 / ITU-R BS.2088
RF64andBW6464-bit-extended forms) demuxer + muxer withfmt,data, and the full Microsoft RIFF MCI §3 "INFO List Chunk" baseline (23 sub-IDs from the 1991 spec:IARL→archival_location,IART→artist,ICMS→commissioned,ICMT→comment,ICOP→copyright,ICRD→date,ICRP→cropped,IDIM→dimensions,IDPI→dpi,IENG→engineer,IGNR→genre,IKEY→keywords,ILGT→lightness,IMED→medium,INAM→title,IPLT→palette_setting,IPRD→album,ISBJ→subject,ISFT→encoder,ISHP→sharpness,ISRC→source,ISRF→source_form,ITCH→technician; non-baselineITRK→trackretained for compatibility). Also recognises the extendedINFOsub-ID namespace catalogued in ExifTool's RIFF Info Tags table (docs/container/riff/metadata/exiftool-riff-tags.html): the per-stream audio-language slotsIAS1..IAS9→first_language..ninth_language, the Windows-Media "more info" set (IBSU→base_url,ICAS→default_audio_stream,ILGU→logo_url,ILIU→logo_icon_url,IMBI→more_info_banner_image,IMBU→more_info_banner_url,IMIT→more_info_text,IMIU→more_info_url,IWMU→watermark_url), and the common production-credit / cataloguing tags (ICDS→costume_designer,ICNM→cinematographer,ICNT→country,IDIT→date_time_original,IDST→distributed_by,IEDT→edited_by,IENC→encoded_by,ILNG→language,IMUS→music_by,IPDS→production_designer,IPRO→produced_by,IRIP→ripped_by,IRTD→rating,ISGN→secondary_genre,ISMP→time_code,ISTD→production_studio,ISTR→starring,IWRI→written_by); unknown sub-IDs are skipped silently). DispatchesWAVE_FORMAT_ALAW (0x0006)/WAVE_FORMAT_MULAW (0x0007)to thepcm_alaw/pcm_mulawcodecs (host runtime applies G.711 decode).WAVE_FORMAT_EXTENSIBLE (0xFFFE)is parsed end-to-end — the 22-byte extension'swValidBitsPerSample,dwChannelMaskand SubFormat GUID are surfaced through bothwav:fmt.*metadata keys and typed accessors on the concreteWavDemuxer. ThedwChannelMaskbitmap is also decoded into a human-readableSPEAKER_*layout (wav:fmt.channel_layout+WavDemuxer::channel_layout),+-joined least-significant-bit-first per the 18 documented flag bits (FRONT_LEFT 0x1..TOP_BACK_RIGHT 0x20000) indocs/container/riff/waveformatextensible/ms-waveformatextensible.html; bits above the highest defined flag are preserved asUNKNOWN(0x...). Any SubFormat GUID built from theKSMedia.hDEFINE_WAVEFORMATEX_GUID(x)template —{0000xxxx-0000-0010-8000- 00AA00389B71}, where the leading 16 bits carry the legacywFormatTagx— resolves through the SAMEwFormatTagdispatch the legacyWAVEFORMATEXpath uses, implementing the documentedIS_VALID_WAVEFORMATEX_GUID/EXTRACT_WAVEFORMATEX_IDmacros fromdocs/container/riff/waveformatextensible/ms-converting-format-tags-and-subformat-guids.md. This generalises the four hand-listedKSDATAFORMAT_SUBTYPE_*GUIDs (PCM0x0001, IEEE_FLOAT0x0003, ALAW0x0006, MULAW0x0007) to every tag-derived GUID, and surfaces the embedded tag aswav:fmt.subformat_tag(e.g. an EXTENSIBLE file whose SubFormat is{00000055-...}is observably MP3-tagged even though this crate doesn't decode MP3). Template GUIDs whose embedded tag isn't a format this crate maps directly, and non-template (unknown) GUIDs, both synthesise awav:guid_<canonical-text>id.WavMuxOptions::with_extensible(mask)opts the muxer into writing a 40-byte EXTENSIBLEfmtchunk. ThebextBroadcast Audio Extension chunk (EBU Tech 3285 v2 §2.3) is supported on BOTH the read and write sides through the typedwav::BextChunkstruct (602-byte fixed body + optional variable-lengthCodingHistorytail). The demuxer surfaceswav:bext.*metadata keys — description, originator, origination date/time, 64-bitTimeReference, BWF version, SMPTE-330M UMID (v1+) and the v2 loudness fields (LoudnessValue,LoudnessRange,MaxTruePeakLevel,MaxMomentaryLoudness,MaxShortTermLoudness, each ×100 fixed-point rendered to two decimals) plusCodingHistory— and the typed view is reachable viaWavDemuxer::bext()(raw loudness WORDs + fixed-width UMID exposed for lossless round-trip); the muxer writes the chunk viaWavMuxOptions::with_bext(fixed-width string slots NUL-padded per §2.3, an odd-lengthCodingHistorytriggers the RIFF word-alignment pad byte). Bodies shorter than the 602-byte fixed struct are skipped-as-opaque; theparse/to_bytespair and the mux→demux round-trip are pinned byte-for-byte in tests. Thefactchunk (RIFF MCI §3 "FACT Chunk") is parsed —dwFileSize(per-channel sample count) surfaces aswav:fact.sample_countand becomes the authoritativeStreamInfo::duration(matters for compressed streams wheredata_size / block_alignis meaningless); future-extension bytes past the 4-byte fixed field surface their total underwav:fact.body_len; a fact-vs-heuristic mismatch surfaces aswav:fact.mismatch. The muxer emits afactchunk for every non-PCMwFormatTag(G.711 A-law/μ-law and the EXTENSIBLE escape hatch) per spec, and skips it for plain PCM where it is optional. Thecuechunk,plst(Playlist) chunk andLIST adtl(Associated Data List) sub-chunks are read+write symmetric per Microsoft RIFF MCI §3 via the typedCuePoint/CueChunk,PlaylistSegment/PlaylistChunkandAdtlEntry/AdtlChunk(labl/note/ltxt) surfaces — byte-exactparse/to_bytes,WavMuxOptions::with_cue/with_plst/with_adtlwriters (emitted in the trailer afterdata, the conventional placement for sample-position-referencing chunks), andWavDemuxer::cue()/plst()/adtl()accessors. Because these chunks are commonly written after the waveform, the demuxer no longer stops scanning atdata: it seeks over the word-aligneddatabody and walks the remaining chunks to EOF. The same data is also mirrored through the read-only metadata keys — cue points surface aswav:cue.countplus per-pointwav:cue.<dwName>.position/.fcc_chunk/.chunk_start/.block_start/.sample_offset; playlist segments surface aswav:plst.countplus per-segmentwav:plst.<n>.cue_id/.length/.loops(zero-based segment index<n>because a single cue id can be replayed by multiple playlist entries);labl/notetext sub-chunks surface aswav:adtl.labl.<dwName>/wav:adtl.note.<dwName>; theltxt(text-with-segment-length) sub-chunk surfaces aswav:adtl.ltxt.<dwName>.length/.purpose(FOURCC) /.textplus its four §3 locale WORDs.country/.language/.dialect/.code_page(raw decimals, always emitted) with.country_name/.language_nameresolved through the same §3 Chapter-2 tables theCSETchunk uses (emitted only when the code is in the enumerated set); thefile(embedded media file) sub-chunk surfaceswav:adtl.file.<dwName>.med_type(FOURCC when printable, the spec-allowed zero as0, hex otherwise) and.body_len(embedded payload length — thefileDatabytes themselves are not exposed through the string-typed metadata API); sub-chunks shorter than their fixed headers are skipped as opaque. Thesmpl(Sampler) andinst(Instrument) chunks surface throughwav:smpl.*(manufacturer / product / sample_period / midi_unity_note / midi_pitch_fraction / smpte_format / smpte_offset rendered asHH:MM:SS:FF/ sampler_data_len / num_sample_loops + per-loopwav:smpl.loop.<n>.{cue_point_id,type,start,end,fraction,play_count}) andwav:inst.{unshifted_note,fine_tune,gain,low_note,high_note, low_velocity,high_velocity}(signedfine_tune/gaindecoded asi8). Loop counts that exceed the chunk body are clamped; bodies shorter than the 36-bytesmpl/ 7-byteinstfixed header are treated as opaque. Both chunks are now also supported on the write side (read/write symmetry) through the typedwav::SmplChunk/wav::SampleLoop/wav::InstChunkstructs (parse/to_bytesbyte-lossless — thesmplloop array + vendorsampler_datatail round-trip verbatim,num_sample_loopsre-derived on write):WavMuxOptions::with_smpl/with_instemit the chunks ahead ofdata, with the typed views reachable viaWavDemuxer::smpl()/inst(). TheiXMLthird-party metadata block (the production-recorder schema catalogued in ExifTool's RIFF tag table) is surfaced throughwav:ixml(UTF-8 text payload, trimmed at the first NUL + surrounding whitespace) andwav:ixml.body_len(raw on-wire chunk size, always emitted when the chunk is present so a NUL-padded "reserved for in-place editing" region is still visible to downstream tooling); bodies that are empty or entirely NUL/whitespace surface onlywav:ixml.body_len. The<axml>chunk (EBU Tech 3285 Supplement 5) carries a UTF-8 XML document — typically an EBUCore wrapper around an<audioFormatExtended>ADM document or an ISRC identifier declaration — and surfaces throughwav:axml(text payload trimmed at the first NUL + surrounding whitespace, schema-agnostic) andwav:axml.body_len(always emitted when the chunk is present, so a NUL-padded ADM reservation block reserved for in-place editing is observable). The<bxml>chunk (ITU-R BS.2088-2 §6) is the compressed-XML counterpart of<axml>: a 2-byte LEfmtTypeheader (0x0000= uncompressed,0x0001= gzip per RFC 1952) precedes the (optionally compressed) XML payload. It surfaceswav:bxml.fmt_type(raw0x%04X),wav:bxml.compression(none/gziplabel, omitted for private/future codes so the rawfmt_typestays authoritative),wav:bxml.body_len(full on-wire span including the 2-byte header, so a NUL-reserved in-place-edit block is observable), and — only for the uncompressed form — thewav:bxmltext payload (trimmed at the first NUL + surrounding whitespace, exactly like<axml>). Compressed payloads are not inflated at the container layer (RFC 1952 decode is left to a higher-level ADM-aware consumer); bodies shorter than the 2-bytefmtTypeheader are skipped-as-opaque (onlybody_len). The<sxml>chunk (ITU-R BS.2088-2 §7) is the serialized-XML third ADM carrier alongside<axml>(uncompressed whole document) and<bxml>(compressed whole document) — it is supported on BOTH the read and write sides through the typedwav::SxmlChunk/wav::SubXmlChunk/wav::AlignmentPointstructs. The body is a 14-byte fixed prefix (fmtTypeWORD + 64-bitsubXMLCkTbSize+nSubXMLChunks) followed by an array ofSubXMLChunkrecords — each asubXMLChunkSize/nSamplesSubDataChunkheader plus anxmlData[]payload binding a run of audio samples to an XML fragment, so time-variant / Serial-ADM (BS.2125) metadata can be streamed contiguously with the audio — then an optionalnAlignmentPointscount and a table of 16-byteAlignmentPointrecords (64-bit byte-offset + 64-bit sample timestamp) for timestamp-based random access.parse/to_bytesare byte-lossless (the on-wiresubXMLCkTbSizeis carried verbatim;computed_sub_table_byte_sizereturns the canonical value for writers). The demuxer surfaceswav:sxml.fmt_type(0x%04X),wav:sxml.compression(none/gzip, omitted for private/future codes),wav:sxml.sub_chunk_count,wav:sxml.alignment_point_count,wav:sxml.total_samples(summednSamplesSubDataChunk),wav:sxml.body_len(always, so a malformed/reserved body is observable), and per-recordwav:sxml.<n>.samples/.xml_lenplus — only for the uncompressedfmtType == 0x0000form —.xml(text trimmed at the first NUL + surrounding whitespace, like<axml>); gzip (0x0001) and private codes surface header fields only. The typed view is reachable viaWavDemuxer::sxml()and the muxer writes the chunk viaWavMuxOptions::with_sxml(emitted ahead ofchna/dataper the §2.1 recommended order, RIFF §2 word-aligned). Because<sxml>(likechna) marks an ADM-carrying file, a forced/promoted 64-bit muxer output uses theBW64top-level magic when either is present. Bodies shorter than the 14-byte prefix, or whose declared counts overrun the chunk, are skipped-as-opaque (onlybody_len); the mux→demux round-trip is pinned in tests. The_PMXchunk (Adobe XMP packet, the WAV/AVI carrier for an XMP serialised packet — FOURCC is little-endian "XMP_" reversed; catalogued in exiftool-riff-tags.html § "RIFF Main tags" entry'_PMX', scope "AVI and WAV files") surfaces throughwav:xmp(UTF-8 XMP packet text trimmed at the first NUL + surrounding whitespace, so writers that NUL-pad a fixed-size XMP region for in-place editing do not leak padding into the text key) andwav:xmp.body_len(always emitted when the chunk is present, so an XMP-aware reserved block is observable). Schema-agnostic —<?xpacket begin=...?>/<?xpacket end=...?>and the innerx:xmpmeta/ RDF tree pass through unchanged. TheCSET(Character Set) chunk (RIFF MCI §3 "CSET Chunk") is parsed end-to-end:wCodePage/wCountryCode/wLanguageCode/wDialect(each a 16-bit LE field) surface underwav:cset.code_page/.country/.language/.dialect, the §3 country and(language, dialect)tables resolve to human-readablewav:cset.country_name/wav:cset.language_namekeys, andwav:cset.body_lenis always emitted (so writers that extend the chunk past its canonical 8-byte struct are observable). Bodies shorter than 8 bytes are treated as opaque; bodies longer than 8 bytes tolerate the trailing region for forward compatibility. When the top-level magic isRF64orBW64(the latter signalling an ADM-carrying file per ITU-R BS.2088) the demuxer expects a mandatoryds64chunk immediately afterWAVEper EBU Tech 3306 §3 and Annex A.2. The 28-byte fixed prefix carries the 64-bitriffSize,dataSizeandsampleCountoverrides plus atableLengthcount for an optional array of(chunkId, chunkSize64)records describing other non-datachunks that exceed 4 GiB. The 32-bit on-wire size field on any chunk may be the0xFFFFFFFFsentinel —datais promoted via the dedicateddataSizefield, other chunk-IDs via the table lookup. Surfaceswav:rf64.magic(RF64/BW64),wav:rf64.riff_size,wav:rf64.data_size,wav:rf64.sample_count,wav:rf64.table.countplus per-entrywav:rf64.table.<i>.id/.sizeandwav:rf64.body_len. A sentinel without ads64override is rejected as malformed; ads64body shorter than 28 bytes is rejected. The 32-bit legacyfact.dwFileSizeis promoted to the 64-bitds64.sampleCountwhen it carries the sentinel. The 64-bit form is also supported on the write side (WavMuxOptions::with_rf64(Rf64Mode)), giving read/write symmetry for files larger than 4 GiB.Rf64Mode::Forcealways emits theds64chunk immediately afterWAVE, sets the legacy RIFF /data/factsize fields to the0xFFFFFFFFsentinel, and writes the 64-bitriffSize/dataSize/sampleCountinto the 28-byteds64body (with an emptyChunkSize64table).Rf64Mode::Reserveimplements the ITU-R BS.2088-2 §3.6 / §4.2 on-the-fly conversion: ads64-sizedJUNKplaceholder is written ahead offmtup front, then at finalisation the muxer either leaves it as an inertJUNKchunk (the file stays a plain 32-bitRIFF/WAVE) or — if the finished payload overflows a 32-bit size field — promotes it in place to ads64chunk and flips the top-level magic. The promoted / forced magic isBW64(per ITU-R BS.2088) when an ADMchnachunk is also requested, elseRF64(per EBU Tech 3306).Rf64Mode::Never(the default) keeps short files byte-identical to the historical 32-bit muxer and errors on a >4 GiB payload. TheJUNK(Filler) chunk (RIFF MCI §2 "JUNK (Filler) Chunk") is recognised end-to-end — the chunk body is defined as "no relevant data" so its bytes are not surfaced, but the demuxer accounts for everyJUNKchunk seen:wav:junk.count(total number ofJUNKchunks),wav:junk.total_bytes(cumulative payload bytes across allJUNKchunks; excludes the 8-byte chunk header and the word-align pad), and per-chunkwav:junk.<n>.body_lenindexed zero-based by encounter order. Lets a downstream tool observe how much filler a writer reserved for in-place edits without pretending the bytes carry meaning. MultipleJUNKchunks are allowed; emptyJUNKchunks (size = 0) still increment the count. Files with noJUNKchunk emit nowav:junk.*keys at all (absence is observable). Theslnt(Silence) chunk (RIFF MCI §3 "Wave Data" —slnt( dwSamples:DWORD )) is recognised end-to-end: each chunk's 4-bytedwSamplescount of silent samples surfaces aswav:slnt.<n>.samples(zero-based by encounter order), the rolling aggregateswav:slnt.count/wav:slnt.total_samplesaccumulate, and no real zero/baseline samples are synthesised into the decoded stream (the §3 note is explicit that the correct fill value is context-dependent, not necessarily zero). Bodies shorter than the 4-byte field are counted but treated as opaque (nosampleskey); over-length bodies decode the leading DWORD and tolerate trailing forward-extension bytes. Files with noslntchunk emit nowav:slnt.*keys at all. TheLIST 'wavl'wave-list waveform container (RIFF MCI §3 "Storage of WAVE Data" —<wave-data> -> { <data-ck> | <data-list> },<wave-list> -> LIST('wavl' { <data-ck> | <silence-ck> }... )) is parsed end-to-end: the segmented form interleaves runs of PCM (datasub-chunks) withslntsilence-count markers, so the demuxer resolves the FIRST embeddeddatasub-chunk as the decode anchor (awavl-form file is now decodable rather than yielding no audio) and surfaces every segment aswav:wavl.segment_count/wav:wavl.data_count/wav:wavl.data_bytesplus per-segmentwav:wavl.<n>.kind(data/slnt) /.length. Embeddedslntsegments feed the samewav:slnt.*accounting as top-levelslntchunks (silence is a count, never synthesised baseline samples); thefactchunk (required by §3 forwavl-form data) remains the authoritative duration. A silence-onlywavl(nodatasegment) is rejected as having no waveform; odd-lengthdatasegments respect RIFF word-alignment. The Acidizeracidchunk (layout per the staged byte-indexed Acidizer table indocs/container/riff/metadata/exiftool-riff-tags.html) is supported on BOTH the read and write sides through the typedwav::AcidChunkstruct (24-byte LE body: flags bit-field @0, root note @4, six reserved bytes carried verbatim @6..12, beats @12, meter @16, tempo @20, withparse/to_bytesand the five documented flag-bit helpers plus the 48..=71 root-note name table). The demuxer surfaceswav:acid.*keys (flags hex, each flag bit, root note + name, beats, meter, tempo, plus reserved-hex / body_len observability keys) and the typed view viaWavDemuxer::acid(); the muxer writes the chunk viaWavMuxOptions::with_acid. Truncated bodies are skipped-as-opaque; the mux→demux round-trip is pinned byte-for-byte in tests. The concrete demuxer is now publicly constructible viawav::open_wav_demuxerso every typed accessor is reachable without downcasting. The BW64/ADMchna(channel-allocation) chunk is now supported on BOTH read and write sides per the staged binary layout indocs/container/riff/metadata/bs2088-chna-chunk-layout.md(ITU-R BS.2088-2 §8.1): the typedwav::ChnaChunk/wav::AudioIdstructs (4-bytenumTracks+numUIDspre-amble followed byNfixed 40-byteaudioIDrecords —trackIndexu16 @0,UID[12]ATU_…@2,trackRef[14]AT_…/AC_…@14,packRef[11]AP_…or 11 NULs @28,pad@39) carryparse/to_bytesand the over-provisioning rule (N = (ckSize−4)/40 ≥ numUIDs, spare records markedtrackIndex == 0round-trip verbatim). The demuxer surfaceswav:chna.num_tracks/.num_uids/.record_count/.defined_countplus per-defined-recordwav:chna.<n>.{track_index,uid,track_ref,pack_ref}(fixed-width char fields rendered up to the first NUL, omitted when entirely NUL) andwav:chna.body_lenwhen trailing extension bytes ride along; the typed view is reachable viaWavDemuxer::chna()and the muxer writes the chunk viaWavMuxOptions::with_chna. Each defined record'strackRef/packRefis additionally classified by ADM prefix (AdmRefKind:AT_=audioTrackFormatID,AC_=audioChannelFormat for linear-PCM essence,AP_=audioPackFormatID,ATU_=audioTrackUID) and by definition scope (DefinitionScopeper §3 — trailing four hex digits≤ 0x0FFF⇒ BS.2094 common definition,≥ 0x1000⇒ file-local custom definition carried in<axml>/<bxml>/<sxml>), exposed viaAudioId::{track_ref,pack_ref}_kind()/_scope()and surfaced aswav:chna.<n>.{track_ref,pack_ref}_{kind,definition}. Bodies shorter than the 4-byte pre-amble are skipped-as-opaque; the mux→demux round-trip and the BS.2088-2 §8.3.1 stereo worked example are pinned byte-for-byte in tests. TheDISP(Display) chunk — the RIFF "SoundSchemeTitle" convention catalogued indocs/container/riff/metadata/exiftool-riff-tags.html(a clipboard- formattypeDWORD followed by a payload in that format) — is supported on BOTH read and write sides through the typedwav::DispChunkstruct (parse/to_bytes, plusDispChunk::text/titlefor the commonCF_TEXTdisplay-title form). The demuxer surfaceswav:disp.{body_len,type}always pluswav:disp.titlefor theCF_TEXT(type == 1) form (text trimmed at the first NUL); non-text clipboard formats surface header-only and the binary GDI payload is not interpreted at the container layer; truncated bodies are skipped-as-opaque. The typed view is reachable viaWavDemuxer::disp()and the muxer writes the chunk viaWavMuxOptions::with_disp. Theid3/ID3(embedded ID3v2 tag) chunk is supported on BOTH sides: the demuxer surfaces the 10-byte ID3v2 header fields for observability perdocs/container/id3/id3v2.3.0.html§3.1 (wav:id3.{body_len,version, flags,tag_size}plus the.unsynchronisation/.extended_header/.experimentalflag bits and the §3.1 synchsafe 28-bittag_sizedecode), and the muxer carries a complete caller-supplied ID3v2 tag verbatim viaWavMuxOptions::with_id3. Frame encoding/decoding (TIT2, APIC, …) is left tooxideav-id3per the codec/container split; malformed / non-ID3-magic bodies surface onlybody_len. ThePAD(Pad / alignment-padding) chunk — the alignment sibling ofJUNKin the RIFF MCI §2 "skip/ignore" dispatch group — is accounted under the parallelwav:pad.*key namespace (count/total_bytes/ per-chunk<n>.body_len), mirroring theJUNKcontract; the two stay in separate namespaces and absence is observable. TheLIST INFOtext-tag namespace (read since the baseline) is now write-symmetric through the typedwav::InfoChunk/wav::InfoEntrystructs (parse/to_bytescarrying an ordered list of(sub-ID FOURCC, text)entries, NUL-terminated + RIFF §2 word-aligned):WavMuxOptions::with_infoemits aLIST(INFO) chunk ahead ofdataand the demuxer reads recognised sub-IDs back through their snake_case keys, with the typed view (including unknown/vendor sub-IDs carried verbatim) reachable viaWavDemuxer::info(). - slin container: Asterisk-style headerless
.sln*/.slin*raw S16LE PCM (extension drives the sample rate). - Y4M (YUV4MPEG2) container: rawvideo demuxer + muxer for
.y4mfiles, supporting 4:2:0 / 4:2:2 / 4:4:4 / mono at 8/10/12-bit. HeaderX<key>=<val>extensions are surfaced verbatim throughDemuxer::metadata. - Filter primitive: typed scalar Reinhard 2002 simple global
tone-mapping operator (
Ld = L / (1 + L)) with its closed-form inverse (L = Ld / (1 − Ld)). Scene-luminance and display-luminance domains are separated by distinctSceneLuminance/DisplayLuminancewrapper types, so callers can't accidentally swap pre- and post-tone-map values; constructors reject invalid inputs (negative, NaN, non-finite, or display ≥ 1.0). The forward map is a published mathematical fact transcribed fromdocs/image/filter/tone-mapping-operators.md§2.2.
[dependencies]
oxideav-basic = "0.0"MIT — see LICENSE.