FA1: unity-Preserve bypass kills preview onset latency; drain-slot reload keeps voices ringing through curve edits; velocity path proven end-to-end

This commit is contained in:
2026-07-27 18:25:01 -04:00
parent 2b8ab4abe4
commit d9321eaf3a
6 changed files with 275 additions and 61 deletions
+45
View File
@@ -1419,6 +1419,50 @@ static void testVelocityCurveResolvesToZone() {
CHECK(r.zones[0].velocityCurve.equals(vst::VelocityCurve::linear()));
}
// FA1 bug 3a — the COMPOSED end-to-end regression, mirroring the processor's reload composition
// exactly: an authored curve survives the component-state round-trip (the save/load seam), then
// resolvePerformance -> buildZonedKeymap -> VoiceEngine (constructed with a Preserve window, the
// DAW configuration) -> render, and the rendered level tracks velocity through the curve. This
// is the full pure slice of the click-to-sound path; only the bridge read + WAV decode (shell
// I/O) are outside it. A y=x curve at velocity 1 must be near-silent — NOT max volume.
static void testVelocityCurveEndToEndThroughReloadComposition() {
// 1. The instrument's own state: one full-keyboard zone with a LINEAR curve (the exact edit
// Daniel made), round-tripped through the v7 component-state wire (save -> load).
ComponentState s;
s.selectionId = "a";
PerformanceZone z = zone("a", 0, 127);
z.velocityCurve = vst::VelocityCurve::linear();
s.map.zones.push_back(z);
const ComponentState back = deserializeComponentState(serializeComponentState(s), 48000.0);
CHECK(back.map.zones.size() == 1);
if (back.map.zones.size() != 1) return;
// 2. Resolve against a live bank blob (the shared bank_book parse, root 60 intrinsic).
const std::string json = bookJson({makeSample("a", "Kick", "reasampler_bank/a.wav", 60)}, {});
const ResolvedPerformance rp = resolvePerformance(json, back.map);
CHECK(rp.zones.size() == 1);
if (rp.zones.size() != 1) return;
// The round-tripped zone still runs the PRESERVE product default (the DAW engine config).
CHECK(rp.zones[0].play.pitchEngine == PitchEngine::Preserve);
// 3. Build the zoned keymap from decoded DC-1 PCM and play it through an engine constructed
// the way reloadFromBank constructs it (Preserve voices pre-sized to a real window).
auto steadyLevelAt = [&](int vel) -> double {
DecodedZonePcm pcm;
pcm.monoFrames.assign(4000, 1.0f);
pcm.sampleRate = 48000;
const Keymap km = buildZonedKeymap(rp.zones, {pcm});
VoiceEngine eng(16, km, /*preserveCap=*/8, /*window=*/256);
eng.noteOn(62, vel); // transposed: the genuine OLA shifter path
std::vector<AudioSample> out;
eng.render(out, 1000);
return static_cast<double>(out[900]); // steady state (ring fully DC past the window)
};
CHECK(approx(steadyLevelAt(127), 1.0));
CHECK(approx(steadyLevelAt(64), 64.0 / 127.0));
CHECK(steadyLevelAt(1) < 0.02); // velocity 1 through y=x: near-silent, never max volume
}
static void testVelocityCurveV6BackCompatLiftsToFlat() {
// A v6 PAYLOAD blob (marker + version 6 + full play tail + keyTrack, but NO velocity-curve field)
// lifts every zone to VelocityCurve::flat() (R10-F1 Option A — flat y=1). This is the DELIBERATE
@@ -1822,6 +1866,7 @@ int main() {
testVelocityCurveRoundTrip();
testVelocityCurveThroughComponentEnvelope();
testVelocityCurveResolvesToZone();
testVelocityCurveEndToEndThroughReloadComposition();
testVelocityCurveV6BackCompatLiftsToFlat();
testPlayParamsV2BackCompatLiftsToDefaults();
testPlayParamsThroughComponentEnvelope();