Merge Ψ-W2-T2: a bit-identical capture collapses to one lossless mono channel
# Conflicts: # src/shell/capture/CLAUDE.md # src/shell/capture/capture.cpp # src/shell/capture/capture.h
This commit is contained in:
@@ -603,8 +603,26 @@ static void testSeamFieldsAdditiveInvariant() {
|
||||
CHECK(idx.query("id-z")->rootNote == 60); // move did not disturb seam fields
|
||||
}
|
||||
|
||||
// A collapsed capture is a 1-channel entry, and the JSON is the only thing carrying
|
||||
// that count across a project reload — the instrument's mono/stereo default reads it.
|
||||
static void testMonoChannelCountRoundTrip() {
|
||||
BankModel idx;
|
||||
Sample s = fullSample("mono");
|
||||
s.channelCount = 1;
|
||||
CHECK(idx.add(s) == AddResult::Added);
|
||||
|
||||
const std::string json = idx.serialize();
|
||||
CHECK(json.find("\"channelCount\":1") != std::string::npos);
|
||||
|
||||
auto back = BankModel::deserialize(json);
|
||||
CHECK(back.has_value());
|
||||
CHECK(back && back->query("id-mono") &&
|
||||
back->query("id-mono")->channelCount == 1);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFullFieldRoundTrip();
|
||||
testMonoChannelCountRoundTrip();
|
||||
testSerializeGoldenLiteral();
|
||||
testDedupByHash();
|
||||
testTierFilterAndMove();
|
||||
|
||||
+193
-1
@@ -11,10 +11,14 @@
|
||||
// buildFloat32Wav golden header + parse round-trip; hashBytes/hashWavContent
|
||||
// determinism, metadata-skip, fallback, and domain separation; a golden hash
|
||||
// literal pinning exact hex output for a fixed input (guards persisted
|
||||
// contentHash values against a silent feed-sequence drift).
|
||||
// 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).
|
||||
|
||||
#include "../src/core/capture/wav_codec.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -47,6 +51,16 @@ static void putFloat(std::vector<std::uint8_t>& b, float f) {
|
||||
std::memcpy(tmp, &f, 4);
|
||||
for (int i = 0; i < 4; ++i) b.push_back(tmp[i]);
|
||||
}
|
||||
static float floatFromBits(std::uint32_t bits) {
|
||||
float f;
|
||||
std::memcpy(&f, &bits, 4);
|
||||
return f;
|
||||
}
|
||||
static std::uint32_t bitsFromFloat(float f) {
|
||||
std::uint32_t bits;
|
||||
std::memcpy(&bits, &f, 4);
|
||||
return bits;
|
||||
}
|
||||
|
||||
// A canonical 32-bit-float WAV: RIFF/WAVE, fmt (tag 3, 16-byte body), data holding
|
||||
// `frames` interleaved frames of `channels`. `leadingJunk` optionally inserts an
|
||||
@@ -593,6 +607,174 @@ static void testGoldenHashLiterals() {
|
||||
CHECK(hashBytes(wav.data(), wav.size()) == "68d8a193c958fd44");
|
||||
}
|
||||
|
||||
// --- Lossless mono collapse --------------------------------------------------
|
||||
|
||||
// Every channel carries frame f's value; the collapse must keep those values verbatim
|
||||
// in one channel and leave frame count / rate / bit depth alone.
|
||||
static void testCollapseBitIdenticalStereo() {
|
||||
auto wav = buildFloatWav(2, 48000, 6,
|
||||
[](std::size_t f, std::uint16_t) {
|
||||
return 0.25f * static_cast<float>(f) - 0.5f;
|
||||
});
|
||||
const MonoCollapse c = collapseToMono(wav);
|
||||
CHECK(c.collapsed);
|
||||
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid); // valid implies float32: the parser rejects anything else
|
||||
CHECK(L.channelCount == 1);
|
||||
CHECK(L.sampleRate == 48000);
|
||||
CHECK(L.frameCount() == 6);
|
||||
|
||||
const auto pcm = extractFloatFrames(c.bytes, L, 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);
|
||||
}
|
||||
|
||||
static void testCollapseDeclinesOnOneDifferingSample() {
|
||||
// Identical everywhere except frame 4's right channel, by the smallest step the
|
||||
// format can express near 1.0.
|
||||
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;
|
||||
});
|
||||
CHECK(!collapseToMono(wav).collapsed);
|
||||
CHECK(collapseToMono(wav).bytes.empty());
|
||||
}
|
||||
|
||||
// An already-mono file must come back untouched — a second capture pass over a
|
||||
// collapsed file must not rebuild (and so must not re-hash) it.
|
||||
static void testCollapseDeclinesOnAlreadyMono() {
|
||||
auto wav = buildFloatWav(1, 44100, 4,
|
||||
[](std::size_t f, std::uint16_t) {
|
||||
return static_cast<float>(f);
|
||||
});
|
||||
CHECK(!collapseToMono(wav).collapsed);
|
||||
}
|
||||
|
||||
// N-channel generalization: all-identical collapses to ONE channel, never a partial
|
||||
// fold (4 -> 2). Unreachable from today's capture paths, which always render 2.
|
||||
static void testCollapseFourChannels() {
|
||||
auto same = buildFloatWav(4, 48000, 5,
|
||||
[](std::size_t f, std::uint16_t) {
|
||||
return -0.125f * static_cast<float>(f);
|
||||
});
|
||||
const MonoCollapse c = collapseToMono(same);
|
||||
CHECK(c.collapsed);
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid && L.channelCount == 1 && L.frameCount() == 5);
|
||||
|
||||
auto oneDiffers = buildFloatWav(4, 48000, 5,
|
||||
[](std::size_t f, std::uint16_t ch) {
|
||||
float v = -0.125f * static_cast<float>(f);
|
||||
if (f == 2 && ch == 3) v += 0.5f;
|
||||
return v;
|
||||
});
|
||||
CHECK(!collapseToMono(oneDiffers).collapsed);
|
||||
}
|
||||
|
||||
static void testCollapseZeroAndSingleFrame() {
|
||||
// No frame of evidence that the channels agree -> decline rather than rebuild.
|
||||
auto empty = buildFloatWav(2, 48000, 0,
|
||||
[](std::size_t, std::uint16_t) { return 0.0f; });
|
||||
CHECK(parseWavLayout(empty).valid && parseWavLayout(empty).frameCount() == 0);
|
||||
CHECK(!collapseToMono(empty).collapsed);
|
||||
|
||||
auto one = buildFloatWav(2, 48000, 1,
|
||||
[](std::size_t, std::uint16_t) { return 0.75f; });
|
||||
const MonoCollapse c = collapseToMono(one);
|
||||
CHECK(c.collapsed);
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid && L.channelCount == 1 && L.frameCount() == 1);
|
||||
const auto pcm = extractFloatFrames(c.bytes, L, 0, 1);
|
||||
CHECK(pcm.size() == 1 && pcm[0] == 0.75f);
|
||||
}
|
||||
|
||||
// The predicate is over BIT PATTERNS: -0.0f == +0.0f compares equal as floats but is
|
||||
// a different value on disk, so folding it would not be lossless.
|
||||
static void testCollapseSignedZeroIsNotIdentical() {
|
||||
auto wav = buildFloatWav(2, 48000, 3,
|
||||
[](std::size_t, std::uint16_t ch) {
|
||||
return ch == 0 ? 0.0f : -0.0f;
|
||||
});
|
||||
CHECK(!collapseToMono(wav).collapsed);
|
||||
}
|
||||
|
||||
// A leading odd-sized chunk exercises the walk's RIFF pad byte; the rebuilt file is
|
||||
// canonical, so that chunk does not survive.
|
||||
static void testCollapseThroughOddPaddedLeadingChunk() {
|
||||
std::vector<std::uint8_t> chunks;
|
||||
putTag(chunks, "LIST");
|
||||
putU32(chunks, 5); // odd body -> one pad byte
|
||||
for (int i = 0; i < 5; ++i) chunks.push_back(0x41);
|
||||
chunks.push_back(0); // the pad
|
||||
putTag(chunks, "fmt ");
|
||||
putU32(chunks, 16);
|
||||
putU16(chunks, 3);
|
||||
putU16(chunks, 2);
|
||||
putU32(chunks, 48000);
|
||||
putU32(chunks, 48000u * 2u * 4u);
|
||||
putU16(chunks, 8);
|
||||
putU16(chunks, 32);
|
||||
putTag(chunks, "data");
|
||||
putU32(chunks, 3u * 2u * 4u);
|
||||
for (std::size_t f = 0; f < 3; ++f)
|
||||
for (int ch = 0; ch < 2; ++ch) putFloat(chunks, 0.5f * static_cast<float>(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());
|
||||
|
||||
const MonoCollapse c = collapseToMono(wav);
|
||||
CHECK(c.collapsed);
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid && L.channelCount == 1 && L.frameCount() == 3);
|
||||
// Canonical rebuild: byte-for-byte what buildFloat32Wav produces for the same PCM.
|
||||
CHECK(c.bytes == buildFloat32Wav(1, 48000, 3, {0.0, 0.5, 1.0}));
|
||||
}
|
||||
|
||||
static void testCollapseDeclinesOnUnparseableBytes() {
|
||||
std::vector<std::uint8_t> junk = {'N','O','P','E', 0,0,0,0, 'W','A','V','E'};
|
||||
CHECK(!collapseToMono(junk).collapsed);
|
||||
CHECK(!collapseToMono(std::vector<std::uint8_t>{}).collapsed);
|
||||
}
|
||||
|
||||
// Stated consequence, pinned: the collapse rewrites both the `fmt ` body and the
|
||||
// `data` payload, so a collapsed capture no longer shares content identity with the
|
||||
// stereo file it came from and will not dedup against one already in the bank.
|
||||
static void testCollapseChangesContentHash() {
|
||||
auto wav = buildFloatWav(2, 48000, 4,
|
||||
[](std::size_t f, std::uint16_t) {
|
||||
return static_cast<float>(f);
|
||||
});
|
||||
const MonoCollapse c = collapseToMono(wav);
|
||||
CHECK(c.collapsed);
|
||||
CHECK(hashWavContent(c.bytes) != hashWavContent(wav));
|
||||
}
|
||||
|
||||
// The float->double->float rebuild's stated hole is a SIGNALING NaN (double promotion
|
||||
// quiets it); a QUIET NaN is not that hole. Both channels carry the identical
|
||||
// quiet-NaN bit pattern, so the predicate collapses; the rebuilt mono channel must
|
||||
// carry that exact bit pattern back, not merely "some NaN".
|
||||
static void testCollapsePreservesQuietNaNBitPattern() {
|
||||
constexpr std::uint32_t kQuietNaNBits = 0x7FC12345u; // exponent all-ones, mantissa MSB set
|
||||
auto wav = buildFloatWav(2, 48000, 1,
|
||||
[kQuietNaNBits](std::size_t, std::uint16_t) {
|
||||
return floatFromBits(kQuietNaNBits);
|
||||
});
|
||||
const MonoCollapse c = collapseToMono(wav);
|
||||
CHECK(c.collapsed);
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid && L.channelCount == 1 && L.frameCount() == 1);
|
||||
const auto pcm = extractFloatFrames(c.bytes, L, 0, 1);
|
||||
CHECK(pcm.size() == 1);
|
||||
if (!pcm.empty()) CHECK(bitsFromFloat(pcm[0]) == kQuietNaNBits);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testParseCanonicalStereo();
|
||||
testParseMonoAndLeadingChunk();
|
||||
@@ -618,6 +800,16 @@ int main() {
|
||||
testHashWavContentDomainSeparationFromWholeFile();
|
||||
testHashMatchesBuildOutput();
|
||||
testGoldenHashLiterals();
|
||||
testCollapseBitIdenticalStereo();
|
||||
testCollapseDeclinesOnOneDifferingSample();
|
||||
testCollapseDeclinesOnAlreadyMono();
|
||||
testCollapseFourChannels();
|
||||
testCollapseZeroAndSingleFrame();
|
||||
testCollapseSignedZeroIsNotIdentical();
|
||||
testCollapseThroughOddPaddedLeadingChunk();
|
||||
testCollapseDeclinesOnUnparseableBytes();
|
||||
testCollapseChangesContentHash();
|
||||
testCollapsePreservesQuietNaNBitPattern();
|
||||
|
||||
if (g_fail == 0) std::printf("wav_codec: all tests passed\n");
|
||||
else std::printf("wav_codec: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user