Q-W3: main.cpp → pointers+entry+dispatch via 4 capture hoists; one pure wav_codec RIFF owner; ICaptureBackend deleted; capture_realtime rename + finalize split; shared stampCaptureSample; makeUniqueTag gains monotonic counter (fixes same-second batch collisions). 60/60 green.

This commit is contained in:
2026-07-29 10:56:11 -04:00
parent d7d7f7e084
commit 09f7173db2
29 changed files with 2972 additions and 2426 deletions
+3 -217
View File
@@ -5,11 +5,8 @@
#include "../src/core/capture/capture_paths.h"
#include <cstdint>
#include <cstdio>
#include <cstring> // std::memcpy (for putF32cp in hashWavContent tests)
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::capture;
@@ -345,208 +342,9 @@ static void testTransitionInPlaceSaveIsNoOp() {
== ProjectTransition::NoOp);
}
// --- hashBytes (FNV-1a content hash) ----------------------------------------
//
// The fix for the confirm-on-last-reference bug: hashBytes produces a 16-char hex
// string that capture.cpp and capture_realtime.cpp store on Sample::contentHash so
// BankBook::hashReferencedElsewhere can detect copies and suppress the confirm when
// another bank still holds the same file.
static void testHashBytesOutputFormat() {
// Output is always 16 lowercase hex characters.
const std::uint8_t bytes[] = {0x01, 0x02, 0x03};
const std::string h = hashBytes(bytes, 3);
CHECK(h.size() == 16);
for (char c : h) {
CHECK((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'));
}
}
static void testHashBytesDeterministic() {
// Same input always produces the same output (bit-identical captures get
// the same hash, so hashReferencedElsewhere fires correctly for copies).
const std::uint8_t bytes[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01};
CHECK(hashBytes(bytes, 5) == hashBytes(bytes, 5));
}
static void testHashBytesDistinct() {
// Different inputs produce different hashes (no accidental dedup of distinct
// files). This covers the "one-bit-flip changes the hash" property.
std::uint8_t a[] = {0x00, 0x00};
std::uint8_t b[] = {0x00, 0x01};
CHECK(hashBytes(a, 2) != hashBytes(b, 2));
std::uint8_t c[] = {0xFF, 0xFF, 0xFF};
std::uint8_t d[] = {0xFF, 0xFF, 0xFE};
CHECK(hashBytes(c, 3) != hashBytes(d, 3));
}
static void testHashBytesEmptyBufferIsNonEmpty() {
// An empty buffer returns the FNV-1a offset basis in hex (stable, non-empty
// sentinel) — capturing the contract that even empty inputs yield a 16-char hash.
const std::string h = hashBytes(nullptr, 0);
CHECK(h.size() == 16);
}
static void testHashBytesLargerBufferDiffersFromSmaller() {
// Padding a buffer with a zero byte must change the hash (order + length
// sensitivity so two differently-sized WAV files don't accidentally collide).
const std::uint8_t short_buf[] = {0xAB, 0xCD};
const std::uint8_t long_buf[] = {0xAB, 0xCD, 0x00};
CHECK(hashBytes(short_buf, 2) != hashBytes(long_buf, 3));
}
// --- hashWavContent (WAV-aware dedup hash) -----------------------------------
//
// Verifies that the WAV-content hash hashes only fmt+data (skipping metadata
// chunks like bext/LIST), falls back gracefully for non-WAV input, and that
// different audio data yields different hashes.
// Minimal synthetic WAV builder (mirrors the one in test_wav_trim.cpp).
static void putU16cp(std::vector<std::uint8_t>& b, std::uint16_t v) {
b.push_back(static_cast<std::uint8_t>(v & 0xFF));
b.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
}
static void putU32cp(std::vector<std::uint8_t>& b, std::uint32_t v) {
b.push_back(static_cast<std::uint8_t>(v & 0xFF));
b.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
b.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFF));
b.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFF));
}
static void putTagcp(std::vector<std::uint8_t>& b, const char* t) {
for (int i = 0; i < 4; ++i) b.push_back(static_cast<std::uint8_t>(t[i]));
}
static void putF32cp(std::vector<std::uint8_t>& b, float f) {
std::uint8_t tmp[4];
std::memcpy(tmp, &f, 4);
for (int i = 0; i < 4; ++i) b.push_back(tmp[i]);
}
// Builds a minimal 32-bit-float RIFF/WAVE with an optional metadata chunk
// inserted between "WAVE" and the fmt chunk. `metaChunkBody` and `metaTag` are
// used when `insertMeta` is true. This is the shape REAPER produces: a `bext`
// or `LIST` chunk before fmt with a render-time timestamp in the body.
static std::vector<std::uint8_t> buildTestWav(
std::uint16_t channels, std::uint32_t sampleRate,
const std::vector<float>& samples,
bool insertMeta = false,
const char* metaTag = "bext",
const std::vector<std::uint8_t>& metaBody = {}) {
std::vector<std::uint8_t> chunks;
if (insertMeta && !metaBody.empty()) {
putTagcp(chunks, metaTag);
putU32cp(chunks, static_cast<std::uint32_t>(metaBody.size()));
chunks.insert(chunks.end(), metaBody.begin(), metaBody.end());
if (metaBody.size() & 1u) chunks.push_back(0); // RIFF pad
}
// fmt chunk (16-byte body, IEEE-float tag 3).
const std::uint32_t dataBytes =
static_cast<std::uint32_t>(samples.size() * 4u);
putTagcp(chunks, "fmt ");
putU32cp(chunks, 16);
putU16cp(chunks, 3); // IEEE float
putU16cp(chunks, channels);
putU32cp(chunks, sampleRate);
putU32cp(chunks, sampleRate * channels * 4u); // byteRate
putU16cp(chunks, static_cast<std::uint16_t>(channels * 4)); // blockAlign
putU16cp(chunks, 32); // bitsPerSample
// data chunk.
putTagcp(chunks, "data");
putU32cp(chunks, dataBytes);
for (float f : samples) putF32cp(chunks, f);
std::vector<std::uint8_t> wav;
putTagcp(wav, "RIFF");
putU32cp(wav, static_cast<std::uint32_t>(4 + chunks.size()));
putTagcp(wav, "WAVE");
wav.insert(wav.end(), chunks.begin(), chunks.end());
return wav;
}
static void testHashWavContentIdenticalAudioSameHash() {
// Two WAVs with the same audio but different metadata body -> same hash.
// This is the core dedup regression: REAPER embeds a bext chunk with a
// render-time origination timestamp; without WAV-aware hashing, two renders
// of the same clip produce different file bytes -> no dedup collapse.
const std::vector<float> audio = {0.1f, -0.2f, 0.3f, -0.4f};
std::vector<std::uint8_t> metaA(64, 0x00); // bext body, all zeros (e.g. epoch)
std::vector<std::uint8_t> metaB(64, 0x00);
// Different origination timestamps: first 10 bytes of bext are ASCII date/time.
metaB[0] = '2'; metaB[1] = '0'; metaB[2] = '2'; metaB[3] = '6'; // year
auto wavA = buildTestWav(1, 44100, audio, /*meta=*/true, "bext", metaA);
auto wavB = buildTestWav(1, 44100, audio, /*meta=*/true, "bext", metaB);
// Files must differ (the bext body is different) to prove the test is valid.
CHECK(wavA != wavB);
// But their content hashes must be equal: same fmt+data, different metadata.
CHECK(hashWavContent(wavA) == hashWavContent(wavB));
}
static void testHashWavContentDifferentAudioDifferentHash() {
// Different PCM data -> different content hashes (no false dedup).
const std::vector<float> audioA = {0.5f, 0.5f};
const std::vector<float> audioB = {0.5f, 0.6f}; // last sample differs
auto wavA = buildTestWav(1, 44100, audioA);
auto wavB = buildTestWav(1, 44100, audioB);
CHECK(hashWavContent(wavA) != hashWavContent(wavB));
}
static void testHashWavContentDifferentFmtDifferentHash() {
// Different fmt fields (sample rate) -> different content hashes.
const std::vector<float> audio = {0.1f, 0.2f};
auto wav44 = buildTestWav(1, 44100, audio);
auto wav48 = buildTestWav(1, 48000, audio);
CHECK(hashWavContent(wav44) != hashWavContent(wav48));
}
static void testHashWavContentNonWavFallsBackToWholeFile() {
// Non-WAV bytes -> falls back to whole-file hashBytes; result is non-empty
// and equals hashBytes of the same bytes directly.
std::vector<std::uint8_t> notWav = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02};
const std::string h = hashWavContent(notWav);
CHECK(!h.empty());
CHECK(h.size() == 16);
CHECK(h == hashBytes(notWav.data(), notWav.size()));
}
static void testHashWavContentEmptyFallsBackToHashBytes() {
// Empty vector -> falls back to whole-file hashBytes (the FNV offset basis).
std::vector<std::uint8_t> empty;
const std::string h = hashWavContent(empty);
CHECK(!h.empty());
CHECK(h.size() == 16);
CHECK(h == hashBytes(nullptr, 0));
}
static void testHashWavContentListMetaSkipped() {
// A LIST/INFO chunk (another common metadata chunk) is likewise skipped.
const std::vector<float> audio = {1.0f, -1.0f, 0.5f};
std::vector<std::uint8_t> listBody = {'I','N','F','O', 'x','x','x','x'};
auto wavClean = buildTestWav(1, 48000, audio);
auto wavList = buildTestWav(1, 48000, audio, true, "LIST", listBody);
// Content hashes must match: only the LIST chunk differs.
CHECK(hashWavContent(wavClean) == hashWavContent(wavList));
}
static void testHashWavContentDomainSeparationFromWholeFile() {
// The content hash ('W'-prefixed) must not accidentally equal the whole-file
// hash of the SAME bytes. This guards against the domain-separation prefix
// being dropped or zeroed out.
const std::vector<float> audio = {0.0f};
auto wav = buildTestWav(1, 44100, audio);
const std::string contentHash = hashWavContent(wav);
const std::string wholeHash = hashBytes(wav.data(), wav.size());
CHECK(contentHash != wholeHash);
}
// NOTE (Q-W3, audit §4e): the hashBytes / hashWavContent tests moved to
// tests/test_wav_codec.cpp with the implementations — capture_paths is now path
// arithmetic only, with no content-hash / RIFF knowledge.
// --- bankRelativeForName spelling consistency (Phase R, R2) -----------------
//
@@ -597,18 +395,6 @@ int main() {
testTransitionTwoUnsavedProjectsSwitchLoads();
testTransitionFirstSaveOfUnsavedRelocatesButPlanNoOps();
testTransitionInPlaceSaveIsNoOp();
testHashBytesOutputFormat();
testHashBytesDeterministic();
testHashBytesDistinct();
testHashBytesEmptyBufferIsNonEmpty();
testHashBytesLargerBufferDiffersFromSmaller();
testHashWavContentIdenticalAudioSameHash();
testHashWavContentDifferentAudioDifferentHash();
testHashWavContentDifferentFmtDifferentHash();
testHashWavContentNonWavFallsBackToWholeFile();
testHashWavContentEmptyFallsBackToHashBytes();
testHashWavContentListMetaSkipped();
testHashWavContentDomainSeparationFromWholeFile();
testBankRelativeForNameMatchesDerivePathSpelling();
testBankRelativeForNameConventionAndEdge();
@@ -1,9 +1,10 @@
// Standalone tests for reasampler::realtime_record — no REAPER, no framework.
// Standalone tests for reasampler::capture_realtime (renamed from realtime_record
// in Q-W3 — the Q-9 naming rider) — no REAPER, no framework.
// Covers the two pure pieces behind the realtime-record backend (M8): the
// record-mode/recipe bookkeeping (channel count + tap -> I_RECMODE / I_RECMODE_FLAGS)
// and the wet/dry -> tap decision, plus the recorded-file -> Sample mapping.
#include "../src/core/capture/realtime_record.h"
#include "../src/core/capture/capture_realtime.h"
#include <cstdio>
#include <string>
@@ -328,7 +329,7 @@ int main() {
testStopRequestedClassification();
testIsTerminalPhaseClassification();
if (g_fail == 0) std::printf("realtime_record: all tests passed\n");
else std::printf("realtime_record: %d CHECK(s) FAILED\n", g_fail);
if (g_fail == 0) std::printf("capture_realtime: all tests passed\n");
else std::printf("capture_realtime: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}
@@ -1,13 +1,17 @@
// Standalone tests for reasampler::wav_trim — no REAPER, no test framework.
// Builds synthetic 32-bit-float WAV byte buffers, asserts the parse geometry, the
// float extraction, and the truncate-plan arithmetic (the header size-field patch).
// Standalone tests for reasampler::wav_codec — no REAPER, no test framework.
// The ONE pure WAV/RIFF owner (Q-W3, audit §4e): builds synthetic 32-bit-float WAV
// byte buffers, asserts the parse geometry, the float extraction, the truncate-plan
// arithmetic + size-field patch, the float32 build round-trip, and the WAV-aware
// content hashes (moved here from capture_paths with the hash implementations).
//
// Covers: canonical stereo/mono 32-bit-float parse; a leading unknown chunk skipped;
// format rejection (16-bit PCM, non-WAV, data-before-fmt, truncated data); frame
// extraction (whole / tail window / clamp / out-of-range); truncate plan (kept<all,
// no-op keep-all, kept==0, grow rejected) with exact size-field values.
// no-op keep-all, kept==0, grow rejected) with exact size-field values; patchU32LE;
// buildFloat32Wav golden header + parse round-trip; hashBytes/hashWavContent
// determinism, metadata-skip, fallback, and domain separation.
#include "../src/core/capture/wav_trim.h"
#include "../src/core/capture/wav_codec.h"
#include <cstdint>
#include <cstdio>
@@ -263,15 +267,10 @@ static void testTruncatePlanKeepFewer() {
// Applying the plan yields a buffer that re-parses to exactly 4 frames.
std::vector<std::uint8_t> trimmed(wav.begin(),
wav.begin() + p.newFileByteLength);
// Patch the two size fields (what the shell does before truncating on disk).
auto writeU32 = [](std::vector<std::uint8_t>& b, std::size_t off, std::uint32_t v) {
b[off + 0] = static_cast<std::uint8_t>(v & 0xFF);
b[off + 1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
b[off + 2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
b[off + 3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
};
writeU32(trimmed, p.dataSizeFieldOffset, p.newDataSize);
writeU32(trimmed, p.riffSizeFieldOffset, p.newRiffSize);
// Patch the two size fields with the module's own patch primitive (what the
// shell does before truncating on disk).
patchU32LE(trimmed, p.dataSizeFieldOffset, p.newDataSize);
patchU32LE(trimmed, p.riffSizeFieldOffset, p.newRiffSize);
WavLayout L2 = parseWavLayout(trimmed);
CHECK(L2.valid);
@@ -325,14 +324,8 @@ static void testExtensibleFloatAccepted() {
CHECK(p.valid);
CHECK(p.newDataSize == 3 * 2 * 4u);
std::vector<std::uint8_t> trimmed(wav.begin(), wav.begin() + p.newFileByteLength);
auto writeU32 = [](std::vector<std::uint8_t>& b, std::size_t off, std::uint32_t v) {
b[off + 0] = static_cast<std::uint8_t>(v & 0xFF);
b[off + 1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
b[off + 2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
b[off + 3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
};
writeU32(trimmed, p.dataSizeFieldOffset, p.newDataSize);
writeU32(trimmed, p.riffSizeFieldOffset, p.newRiffSize);
patchU32LE(trimmed, p.dataSizeFieldOffset, p.newDataSize);
patchU32LE(trimmed, p.riffSizeFieldOffset, p.newRiffSize);
WavLayout L2 = parseWavLayout(trimmed);
CHECK(L2.valid);
CHECK(L2.frameCount() == 3);
@@ -356,6 +349,233 @@ static void testTruncatePlanKeepZeroAndGrowRejected() {
CHECK(!planWavTruncate(bad, 0).valid);
}
// --- patchU32LE (the size-field patch primitive) ------------------------------
static void testPatchU32LEWritesLittleEndian() {
std::vector<std::uint8_t> buf(8, 0xEE);
patchU32LE(buf, 2, 0x0A0B0C0Du);
CHECK(buf[0] == 0xEE && buf[1] == 0xEE); // bytes outside the field untouched
CHECK(buf[2] == 0x0D && buf[3] == 0x0C && buf[4] == 0x0B && buf[5] == 0x0A);
CHECK(buf[6] == 0xEE && buf[7] == 0xEE);
}
// --- buildFloat32Wav (the one WAV writer, absorbed from ingest — T4-10) -------
static void testBuildFloat32WavGoldenHeaderAndRoundTrip() {
// 2 channels, 3 frames of known interleaved values.
const std::vector<double> pcm = {0.0, 0.5, -0.25, 1.0, -1.0, 0.125};
auto wav = buildFloat32Wav(2, 48000, 3, pcm);
// Golden container shape: 44-byte header + 6 samples * 4 bytes.
CHECK(wav.size() == 44u + 6u * 4u);
CHECK(std::memcmp(wav.data(), "RIFF", 4) == 0);
CHECK(std::memcmp(wav.data() + 8, "WAVE", 4) == 0);
CHECK(std::memcmp(wav.data() + 12, "fmt ", 4) == 0);
CHECK(std::memcmp(wav.data() + 36, "data", 4) == 0);
CHECK(wav[20] == 0x03 && wav[21] == 0x00); // WAVE_FORMAT_IEEE_FLOAT
CHECK(wav[34] == 32 && wav[35] == 0); // bitsPerSample = 32
// The build round-trips through the module's own parse + extraction, with the
// documented double->float narrowing.
WavLayout L = parseWavLayout(wav);
CHECK(L.valid);
CHECK(L.channelCount == 2);
CHECK(L.sampleRate == 48000);
CHECK(L.frameCount() == 3);
auto back = extractFloatFrames(wav, L, 0, 3);
CHECK(back.size() == 6);
for (std::size_t i = 0; i < back.size(); ++i)
CHECK(back[i] == static_cast<float>(pcm[i]));
}
static void testBuildFloat32WavShortInputRejectedByParse() {
// Fewer interleaved samples than frameCount*nch declares: the data chunk still
// declares the full length; the missing tail simply is not written. The build
// caller (ingest) always passes a full buffer; this locks the clamp-no-OOB shape.
const std::vector<double> pcm = {1.0}; // 1 sample for a 2-frame mono request
auto wav = buildFloat32Wav(1, 44100, 2, pcm);
// Declared data size covers 2 frames; actual bytes stop after 1 sample, so the
// declared length overruns the buffer -> parse rejects (the honest verdict for
// a short-fed build; ingest never produces this).
CHECK(wav.size() == 44u + 4u);
CHECK(!parseWavLayout(wav).valid);
}
// --- hashBytes (FNV-1a content hash — moved with the impl from capture_paths) --
static void testHashBytesOutputFormat() {
// Output is always 16 lowercase hex characters.
const std::uint8_t bytes[] = {0x01, 0x02, 0x03};
const std::string h = hashBytes(bytes, 3);
CHECK(h.size() == 16);
for (char c : h) {
CHECK((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'));
}
}
static void testHashBytesDeterministicAndDistinct() {
// Same input always produces the same output; different inputs differ (no
// accidental dedup of distinct files), including a one-bit flip and a
// trailing-zero-byte length change.
const std::uint8_t bytes[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01};
CHECK(hashBytes(bytes, 5) == hashBytes(bytes, 5));
std::uint8_t a[] = {0x00, 0x00};
std::uint8_t b[] = {0x00, 0x01};
CHECK(hashBytes(a, 2) != hashBytes(b, 2));
const std::uint8_t short_buf[] = {0xAB, 0xCD};
const std::uint8_t long_buf[] = {0xAB, 0xCD, 0x00};
CHECK(hashBytes(short_buf, 2) != hashBytes(long_buf, 3));
}
static void testHashBytesEmptyBufferIsNonEmpty() {
// An empty buffer returns the FNV-1a offset basis in hex (stable, non-empty
// sentinel) — capturing the contract that even empty inputs yield a 16-char hash.
const std::string h = hashBytes(nullptr, 0);
CHECK(h.size() == 16);
}
// --- hashWavContent (WAV-aware dedup hash) -----------------------------------
//
// Verifies that the WAV-content hash hashes only fmt+data (skipping metadata
// chunks like bext/LIST), falls back gracefully for non-WAV input, and that
// different audio data yields different hashes.
// Builds a minimal 32-bit-float RIFF/WAVE with an optional metadata chunk
// inserted between "WAVE" and the fmt chunk. This is the shape REAPER produces: a
// `bext` or `LIST` chunk before fmt with a render-time timestamp in the body.
static std::vector<std::uint8_t> buildMetaWav(
std::uint16_t channels, std::uint32_t sampleRate,
const std::vector<float>& samples,
bool insertMeta = false,
const char* metaTag = "bext",
const std::vector<std::uint8_t>& metaBody = {}) {
std::vector<std::uint8_t> chunks;
if (insertMeta && !metaBody.empty()) {
putTag(chunks, metaTag);
putU32(chunks, static_cast<std::uint32_t>(metaBody.size()));
chunks.insert(chunks.end(), metaBody.begin(), metaBody.end());
if (metaBody.size() & 1u) chunks.push_back(0); // RIFF pad
}
// fmt chunk (16-byte body, IEEE-float tag 3).
const std::uint32_t dataBytes =
static_cast<std::uint32_t>(samples.size() * 4u);
putTag(chunks, "fmt ");
putU32(chunks, 16);
putU16(chunks, 3); // IEEE float
putU16(chunks, channels);
putU32(chunks, sampleRate);
putU32(chunks, sampleRate * channels * 4u); // byteRate
putU16(chunks, static_cast<std::uint16_t>(channels * 4)); // blockAlign
putU16(chunks, 32); // bitsPerSample
// data chunk.
putTag(chunks, "data");
putU32(chunks, dataBytes);
for (float f : samples) putFloat(chunks, f);
std::vector<std::uint8_t> wav;
putTag(wav, "RIFF");
putU32(wav, static_cast<std::uint32_t>(4 + chunks.size()));
putTag(wav, "WAVE");
wav.insert(wav.end(), chunks.begin(), chunks.end());
return wav;
}
static void testHashWavContentIdenticalAudioSameHash() {
// Two WAVs with the same audio but different metadata body -> same hash.
// This is the core dedup regression: REAPER embeds a bext chunk with a
// render-time origination timestamp; without WAV-aware hashing, two renders
// of the same clip produce different file bytes -> no dedup collapse.
const std::vector<float> audio = {0.1f, -0.2f, 0.3f, -0.4f};
std::vector<std::uint8_t> metaA(64, 0x00); // bext body, all zeros (e.g. epoch)
std::vector<std::uint8_t> metaB(64, 0x00);
// Different origination timestamps: first 10 bytes of bext are ASCII date/time.
metaB[0] = '2'; metaB[1] = '0'; metaB[2] = '2'; metaB[3] = '6'; // year
auto wavA = buildMetaWav(1, 44100, audio, /*meta=*/true, "bext", metaA);
auto wavB = buildMetaWav(1, 44100, audio, /*meta=*/true, "bext", metaB);
// Files must differ (the bext body is different) to prove the test is valid.
CHECK(wavA != wavB);
// But their content hashes must be equal: same fmt+data, different metadata.
CHECK(hashWavContent(wavA) == hashWavContent(wavB));
}
static void testHashWavContentDifferentAudioDifferentHash() {
// Different PCM data -> different content hashes (no false dedup).
const std::vector<float> audioA = {0.5f, 0.5f};
const std::vector<float> audioB = {0.5f, 0.6f}; // last sample differs
auto wavA = buildMetaWav(1, 44100, audioA);
auto wavB = buildMetaWav(1, 44100, audioB);
CHECK(hashWavContent(wavA) != hashWavContent(wavB));
}
static void testHashWavContentDifferentFmtDifferentHash() {
// Different fmt fields (sample rate) -> different content hashes.
const std::vector<float> audio = {0.1f, 0.2f};
auto wav44 = buildMetaWav(1, 44100, audio);
auto wav48 = buildMetaWav(1, 48000, audio);
CHECK(hashWavContent(wav44) != hashWavContent(wav48));
}
static void testHashWavContentNonWavFallsBackToWholeFile() {
// Non-WAV bytes -> falls back to whole-file hashBytes; result is non-empty
// and equals hashBytes of the same bytes directly. Empty input likewise.
std::vector<std::uint8_t> notWav = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02};
const std::string h = hashWavContent(notWav);
CHECK(!h.empty());
CHECK(h.size() == 16);
CHECK(h == hashBytes(notWav.data(), notWav.size()));
std::vector<std::uint8_t> empty;
const std::string he = hashWavContent(empty);
CHECK(he.size() == 16);
CHECK(he == hashBytes(nullptr, 0));
}
static void testHashWavContentListMetaSkipped() {
// A LIST/INFO chunk (another common metadata chunk) is likewise skipped.
const std::vector<float> audio = {1.0f, -1.0f, 0.5f};
std::vector<std::uint8_t> listBody = {'I','N','F','O', 'x','x','x','x'};
auto wavClean = buildMetaWav(1, 48000, audio);
auto wavList = buildMetaWav(1, 48000, audio, true, "LIST", listBody);
// Content hashes must match: only the LIST chunk differs.
CHECK(hashWavContent(wavClean) == hashWavContent(wavList));
}
static void testHashWavContentDomainSeparationFromWholeFile() {
// The content hash ('W'-prefixed) must not accidentally equal the whole-file
// hash of the SAME bytes. This guards against the domain-separation prefix
// being dropped or zeroed out.
const std::vector<float> audio = {0.0f};
auto wav = buildMetaWav(1, 44100, audio);
const std::string contentHash = hashWavContent(wav);
const std::string wholeHash = hashBytes(wav.data(), wav.size());
CHECK(contentHash != wholeHash);
}
static void testHashMatchesBuildOutput() {
// The consolidation guarantee end-to-end: a WAV produced by the module's own
// builder hashes as WAV content (not the whole-file fallback), so an imported
// conversion and a captured render of identical audio can dedup-collapse.
const std::vector<double> pcm = {0.25, -0.25};
auto wav = buildFloat32Wav(1, 44100, 2, pcm);
CHECK(hashWavContent(wav) != hashBytes(wav.data(), wav.size())); // chunk-aware path taken
// And a metadata-bearing copy of the same audio content hashes identically.
auto withMeta = buildMetaWav(1, 44100, {0.25f, -0.25f}, true, "bext",
std::vector<std::uint8_t>(16, 0x7A));
CHECK(hashWavContent(wav) == hashWavContent(withMeta));
}
int main() {
testParseCanonicalStereo();
testParseMonoAndLeadingChunk();
@@ -367,7 +587,21 @@ int main() {
testTruncatePlanKeepZeroAndGrowRejected();
testExtensiblePcmIntegerRejected();
testExtensibleFloatAccepted();
testPatchU32LEWritesLittleEndian();
testBuildFloat32WavGoldenHeaderAndRoundTrip();
testBuildFloat32WavShortInputRejectedByParse();
testHashBytesOutputFormat();
testHashBytesDeterministicAndDistinct();
testHashBytesEmptyBufferIsNonEmpty();
testHashWavContentIdenticalAudioSameHash();
testHashWavContentDifferentAudioDifferentHash();
testHashWavContentDifferentFmtDifferentHash();
testHashWavContentNonWavFallsBackToWholeFile();
testHashWavContentListMetaSkipped();
testHashWavContentDomainSeparationFromWholeFile();
testHashMatchesBuildOutput();
if (g_fail == 0) std::printf("All tests passed.\n");
if (g_fail == 0) std::printf("wav_codec: all tests passed\n");
else std::printf("wav_codec: %d CHECK(s) FAILED\n", g_fail);
return g_fail ? 1 : 0;
}