Bound the automation hold to the window the model has not caught up on, and make that authority model stated, enforced and tested
This commit is contained in:
@@ -938,6 +938,106 @@ static void testAPublishedPitchOffsetRefitsThePitchEnvelopeSpan() {
|
||||
if (!(life > 11000 && life < 13000)) std::printf(" refit span: life %zu\n", life);
|
||||
}
|
||||
|
||||
// Key-track is the second member of that class, and it is a PITCH-RATIO scalar: a sounding note
|
||||
// must not be retuned by it, the next note-on must take it. Measured at a note away from the root
|
||||
// (the ratio is 1.0 at the root whatever key-track says, so the root would prove nothing).
|
||||
static void testAKeyTrackChangeSparesTheSoundingNoteAndReachesTheNextOne() {
|
||||
SampleData still = rampForReadRate();
|
||||
SampleData moved = rampForReadRate();
|
||||
LiveParams blockA, blockB;
|
||||
LiveValues halfTrack = foldLive(moved.play, moved.keyTrack);
|
||||
halfTrack.keyTrack = 0.5; // half key-tracking: an octave up reads at ratio ~1.414, not 2.0
|
||||
|
||||
const std::vector<AudioSample> baseline =
|
||||
renderPreserveCapable(still, blockA, nullptr, -1, 72);
|
||||
const std::vector<AudioSample> swept =
|
||||
renderPreserveCapable(moved, blockB, &halfTrack, 8, 72);
|
||||
CHECK(baseline.size() == swept.size());
|
||||
bool untouched = true;
|
||||
for (std::size_t i = 0; i < baseline.size() && i < swept.size(); ++i) {
|
||||
if (baseline[i] != swept[i]) { untouched = false; break; }
|
||||
}
|
||||
CHECK(untouched);
|
||||
|
||||
// The next note-on takes it, read straight off the ramp: under Varispeed the output value at
|
||||
// frame i IS the read position, so the slope over one block is the pitch ratio.
|
||||
auto slopePerFrame = [&](double keyTrack) {
|
||||
SampleData fresh = rampForReadRate();
|
||||
LiveParams block;
|
||||
fresh.live = █
|
||||
LiveValues published = foldLive(fresh.play, fresh.keyTrack);
|
||||
published.keyTrack = keyTrack;
|
||||
block.publish(published);
|
||||
VoiceEngine engine(1, fresh);
|
||||
engine.noteOn(72, 100);
|
||||
std::vector<AudioSample> out;
|
||||
engine.render(out, 512);
|
||||
return (static_cast<double>(out.back()) - static_cast<double>(out.front())) /
|
||||
static_cast<double>(out.size() - 1) * 200000.0;
|
||||
};
|
||||
// kKeyTrackDefault is 1.0 — full tracking, so an octave up reads at 2.0.
|
||||
CHECK(std::fabs(slopePerFrame(1.0) - 2.0) < 0.01);
|
||||
CHECK(std::fabs(slopePerFrame(0.5) - std::pow(2.0, 0.5)) < 0.01);
|
||||
// And the value really is carried by the BLOCK: sample.play/keyTrack never moved.
|
||||
CHECK(std::fabs(slopePerFrame(0.0) - 1.0) < 0.01);
|
||||
}
|
||||
|
||||
// Trigger length is the third: it resolves playEnd_, so it re-spans the NEXT note and leaves the
|
||||
// sounding one at the span it was struck with.
|
||||
static void testATriggerLengthChangeSparesTheSoundingNoteAndReachesTheNextOne() {
|
||||
auto triggerSource = [] {
|
||||
SampleData s = rampForReadRate();
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.trigger.lengthFraction = 1.0;
|
||||
s.play.trigAhd.holdFraction = 1.0; // flat through the span, so the span IS the lifetime
|
||||
return s;
|
||||
};
|
||||
// The note's LIFETIME is what the fraction spans, so blocks-alive measures it directly.
|
||||
auto blocksAlive = [&](double fraction) {
|
||||
SampleData fresh = triggerSource();
|
||||
LiveParams block;
|
||||
fresh.live = █
|
||||
LiveValues published = foldLive(fresh.play, fresh.keyTrack);
|
||||
published.lengthFraction = fraction;
|
||||
block.publish(published);
|
||||
VoiceEngine engine(1, fresh);
|
||||
engine.noteOn(60, 100);
|
||||
std::vector<AudioSample> out;
|
||||
int blocks = 0;
|
||||
while (engine.activeVoiceCount() > 0 && blocks < 4000) {
|
||||
engine.render(out, 512);
|
||||
++blocks;
|
||||
}
|
||||
return blocks;
|
||||
};
|
||||
const int whole = blocksAlive(1.0);
|
||||
const int quarterSpan = blocksAlive(0.25);
|
||||
CHECK(whole > 100 && whole < 4000);
|
||||
CHECK(std::fabs(static_cast<double>(quarterSpan) - 0.25 * whole) < 0.05 * whole);
|
||||
|
||||
// And the sounding note is spared. The published fraction is small enough that its span ENDS
|
||||
// inside the window rendered — asserted, not assumed, because a fraction whose playEnd_ still
|
||||
// sat past the render would leave the two runs identical whether the field were live or not.
|
||||
constexpr int kSweepBlocks = 24; // renderPreserveCapable's own loop count
|
||||
CHECK(blocksAlive(0.05) < kSweepBlocks);
|
||||
|
||||
SampleData still = triggerSource();
|
||||
SampleData moved = triggerSource();
|
||||
LiveParams blockA, blockB;
|
||||
LiveValues shortened = foldLive(moved.play, moved.keyTrack);
|
||||
shortened.lengthFraction = 0.05;
|
||||
const std::vector<AudioSample> baseline =
|
||||
renderPreserveCapable(still, blockA, nullptr, -1, 60);
|
||||
const std::vector<AudioSample> swept =
|
||||
renderPreserveCapable(moved, blockB, &shortened, 8, 60);
|
||||
CHECK(baseline.size() == swept.size());
|
||||
bool untouched = true;
|
||||
for (std::size_t i = 0; i < baseline.size() && i < swept.size(); ++i) {
|
||||
if (baseline[i] != swept[i]) { untouched = false; break; }
|
||||
}
|
||||
CHECK(untouched);
|
||||
}
|
||||
|
||||
// --- What stays latched at note-on -------------------------------------------------------
|
||||
|
||||
static void testPitchRatioAndVelocityGainStayLatched() {
|
||||
@@ -966,8 +1066,12 @@ static void testPitchRatioAndVelocityGainStayLatched() {
|
||||
|
||||
std::vector<AudioSample> out;
|
||||
LiveValues hostile = foldLive(s.play, s.keyTrack);
|
||||
// Everything the block CAN carry, moved as far as it goes. None of it names velocity, the
|
||||
// note, the pitch ratio, or the PCM — that is the property under test.
|
||||
// Everything the block CAN carry, moved as far as it goes. keyTrack and lengthFraction DO
|
||||
// name the pitch ratio and the play span — they are here precisely because a SOUNDING voice
|
||||
// must not read either, which is what makes them note-on-latched rather than live; the tests
|
||||
// above are what prove the next note does take them.
|
||||
hostile.keyTrack = 0.0;
|
||||
hostile.lengthFraction = 0.05;
|
||||
hostile.filterKeyTrack = 2.0;
|
||||
hostile.filterSettings.cutoffNorm = 0.0f;
|
||||
hostile.filterModAmount = 1.0;
|
||||
@@ -1078,6 +1182,8 @@ int main() {
|
||||
testEveryLiveFilterControlMovesTheSoundingNote();
|
||||
testOneBlockServesTwoIndependentObservers();
|
||||
testARateChangeSpareTheSoundingNoteAndReachesTheNextOne();
|
||||
testAKeyTrackChangeSparesTheSoundingNoteAndReachesTheNextOne();
|
||||
testATriggerLengthChangeSparesTheSoundingNoteAndReachesTheNextOne();
|
||||
testAPitchOffsetChangeMovesTheSoundingNoteInBothEngines();
|
||||
testAPublishedPitchOffsetLeavesTheStagedAttackWallClock();
|
||||
testAPublishedPitchOffsetRefitsThePitchEnvelopeSpan();
|
||||
|
||||
Reference in New Issue
Block a user