Collapse a dual-mono resample bake to one channel, like every other capture

prepareLanding takes the shared collapse on the staged buffer before the hash
and the channel-count read, so hash, entry and file all derive from one buffer.
A true-stereo bake stays byte-identical.
This commit is contained in:
2026-08-02 18:31:05 -04:00
parent 4231b2321c
commit 425b9f708c
6 changed files with 167 additions and 6 deletions
+121 -1
View File
@@ -14,7 +14,8 @@
// contentHash values against a silent feed-sequence drift); and the lossless mono
// collapse (bit-identical N-channel fold, the one-sample-differs and signed-zero
// declines, already-mono, zero/single-frame, an odd padded leading chunk, and the
// content-hash consequence).
// content-hash consequence) plus its buffer-side wrapper applyMonoCollapse (the
// bytes/layout pairing, the byte-identical decline, one-ULP, and double-apply).
#include "../src/core/capture/wav_codec.h"
@@ -795,6 +796,119 @@ static void testCollapseOutcomeSuffixesAreDistinctStrings() {
CHECK(collapsed.find("failed") == std::string::npos);
}
// --- applyMonoCollapse: the buffer-side collapse a bake landing takes ---------
// A dead-center instrument render is dual-mono, and must land 1-channel: the returned
// layout says one channel, and it is the parse OF the returned bytes, so the caller's
// channelCount, its hash and the file it writes cannot come from different buffers.
static void testApplyCollapseDualMonoLandsOneChannel() {
auto wav = buildFloatWav(2, 48000, 6,
[](std::size_t f, std::uint16_t) {
return 0.25f * static_cast<float>(f) - 0.5f;
});
const CollapsedWav staged = applyMonoCollapse(wav);
CHECK(staged.collapsed);
CHECK(staged.layout.valid);
CHECK(staged.layout.channelCount == 1);
const WavLayout reparsed = parseWavLayout(staged.bytes);
CHECK(reparsed.valid);
CHECK(reparsed.channelCount == staged.layout.channelCount);
CHECK(reparsed.sampleRate == staged.layout.sampleRate);
CHECK(reparsed.frameCount() == staged.layout.frameCount());
CHECK(reparsed.dataByteOffset == staged.layout.dataByteOffset);
CHECK(reparsed.dataByteLength == staged.layout.dataByteLength);
const auto pcm = extractFloatFrames(staged.bytes, staged.layout, 0, 6);
CHECK(pcm.size() == 6);
for (std::size_t f = 0; f < 6 && f < pcm.size(); ++f)
CHECK(pcm[f] == 0.25f * static_cast<float>(f) - 0.5f);
}
// A true-stereo bake must land exactly the bytes it staged — this is the regression the
// collapse must not cause, so it is asserted on the bytes themselves, not on the verdict.
static void testApplyCollapseTrueStereoIsByteIdentical() {
auto wav = buildFloatWav(2, 48000, 5,
[](std::size_t f, std::uint16_t ch) {
return ch == 0 ? static_cast<float>(f)
: -static_cast<float>(f);
});
const CollapsedWav staged = applyMonoCollapse(wav);
CHECK(!staged.collapsed);
CHECK(staged.bytes == wav);
CHECK(staged.layout.channelCount == 2);
CHECK(staged.layout.frameCount() == 5);
// The dedup key a declined bake writes is the one it would have written before the
// collapse existed.
CHECK(hashWavContent(staged.bytes) == hashWavContent(wav));
}
// One float ULP apart in ONE sample is a difference, not an epsilon: the buffer path
// must decline it exactly as the predicate does, and hand the bytes back untouched.
static void testApplyCollapseDeclinesOnOneUlpDifference() {
auto wav = buildFloatWav(2, 48000, 8,
[](std::size_t f, std::uint16_t ch) {
float v = 1.0f + static_cast<float>(f);
if (f == 4 && ch == 1) v = nextafterf(v, 2.0f);
return v;
});
const CollapsedWav staged = applyMonoCollapse(wav);
CHECK(!staged.collapsed);
CHECK(staged.bytes == wav);
CHECK(staged.layout.channelCount == 2);
}
// A sound that was already mono comes back untouched, and a collapsed buffer fed back
// through does not collapse a second time (the rebuild would otherwise re-hash).
static void testApplyCollapseAlreadyMonoIsUntouched() {
auto mono = buildFloatWav(1, 44100, 4,
[](std::size_t f, std::uint16_t) {
return static_cast<float>(f);
});
const CollapsedWav staged = applyMonoCollapse(mono);
CHECK(!staged.collapsed);
CHECK(staged.bytes == mono);
CHECK(staged.layout.channelCount == 1);
auto dual = buildFloatWav(2, 44100, 4,
[](std::size_t f, std::uint16_t) {
return static_cast<float>(f);
});
const CollapsedWav once = applyMonoCollapse(dual);
CHECK(once.collapsed);
const CollapsedWav twice = applyMonoCollapse(once.bytes);
CHECK(!twice.collapsed);
CHECK(twice.bytes == once.bytes);
}
// The collapse is permitted only because it is lossless: frame count, sample rate and
// bit depth survive it, and only the interleave stride changes.
static void testApplyCollapsePreservesFramesRateAndDepth() {
auto wav = buildFloatWav(2, 44100, 7,
[](std::size_t f, std::uint16_t) {
return 0.5f - 0.125f * static_cast<float>(f);
});
const WavLayout before = parseWavLayout(wav);
const CollapsedWav staged = applyMonoCollapse(wav);
CHECK(staged.collapsed);
CHECK(staged.layout.frameCount() == before.frameCount());
CHECK(staged.layout.sampleRate == before.sampleRate);
// `valid` implies 32-bit float (the parser accepts nothing else), and 4 bytes per
// frame at one channel is that depth spelled out in the data chunk's own length.
CHECK(staged.layout.valid);
CHECK(staged.layout.dataByteLength == before.frameCount() * 4u);
}
// Bytes that never parsed keep `collapsed` false AND `layout.valid` false — the pair a
// caller refuses on, and the reason an invalid layout can only mean a bad INPUT.
static void testApplyCollapseUnparseableInputIsReportedInvalid() {
std::vector<std::uint8_t> junk = {'N','O','P','E', 0,0,0,0, 'W','A','V','E'};
const CollapsedWav staged = applyMonoCollapse(junk);
CHECK(!staged.collapsed);
CHECK(!staged.layout.valid);
CHECK(staged.bytes == junk);
}
int main() {
testParseCanonicalStereo();
testParseMonoAndLeadingChunk();
@@ -831,6 +945,12 @@ int main() {
testCollapseChangesContentHash();
testCollapsePreservesQuietNaNBitPattern();
testCollapseOutcomeSuffixesAreDistinctStrings();
testApplyCollapseDualMonoLandsOneChannel();
testApplyCollapseTrueStereoIsByteIdentical();
testApplyCollapseDeclinesOnOneUlpDifference();
testApplyCollapseAlreadyMonoIsUntouched();
testApplyCollapsePreservesFramesRateAndDepth();
testApplyCollapseUnparseableInputIsReportedInvalid();
if (g_fail == 0) std::printf("wav_codec: all tests passed\n");
else std::printf("wav_codec: %d CHECK(s) FAILED\n", g_fail);