// wav_codec — pure implementation. See wav_codec.h. NO REAPER / SWELL / vendor. // // The ONE RIFF chunk traversal lives here (nextWavChunk); the layout parse and the // content hash both walk with it, so their view of the container cannot drift. #include "core/capture/wav_codec.h" #include // std::snprintf (hash hex render) #include // std::memcpy, std::memcmp namespace reasampler::capture { namespace { // Little-endian readers. Bounds are checked by the caller before each read; these // assume `off + N <= bytes.size()`. memcpy avoids alignment/aliasing UB. std::uint16_t readU16LE(const std::vector& b, std::size_t off) { return static_cast(b[off] | (b[off + 1] << 8)); } std::uint32_t readU32LE(const std::vector& b, std::size_t off) { return static_cast(b[off]) | (static_cast(b[off + 1]) << 8) | (static_cast(b[off + 2]) << 16) | (static_cast(b[off + 3]) << 24); } bool tagEquals(const std::vector& b, std::size_t off, const char* tag) { return off + 4 <= b.size() && std::memcmp(b.data() + off, tag, 4) == 0; } // WAVE format tags we accept as 32-bit float (see wav_codec.h FORMAT ASSUMPTION). constexpr std::uint16_t kWaveFormatIeeeFloat = 0x0003; constexpr std::uint16_t kWaveFormatExtensible = 0xFFFE; // FNV-1a 64-bit constants (http://www.isthe.com/chongo/tech/comp/fnv/). constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ULL; constexpr std::uint64_t kFnvPrime = 1099511628211ULL; std::string fnvHex(std::uint64_t h) { // 16-digit lowercase hex (zero-padded) for a fixed-length string. char buf[17]; std::snprintf(buf, sizeof(buf), "%016llx", static_cast(h)); return std::string(buf); } // --- The ONE RIFF chunk traversal -------------------------------------------- // // One sub-chunk of a RIFF/WAVE container as the walk sees it: header at // `headerOffset` (id(4) + size(4)), body at `bodyOffset` with declared `bodySize`. // `bodyInBounds` is whether the declared body fits inside the buffer — a chunk // whose declared size lies past the end is still REPORTED (callers decide how to // treat it) but its body must not be read. struct WavChunkView { std::size_t headerOffset = 0; std::size_t bodyOffset = 0; std::uint32_t bodySize = 0; bool bodyInBounds = false; }; // Advances one chunk. `pos` starts at 12 (after "RIFF" size "WAVE"); each call // fills `out` and moves `pos` past the chunk's body, honoring RIFF even-byte // padding. Returns false when no further chunk header fits. If the padded advance // would overrun the buffer, the chunk is still reported (return true) and `pos` is // parked past the end so the NEXT call returns false — exactly the process-then- // break shape the pre-consolidation walkers shared. bool nextWavChunk(const std::vector& bytes, std::size_t& pos, WavChunkView& out) { if (pos + 8 > bytes.size()) return false; out.headerOffset = pos; out.bodyOffset = pos + 8; out.bodySize = readU32LE(bytes, pos + 4); out.bodyInBounds = (out.bodyOffset + out.bodySize <= bytes.size()); std::size_t advance = out.bodySize; if (advance & 1u) ++advance; // RIFF pad byte if (advance > bytes.size() - out.bodyOffset) { pos = bytes.size(); // overrun -> this is the last reported chunk } else { pos = out.bodyOffset + advance; } return true; } bool isRiffWave(const std::vector& bytes) { // Minimum viable RIFF/WAVE: "RIFF"(4) size(4) "WAVE"(4) = 12 bytes. return bytes.size() >= 12 && tagEquals(bytes, 0, "RIFF") && tagEquals(bytes, 8, "WAVE"); } } // namespace WavLayout parseWavLayout(const std::vector& bytes) { WavLayout out; if (!isRiffWave(bytes)) return out; bool haveFmt = false; std::uint16_t fmtTag = 0, channels = 0, bitsPerSample = 0; std::uint32_t sampleRate = 0; std::uint16_t extensibleSubFormatTag = 0; // set only when fmtTag == kWaveFormatExtensible // Walk the sub-chunks after "WAVE" (offset 12) with the shared traversal. A // malformed/truncated file is "invalid", never an OOB read. std::size_t pos = 12; WavChunkView c; while (nextWavChunk(bytes, pos, c)) { if (tagEquals(bytes, c.headerOffset, "fmt ")) { // fmt body: at least 16 bytes (PCM/float common fields). if (c.bodyOffset + 16 > bytes.size() || c.bodySize < 16) return out; fmtTag = readU16LE(bytes, c.bodyOffset + 0); channels = readU16LE(bytes, c.bodyOffset + 2); sampleRate = readU32LE(bytes, c.bodyOffset + 4); bitsPerSample = readU16LE(bytes, c.bodyOffset + 14); // For WAVE_FORMAT_EXTENSIBLE (0xFFFE), read the SubFormat GUID's leading // 2-byte tag at body offset 24 to distinguish float (0x0003) from PCM // integer (0x0001) and all other sub-formats. Body must be >= 40 bytes to // reach GUID offset 24 + 16 bytes of GUID, and the full GUID must fit in // the buffer; otherwise we leave extensibleSubFormatTag at 0 (rejected). if (fmtTag == kWaveFormatExtensible) { if (c.bodySize >= 40 && c.bodyOffset + 40 <= bytes.size()) { extensibleSubFormatTag = readU16LE(bytes, c.bodyOffset + 24); } } haveFmt = true; } else if (tagEquals(bytes, c.headerOffset, "data")) { // The data chunk: PCM starts at bodyOffset, declared length bodySize. // Reject if it runs past the buffer (truncated / lying header). if (!c.bodyInBounds) return out; if (!haveFmt) return out; // data before fmt — not a WAV we parse // Plain IEEE-float tag (0x0003): accept as-is. // Extensible tag (0xFFFE): accept only when the SubFormat tag read from // the GUID at body offset 24 is also 0x0003 (IEEE float). SubFormat tag // 0x0001 (PCM integer) or anything else with bitsPerSample==32 is NOT // float and must be rejected to prevent mis-decoding as float. const bool floatTag = (fmtTag == kWaveFormatIeeeFloat) || (fmtTag == kWaveFormatExtensible && extensibleSubFormatTag == kWaveFormatIeeeFloat); if (!floatTag || bitsPerSample != 32 || channels == 0) return out; out.valid = true; out.channelCount = channels; out.sampleRate = sampleRate; out.dataByteOffset = c.bodyOffset; out.dataByteLength = c.bodySize; out.riffSizeFieldOffset = 4; out.dataSizeFieldOffset = c.headerOffset + 4; // the `data` size field (LE uint32) return out; } } return out; // no data chunk found -> invalid } std::vector extractFloatFrames(const std::vector& bytes, const WavLayout& layout, std::size_t startFrame, std::size_t frameCount) { std::vector out; if (!layout.valid) return out; const std::size_t bytesPerFrame = static_cast(layout.channelCount) * 4u; const std::size_t totalFrames = layout.frameCount(); if (startFrame >= totalFrames) return out; // Clamp the requested span to the frames that actually exist. const std::size_t avail = totalFrames - startFrame; const std::size_t frames = (frameCount < avail) ? frameCount : avail; if (frames == 0) return out; const std::size_t firstByte = layout.dataByteOffset + startFrame * bytesPerFrame; out.resize(frames * layout.channelCount); // memcpy each float (LE on target hosts — see header's byte-order note). for (std::size_t i = 0; i < out.size(); ++i) { float f = 0.0f; std::memcpy(&f, bytes.data() + firstByte + i * 4u, 4u); out[i] = f; } return out; } WavTruncatePlan planWavTruncate(const WavLayout& layout, std::size_t keptFrames) { WavTruncatePlan plan; if (!layout.valid) return plan; const std::size_t totalFrames = layout.frameCount(); if (keptFrames > totalFrames) return plan; // never grow const std::size_t bytesPerFrame = static_cast(layout.channelCount) * 4u; const std::size_t keptDataBytes = keptFrames * bytesPerFrame; plan.valid = true; plan.newFileByteLength = layout.dataByteOffset + keptDataBytes; plan.dataSizeFieldOffset = layout.dataSizeFieldOffset; plan.newDataSize = static_cast(keptDataBytes); plan.riffSizeFieldOffset = layout.riffSizeFieldOffset; // RIFF size counts everything after the 8-byte "RIFF"+size prefix. plan.newRiffSize = static_cast(plan.newFileByteLength - 8); return plan; } void patchU32LE(std::vector& bytes, std::size_t off, std::uint32_t v) { bytes[off + 0] = static_cast(v & 0xFF); bytes[off + 1] = static_cast((v >> 8) & 0xFF); bytes[off + 2] = static_cast((v >> 16) & 0xFF); bytes[off + 3] = static_cast((v >> 24) & 0xFF); } std::vector buildFloat32Wav(int nch, std::uint32_t rate, std::size_t frameCount, const std::vector& interleaved) { const std::size_t sampleCount = frameCount * static_cast(nch); const std::size_t dataBytesCount = sampleCount * 4u; // 4 bytes per float32 // The WAV is: RIFF(4)+size(4)+WAVE(4) = 12, fmt (4)+size(4)+16 body = 24, data (4)+size(4)+payload. // Total = 12 + 24 + 8 + dataBytesCount = 44 + dataBytesCount. const std::uint32_t riffSize = static_cast(36u + dataBytesCount); // 4("WAVE")+24(fmt chunk)+8(data hdr)+data std::vector out; out.reserve(44u + dataBytesCount); auto putU16 = [&](std::uint16_t v) { out.push_back(static_cast(v & 0xFF)); out.push_back(static_cast((v >> 8) & 0xFF)); }; auto putU32 = [&](std::uint32_t v) { out.push_back(static_cast(v & 0xFF)); out.push_back(static_cast((v >> 8) & 0xFF)); out.push_back(static_cast((v >> 16) & 0xFF)); out.push_back(static_cast((v >> 24) & 0xFF)); }; auto putTag = [&](const char* t) { for (int i = 0; i < 4; ++i) out.push_back(static_cast(t[i])); }; auto putF32 = [&](float f) { std::uint8_t tmp[4]; std::memcpy(tmp, &f, 4); for (int i = 0; i < 4; ++i) out.push_back(tmp[i]); }; // RIFF header putTag("RIFF"); putU32(riffSize); putTag("WAVE"); // fmt chunk (16-byte body, WAVE_FORMAT_IEEE_FLOAT = 0x0003) putTag("fmt "); putU32(16u); // chunk body size putU16(0x0003u); // WAVE_FORMAT_IEEE_FLOAT putU16(static_cast(nch)); putU32(rate); putU32(rate * static_cast(nch) * 4u); // avgBytesPerSec putU16(static_cast(nch * 4)); // blockAlign putU16(32u); // bitsPerSample // data chunk putTag("data"); putU32(static_cast(dataBytesCount)); for (std::size_t i = 0; i < sampleCount && i < interleaved.size(); ++i) putF32(static_cast(interleaved[i])); return out; } std::string hashBytes(const std::uint8_t* data, std::size_t len) { // FNV-1a 64-bit: deterministic, no dependencies, adequate for dedup identity. std::uint64_t h = kFnvOffsetBasis; for (std::size_t i = 0; i < len; ++i) { h ^= static_cast(data[i]); h *= kFnvPrime; } return fnvHex(h); } std::string hashWavContent(const std::vector& bytes) { // Walk the RIFF/WAVE container (the shared traversal) and feed only the `fmt ` // body and `data` body through FNV-1a, prefixed with the domain-separation tag // byte 'W' (0x57). Any render-varying metadata chunks (bext, iXML, LIST, SMED, // etc.) are skipped. If the file does not parse as RIFF/WAVE with both fmt and // data chunks, fall back to whole-file hashBytes (no prefix) so an unrecognized // file still gets a hash. if (isRiffWave(bytes)) { std::uint64_t h = kFnvOffsetBasis; auto feedByte = [&](std::uint8_t b) { h ^= static_cast(b); h *= kFnvPrime; }; bool haveFmt = false; bool haveData = false; // Domain-separation prefix: 'W' (0x57) distinguishes a content hash from a // whole-file hash of different bytes that happen to be the same length. feedByte(static_cast('W')); std::size_t pos = 12; WavChunkView c; while (nextWavChunk(bytes, pos, c)) { if (tagEquals(bytes, c.headerOffset, "fmt ")) { // Feed the entire fmt body (all fields, including format tag, channels, // sample rate, bits-per-sample — everything that defines the audio format). if (c.bodyInBounds) { for (std::uint32_t i = 0; i < c.bodySize; ++i) feedByte(bytes[c.bodyOffset + i]); haveFmt = true; } } else if (tagEquals(bytes, c.headerOffset, "data")) { // Feed the entire PCM payload. if (c.bodyInBounds) { for (std::uint32_t i = 0; i < c.bodySize; ++i) feedByte(bytes[c.bodyOffset + i]); haveData = true; } } // All other chunks (bext, iXML, LIST, SMED, cue, etc.) are skipped. } if (haveFmt && haveData) return fnvHex(h); // Falls through to whole-file fallback if chunks were missing/malformed. } // Fallback: not a parseable RIFF/WAVE — hash the whole file (identical to // hashBytes(data, size); no prefix tag). return hashBytes(bytes.data(), bytes.size()); } } // namespace reasampler::capture