fix: guard filter tail NaNs, skip static-filter envelope work, pin stereo filter path, correct stale comments

Codec fallback for non-finite filter fields, a modAmount==0 early-out in tickFilterCutoff,
new stereo render tests for the dual-mono mirror, and comment/test accuracy fixes flagged
in review (stale deck-width claims, restated invariants, drifted CMake link comments).
This commit is contained in:
2026-07-30 17:35:45 -04:00
parent 67215509cb
commit 39389c1183
14 changed files with 137 additions and 41 deletions
+26
View File
@@ -11,6 +11,7 @@
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
@@ -608,6 +609,30 @@ static void testFilterTailRoundTripsLosslessly() {
reasampler::instrument::engine::VelocityCurve::flat()));
}
// A non-finite modAmount/velAmount/keyTrack (a corrupt blob, or any writer that skipped the
// same guard the v8 master gain already applies) must lift to the neutral default rather than
// reach Voice::tickFilterCutoff, where both clamp compares are false against NaN and the
// static_cast<int> is UB. Mirrors testCorruptFieldsFallBackToDefaults' per-field precedent.
static void testNonFiniteFilterFieldsLiftToTheNeutralDefault() {
ComponentState in;
in.selectionId = "pad";
FilterSeconds& f = in.params.play.filter;
f.enabled = true;
f.modAmount = std::numeric_limits<double>::quiet_NaN();
f.velAmount = std::numeric_limits<double>::infinity();
f.keyTrack = -std::numeric_limits<double>::infinity();
const ComponentState out =
deserializeComponentState(serializeComponentState(in), 48000.0);
const FilterSeconds& g = out.params.play.filter;
const FilterSeconds def;
CHECK(g.modAmount == def.modAmount);
CHECK(g.velAmount == def.velAmount);
CHECK(g.keyTrack == def.keyTrack);
// The fallback is per-field, not per-record: the untouched fields still round-trip.
CHECK(g.enabled);
}
// The WRITER emits the CURRENT payload version, and the marker + version sit at the head of
// the payload — the self-describing property every legacy branch depends on. Asserted
// against the semantic constants, not literals.
@@ -1080,6 +1105,7 @@ int main() {
testTruncationDegradesCleanly();
testV8RecordLiftsToTheOffNeutralFilter();
testFilterTailRoundTripsLosslessly();
testNonFiniteFilterFieldsLiftToTheNeutralDefault();
if (failures == 0) {
std::printf("component_state_io_tests: all tests passed\n");
return 0;
+2 -1
View File
@@ -96,7 +96,8 @@ static void testWrappedDeckHeightAtThePinnedEditorWidths() {
// the remaining four fit the second.
CHECK(deckRowCount(g, kAvailAtDefaultWidth) == 2);
CHECK(deckHeight(g, kAvailAtDefaultWidth) == 2 * kDeckGroupH + kDeckRowGap);
// At the 560 floor it takes four; FILTER is wider than the row on its own.
// At the 560 floor it takes four: FILTER (440px) fits the row alone, but not alongside
// PITCH + PITCH ENV (318 + 12 + 440 = 770 > 544), so it wraps to its own row.
CHECK(deckRowCount(g, kAvailAtMinWidth) == 4);
CHECK(deckHeight(g, kAvailAtMinWidth) == 4 * kDeckGroupH + 3 * kDeckRowGap);
-14
View File
@@ -51,19 +51,6 @@ static void testGroupWidth() {
CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
}
static void testShellDeckFitsOneRowAtDefaultWidth() {
// The r11 default window is 840 with kPad=8 margins -> 824 available. The five shell
// groups must fit ONE deck row there (the elastic hero keeps ~430px — the layout spec's
// premise). Locks the constants against accidental growth.
const auto deck = shellLikeDeck();
int total = 0;
for (const auto& g : deck) total += deckGroupWidth(g);
total += (static_cast<int>(deck.size()) - 1) * kDeckGroupGap;
CHECK(total <= 824);
CHECK(deckRowCount(deck, 824) == 1);
CHECK(deckHeight(deck, 824) == kDeckGroupH);
}
static void testWrapAtNarrowWidthIsDeterministic() {
// At the 560x460 checkSizeConstraint floor (544 available) the deck wraps to TWO rows,
// whole trailing groups only.
@@ -185,7 +172,6 @@ static void testEmptyDeck() {
int main() {
testGroupWidth();
testShellDeckFitsOneRowAtDefaultWidth();
testWrapAtNarrowWidthIsDeterministic();
testFirstGroupAlwaysPlaces();
testGroupInnerGeometry();
+65 -1
View File
@@ -9,6 +9,7 @@
#include <cmath>
#include <cstdio>
#include <type_traits>
#include <utility>
#include <vector>
using namespace reasampler;
@@ -67,6 +68,22 @@ static std::vector<double> render(SampleData& sample, int note, int velocity, in
return out;
}
// Renders via renderFrameStereo — exercises voice.h's stereo filter block (both the
// filter_.process(1, ...) call for a genuinely stereo source and the dual-mono mirror for a
// mono one), never touched by the mono `render` helper above.
static std::vector<std::pair<double, double>> renderStereo(SampleData& sample, int note,
int velocity, int frames) {
Voice v;
v.start(note, velocity, sample);
std::vector<std::pair<double, double>> out(static_cast<std::size_t>(frames));
for (int i = 0; i < frames; ++i) {
AudioSample l = 0.0f, r = 0.0f;
v.renderFrameStereo(l, r);
out[static_cast<std::size_t>(i)] = {static_cast<double>(l), static_cast<double>(r)};
}
return out;
}
static double rms(const std::vector<double>& x, std::size_t from, std::size_t to) {
double acc = 0.0;
for (std::size_t i = from; i < to; ++i) acc += x[i] * x[i];
@@ -218,7 +235,8 @@ static void testModAmountPolarityDrivesCutoffFromOppositeEnds() {
const std::vector<double> falling = sweep(1.0f, -1.0);
CHECK(rms(falling, 10000, 12000) < 0.05 * rms(falling, 0, 2000));
// Zero depth: the envelope is still running, but it must not reach cutoff at all.
// Zero depth: tickFilterCutoff's early-out skips the envelope entirely, so cutoff must
// never move regardless of how long the (unread) envelope stage runs.
const std::vector<double> steady = sweep(0.5f, 0.0);
const double early = rms(steady, 2000, 4000);
const double late = rms(steady, 10000, 12000);
@@ -282,6 +300,50 @@ static void testNoteOnResetsTheFilterSoAPreviousNoteCannotLeak() {
for (std::size_t i = 0; i < fresh.size(); ++i) CHECK(restarted[i] == fresh[i]);
}
// ---------------------------------------------------------------------------
// Stereo: the two filter paths renderFrame() (mono) never reaches.
// ---------------------------------------------------------------------------
static void testStereoRenderOfAMonoSampleMirrorsTheMonoResultExactly() {
// A mono SampleData in a stereo render never calls filter_.process(1, ...) — voice.h's
// dual-mono block copies channel 0's already-filtered result to channel 1 instead. Pin
// both channels bit-identical to the plain mono render.
SampleData s = periodicSine(4000, 64);
s.play.adsr = flatAdsr();
s.play.filter = engagedFilter(0.3f, 0.85f, 1.0f);
s.play.filter.settings.driveNorm = 0.6f;
SampleData monoSrc = s;
const std::vector<double> mono = render(monoSrc, 60, 100, 2000);
SampleData stereoSrc = s;
const auto stereo = renderStereo(stereoSrc, 60, 100, 2000);
for (std::size_t i = 0; i < stereo.size(); ++i) {
CHECK(stereo[i].first == mono[i]);
CHECK(stereo[i].second == mono[i]);
}
}
static void testStereoFilterChannel1MatchesChannel0ForIdenticalLRInput() {
// A genuinely stereo sample whose channels are identical drives filter_.process(1, ...)
// for real (haveR true). Identical input plus identical reset() state must give identical
// output on both channels — the mirror's correctness argument, pinned against the actual
// per-channel path instead of only asserted in a comment.
SampleData s = periodicSine(4000, 64);
s.framesR = s.frames; // genuinely stereo, L == R
s.play.adsr = flatAdsr();
s.play.filter = engagedFilter(0.3f, 0.85f, 1.0f);
s.play.filter.settings.driveNorm = 0.6f;
const auto stereo = renderStereo(s, 60, 100, 2000);
bool sawSignal = false;
for (const auto& lr : stereo) {
CHECK(lr.first == lr.second);
if (std::fabs(lr.first) > 1e-6) sawSignal = true;
}
CHECK(sawSignal);
}
int main() {
testDisengagedFilterIsBitInertEvenWithExtremeSettingsStored();
testFilterSeesThePreAmpSignalSoAmpGainScalesTheResultLinearly();
@@ -289,6 +351,8 @@ int main() {
testModAmountPolarityDrivesCutoffFromOppositeEnds();
testVelocityAndKeyTrackingReachCutoffAndAreNoOpsAtTheirDefaults();
testNoteOnResetsTheFilterSoAPreviousNoteCannotLeak();
testStereoRenderOfAMonoSampleMirrorsTheMonoResultExactly();
testStereoFilterChannel1MatchesChannel0ForIdenticalLRInput();
if (g_fail == 0) std::printf("sampler_filter: all tests passed\n");
return g_fail == 0 ? 0 : 1;
}