instrument: latch the note done at the read-head run-off, fit the migrated fades, and lift the curve dial and overlay selection into pure modules

This commit is contained in:
2026-07-31 09:17:04 -04:00
parent 13e8c5c4d9
commit d60ab1524a
18 changed files with 575 additions and 129 deletions
+23
View File
@@ -688,6 +688,28 @@ static void testResolvePlayRoundsAndFloorsNegatives() {
CHECK(p.adsr.releaseFrames == 0);
}
// The retired Trigger fade pair was SOURCE frames; the AHD that replaced it stores wall-clock
// seconds, and the codec's lift can only divide by the PROJECT rate. This is the far end of
// that seam: the build multiplies by the DECODE rate, so a migrated fade comes back scaled by
// decodeRate/projectRate whenever a file's own rate differs from the project's. The bound is
// documented at the lift in component_state_io.h; this is its measured size.
static void testMigratedFadeStretchesWhenTheDecodeRateDiffersFromTheProjectRate() {
PlaySeconds st;
st.trigAhd.attackSeconds = 441.0 / 44100.0; // a 441-SOURCE-frame fade lifted at 44.1k
st.trigAhd.decaySeconds = 882.0 / 44100.0;
// Matched rates are EXACT: the round trip through seconds loses nothing.
const PlayParams matched = resolvePlay(st, 44100);
CHECK(matched.trigAhd.attackFrames == 441);
CHECK(matched.trigAhd.decayFrames == 882);
// A 44.1 kHz file opened in a 48 kHz project: 441 * 48000/44100 = 480 source frames, ~8.8%
// longer than the fade the saved instance actually had.
const PlayParams stretched = resolvePlay(st, 48000);
CHECK(stretched.trigAhd.attackFrames == 480);
CHECK(stretched.trigAhd.decayFrames == 960);
}
// --- resolveCapture: the ONE override-beats-intrinsic fold ---------------------
static SelectedSample ref(const std::string& rel, int root, bool hasLoop = false,
@@ -914,6 +936,7 @@ int main() {
testResolvePlayConvertsWallClockAtTheRate();
testResolvePlayCarriesTheFilterAndResolvesOnlyItsEnvelope();
testResolvePlayRoundsAndFloorsNegatives();
testMigratedFadeStretchesWhenTheDecodeRateDiffersFromTheProjectRate();
testResolveCaptureUsesIntrinsicsWhenNoOverride();
testResolveCaptureOverridesBeatIntrinsics();
testResolveCaptureLoopOverrideCanDisableTheLoop();