135 lines
6.5 KiB
C++
135 lines
6.5 KiB
C++
#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 <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<std::size_t>(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<std::uint8_t>& 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<AudioSample> extractFloatFrames(const std::vector<std::uint8_t>& 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<std::uint8_t>& 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<std::uint8_t> buildFloat32Wav(int nch, std::uint32_t rate,
|
|
std::size_t frameCount,
|
|
const std::vector<double>& 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<std::uint8_t> 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<std::uint8_t>& 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<std::uint8_t>& bytes);
|
|
|
|
} // namespace reasampler::capture
|