// wav_codec — pure implementation. See wav_codec.h. The one RIFF chunk // traversal lives here (nextWavChunk); layout parse and 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. Caller checks bounds before each read (off + N <= size). 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). 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) { char buf[17]; // 16 hex digits, zero-padded 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: header at `headerOffset` (id(4) + // size(4)), body at `bodyOffset`/`bodySize`. `bodyInBounds` false means the // declared body runs past the buffer — still reported, but 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. 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). 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); // WAVE_FORMAT_EXTENSIBLE: the real format lives in the SubFormat GUID's // leading 2-byte tag at body offset 24, not in fmtTag itself. Body must // reach offset 24+16; otherwise leave the tag 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")) { // Reject if the declared body runs past the buffer (truncated/lying // header), or if data arrived before fmt. if (!c.bodyInBounds) return out; if (!haveFmt) return out; // Extensible tag (0xFFFE) is float only when its SubFormat sub-tag is // also IEEE-float (0x0003) — PCM-integer-in-extensible must be rejected. 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; } MonoCollapse collapseToMono(const std::vector& bytes) { MonoCollapse out; const WavLayout layout = parseWavLayout(bytes); if (!layout.valid || layout.channelCount < 2) return out; const std::size_t frames = layout.frameCount(); if (frames == 0) return out; const std::size_t stride = layout.channelCount; const std::vector pcm = extractFloatFrames(bytes, layout, 0, frames); if (pcm.size() != frames * stride) return out; // short read -> decline, never guess // Bit patterns, not values: see the header. memcpy is the only defined float->bits // read, and it compiles to a register move. auto bitsOf = [](AudioSample s) { std::uint32_t bits = 0; std::memcpy(&bits, &s, 4u); return bits; }; for (std::size_t f = 0; f < frames; ++f) { const std::uint32_t first = bitsOf(pcm[f * stride]); for (std::size_t c = 1; c < stride; ++c) { if (bitsOf(pcm[f * stride + c]) != first) return out; } } // float -> double -> float round-trips exactly for every finite value and for // +-0/+-infinity (double represents every float bit pattern in those classes), so // channel 0 reaches the rebuilt file unaltered. The one hole: a signaling NaN is // quieted by the float->double promotion, so an identical-bit sNaN pair could // collapse to a different bit pattern than it started with. Not reachable from // REAPER-rendered audio, but the bit-identical predicate above admits NaN inputs, // so this rebuild is not exempt from the claim it makes. std::vector mono(frames); for (std::size_t f = 0; f < frames; ++f) mono[f] = static_cast(pcm[f * stride]); out.collapsed = true; out.bytes = buildFloat32Wav(1, layout.sampleRate, frames, mono); return out; } std::string monoCollapseSuffix(MonoCollapseOutcome outcome) { switch (outcome) { case MonoCollapseOutcome::Declined: return {}; case MonoCollapseOutcome::Collapsed: return " (collapsed to mono)"; case MonoCollapseOutcome::Failed: return " (mono collapse failed -- left as captured)"; } return {}; // unreachable for a valid enum; claim nothing rather than a wrong outcome } 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) { 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; feedByte(static_cast('W')); // domain-separation prefix std::size_t pos = 12; WavChunkView c; while (nextWavChunk(bytes, pos, c)) { if (tagEquals(bytes, c.headerOffset, "fmt ")) { 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")) { 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