fix(S4): close graveyard epoch ordering race; add canvas-clip + stride integration tests

Tag LoadedInstrument with installedAt; process() publishes that field (not a re-read
of reloadGeneration_) so the pruner's displacedAt <= seen bound is airtight. Also fixes
sampleRowHitTest canvas.bottom over-read and adds interleave-stride + canvas-clip tests.
This commit is contained in:
2026-07-26 16:57:48 -04:00
parent 0cde457224
commit b4b64ce68f
5 changed files with 191 additions and 19 deletions
+105
View File
@@ -16,12 +16,17 @@
// average / zero-stride / empty; buildTier0Keymap single full-keyboard zone with the
// root + loop + rate threaded and rate defaulting; selection state round-trip + empty id
// + wrong-version / truncated -> "".
// wav_trim -> extractFloatFrames -> downmixToMono integration: locks the interleave-
// stride contract across the seam (that the byte stride wav_trim reports matches the
// channel-count stride downmixToMono divides by).
#include "../src/vst/sample_map.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "../src/bank_book.h"
#include "../src/bank_model.h"
@@ -247,6 +252,104 @@ static void testSelectionStateTruncated() {
CHECK(deserializeSelection({1, 0, 0}) == ""); // fewer than 4 bytes (no tag)
}
// --- wav_trim -> extractFloatFrames -> downmixToMono integration ---------------
//
// Locks the interleave-stride contract at the seam between wav_trim and sample_map:
// wav_trim reports channelCount, extractFloatFrames yields interleaved samples with
// that stride, and downmixToMono divides by that same stride. If either module
// changed its understanding of the layout (e.g. extractFloatFrames started packing
// differently, or downmixToMono changed its stride divisor), this test catches it.
static void putU16sm(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 putU32sm(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 putTagsm(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 putFloatsm(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]);
}
// Build a 32-bit-float WAV byte buffer. Samples: frame f, channel c = value(f, c).
template <typename Fn>
static std::vector<std::uint8_t> buildWav(std::uint16_t channels,
std::uint32_t sampleRate,
std::size_t frames,
Fn value) {
const std::uint32_t dataBytes =
static_cast<std::uint32_t>(frames * channels * 4u);
std::vector<std::uint8_t> chunks;
putTagsm(chunks, "fmt ");
putU32sm(chunks, 16);
putU16sm(chunks, 3); // IEEE float
putU16sm(chunks, channels);
putU32sm(chunks, sampleRate);
putU32sm(chunks, sampleRate * channels * 4u);
putU16sm(chunks, static_cast<std::uint16_t>(channels * 4));
putU16sm(chunks, 32);
putTagsm(chunks, "data");
putU32sm(chunks, dataBytes);
for (std::size_t f = 0; f < frames; ++f)
for (std::uint16_t c = 0; c < channels; ++c)
putFloatsm(chunks, value(f, c));
std::vector<std::uint8_t> wav;
putTagsm(wav, "RIFF");
putU32sm(wav, static_cast<std::uint32_t>(4 + chunks.size()));
putTagsm(wav, "WAVE");
wav.insert(wav.end(), chunks.begin(), chunks.end());
return wav;
}
static void testWavTrimToDownmixPipelineStereo() {
// Stereo WAV: frame f, L = f * 0.1f, R = f * 0.1f + 0.5f. Expected mono average:
// (f * 0.1f + f * 0.1f + 0.5f) / 2 = f * 0.1f + 0.25f.
const std::size_t kFrames = 4;
auto wav = buildWav(2, 48000, kFrames,
[](std::size_t f, std::uint16_t c) {
return static_cast<float>(f) * 0.1f + (c == 1 ? 0.5f : 0.0f);
});
WavLayout layout = parseWavLayout(wav);
CHECK(layout.valid);
CHECK(layout.channelCount == 2);
CHECK(layout.frameCount() == kFrames);
const std::vector<AudioSample> interleaved =
extractFloatFrames(wav, layout, 0, layout.frameCount());
CHECK(interleaved.size() == kFrames * 2);
const std::vector<AudioSample> mono = downmixToMono(interleaved, layout.channelCount);
CHECK(mono.size() == kFrames);
for (std::size_t f = 0; f < kFrames; ++f) {
const float expected = static_cast<float>(f) * 0.1f + 0.25f;
CHECK(approx(mono[f], expected));
}
}
static void testWavTrimToDownmixPipelineMono() {
// Mono WAV: extractFloatFrames -> downmixToMono with channelCount==1 is a passthrough.
const std::size_t kFrames = 3;
auto wav = buildWav(1, 44100, kFrames,
[](std::size_t f, std::uint16_t) {
return static_cast<float>(f) * 0.5f;
});
WavLayout layout = parseWavLayout(wav);
CHECK(layout.valid);
CHECK(layout.channelCount == 1);
const std::vector<AudioSample> interleaved =
extractFloatFrames(wav, layout, 0, layout.frameCount());
CHECK(interleaved.size() == kFrames);
const std::vector<AudioSample> mono = downmixToMono(interleaved, layout.channelCount);
CHECK(mono.size() == kFrames);
CHECK(approx(mono[0], 0.0) && approx(mono[1], 0.5) && approx(mono[2], 1.0));
}
int main() {
testSelectByIdHit();
testSelectFirstSampleFallbackOnEmptyId();
@@ -269,6 +372,8 @@ int main() {
testSelectionStateEmptyId();
testSelectionStateWrongVersion();
testSelectionStateTruncated();
testWavTrimToDownmixPipelineStereo();
testWavTrimToDownmixPipelineMono();
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
return g_fail != 0;