#pragma once // wav_codec — the pure owner of the WAV/RIFF byte format: chunk walker, layout // parse, float32 build, size-field patch, and the WAV-aware content hash — one // chunk traversal shared by all of them so hashing and decoding cannot desync. // Handles 32-bit float WAV only (RIFF/WAVE, `fmt ` tag 3 or 0xFFFE-extensible // w/ float subformat, float32 `data`); anything else parses as invalid. #include #include #include #include #include "core/audio/peaks.h" // AudioSample (float) namespace reasampler::capture { using audio::AudioSample; // --- Layout parse ------------------------------------------------------------ // The parsed geometry of a canonical 32-bit-float WAV. `valid` is false when the // bytes are not a WAV we can safely trim; every other field is meaningful only // when valid. struct WavLayout { bool valid = false; std::uint16_t channelCount = 0; // from `fmt ` (interleave stride) std::uint32_t sampleRate = 0; // The `data` chunk: PCM byte offset + declared length. // frameCount = dataByteLength / (channelCount * 4). std::size_t dataByteOffset = 0; std::size_t dataByteLength = 0; // Offsets of the two LE uint32 size fields the truncate patch rewrites. std::size_t riffSizeFieldOffset = 4; // always 4 for a RIFF file std::size_t dataSizeFieldOffset = 0; std::size_t frameCount() const { const std::size_t bytesPerFrame = static_cast(channelCount) * 4u; return bytesPerFrame ? dataByteLength / bytesPerFrame : 0; } }; // Parses a WAV byte buffer's header geometry; {valid=false} for anything not a // canonical float32 RIFF/WAVE, or a `data` length running past the buffer. // Does not copy PCM, only locates it. Pure + total (no throw, no UB). WavLayout parseWavLayout(const std::vector& bytes); // Copies `frameCount` interleaved float frames starting at `startFrame` out of // the WAV's `data` region into a flat [f0c0,f0c1,...] buffer, clamped to frames // actually present; never reads past `data`. Reads little-endian via memcpy — // target is x86/ARM-LE only, no big-endian byte-swap. std::vector extractFloatFrames(const std::vector& bytes, const WavLayout& layout, std::size_t startFrame, std::size_t frameCount); // --- Truncate plan + size-field patch --------------------------------------- // The plan to truncate a parsed WAV to `keptFrames` frames. `valid` is false if // the layout is invalid or keptFrames exceeds the file's frames (never grow). struct WavTruncatePlan { bool valid = false; std::size_t newFileByteLength = 0; // truncate the file to exactly this length std::size_t dataSizeFieldOffset = 0; // where to write newDataSize (LE uint32) std::uint32_t newDataSize = 0; // kept PCM byte length std::size_t riffSizeFieldOffset = 4; // where to write newRiffSize (LE uint32) std::uint32_t newRiffSize = 0; // newFileByteLength - 8 (RIFF size excludes // the 8-byte "RIFF"+size prefix) }; // Computes the truncate plan to keep exactly `keptFrames` frames. The shell // applies it: patch the two size fields (patchU32LE), then truncate to // newFileByteLength. WavTruncatePlan planWavTruncate(const WavLayout& layout, std::size_t keptFrames); // Patches a little-endian uint32 into a byte buffer at `off`. Caller guarantees // off + 4 <= bytes.size() (the plan's offsets came from a valid parse of the same // buffer). void patchU32LE(std::vector& bytes, std::size_t off, std::uint32_t v); // --- Float32 WAV build ------------------------------------------------------- // Builds a minimal canonical float32 RIFF/WAVE byte buffer from interleaved // double samples (narrowed to float by cast). Round-trips through // parseWavLayout/extractFloatFrames. std::vector buildFloat32Wav(int nch, std::uint32_t rate, std::size_t frameCount, const std::vector& interleaved); // --- Lossless mono collapse --------------------------------------------------- // The outcome of the bit-identical mono collapse. `collapsed == false` means the // caller must leave the source file exactly as it is — it writes nothing. struct MonoCollapse { bool collapsed = false; std::vector bytes; // the rebuilt 1-channel WAV; empty unless collapsed }; // Collapses a multi-channel float32 WAV to one channel when EVERY channel of EVERY // frame carries the identical float BIT PATTERN. Bit equality, never an epsilon and // never `==` on floats: +0.0/-0.0 and two NaNs with differing payloads are NOT // identical and are never folded. Frame count, sample rate and bit depth are // preserved — only the interleave stride changes — so the collapse cannot lose // information, and a lossy downmix (summing differing channels) is not something // this can express. // // Declines for: bytes that do not parse; a file already at one channel; a zero-frame // file (no frame of evidence to act on); any differing channel pair. // // The rebuild is a canonical minimal WAV, so non-audio chunks (a renderer's `bext` // timestamp, iXML, LIST) do not survive it. That much hashWavContent already skips — // but the collapse rewrites the `fmt ` body and the `data` payload too, which moves // the file's content identity; see this directory's CLAUDE.md for what that costs, // including the bext/source-position consequence beyond hashing. MonoCollapse collapseToMono(const std::vector& bytes); // --- Content identity (dedup hashes) ----------------------------------------- // Deterministic FNV-1a 64-bit content hash over `len` bytes, as 16-char lowercase // hex. Fills Sample::contentHash for the confirm-on-last-reference dedup guardrail. std::string hashBytes(const std::uint8_t* data, std::size_t len); // WAV-aware content hash: hashes only the `fmt ` body + `data` payload, skipping // other chunks. WHY: REAPER's offline renderer embeds a render-varying `bext` // timestamp chunk even with no BWF metadata requested, so two renders of // identical audio would otherwise hash differently and never dedup. Prefixed // with tag byte 'W' so it can't collide with a same-size hashBytes result. // Falls back to whole-file hashBytes (no prefix) for a file that doesn't parse. std::string hashWavContent(const std::vector& bytes); } // namespace reasampler::capture