instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes
Trigger's fade pair folds into the AHD (and goes live); the release anchors right; Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
@@ -191,36 +191,76 @@ static void testSustainLevelChangeGlides() {
|
||||
}
|
||||
|
||||
static void testPitchEnvelopeHoldsPhaseAndGlidesDepth() {
|
||||
// No hold stage, so the shape is the attack-decay one the pre-AHD envelope had.
|
||||
PitchEnvParams p;
|
||||
p.enabled = true;
|
||||
p.attackFrames = 0;
|
||||
p.decayFrames = 1000;
|
||||
p.peakSemitones = 12.0;
|
||||
p.shape.attackFrames = 0;
|
||||
p.shape.decayFrames = 1000;
|
||||
p.shape.holdFraction = 0.0;
|
||||
PitchEnvelope a, b;
|
||||
a.configure(p);
|
||||
b.configure(p);
|
||||
a.configure(100000, p);
|
||||
b.configure(100000, p);
|
||||
a.noteOn();
|
||||
b.noteOn();
|
||||
for (int i = 0; i < 400; ++i) { a.tick(); b.tick(); }
|
||||
|
||||
b.applyLive(0, 2000, 12.0); // decay doubled mid-decay
|
||||
PitchEnvParams longer = p;
|
||||
longer.shape.decayFrames = 2000;
|
||||
b.applyLive(longer); // decay doubled mid-decay
|
||||
CHECK(a.tick() == b.tick()); // phi held: the semitone offset is unchanged this frame
|
||||
|
||||
// A depth move is a level step, so it glides rather than jumping: the first frame after
|
||||
// the edit is exactly what the unedited peer emits.
|
||||
PitchEnvelope c, d;
|
||||
c.configure(p);
|
||||
d.configure(p);
|
||||
c.configure(100000, p);
|
||||
d.configure(100000, p);
|
||||
c.noteOn();
|
||||
d.noteOn();
|
||||
for (int i = 0; i < 400; ++i) { c.tick(); d.tick(); }
|
||||
c.applyLive(0, 1000, 0.0); // depth to zero mid-decay
|
||||
PitchEnvParams noDepth = p;
|
||||
noDepth.peakSemitones = 0.0;
|
||||
c.applyLive(noDepth); // depth to zero mid-decay
|
||||
CHECK(c.tick() == d.tick());
|
||||
// ...and it does eventually reach the new depth rather than staying put.
|
||||
for (int i = 0; i < 400; ++i) c.tick();
|
||||
CHECK(c.tick() == 0.0);
|
||||
}
|
||||
|
||||
// The pitch envelope's new middle stage, on the same phi rule: a hold dialled mid-hold keeps
|
||||
// the level (flat by definition) and moves the boundary, and the fraction is taken against
|
||||
// what attack and decay left rather than against the whole span.
|
||||
static void testPitchEnvelopeHoldStagePlaysAndHoldsPhase() {
|
||||
PitchEnvParams p;
|
||||
p.enabled = true;
|
||||
p.peakSemitones = 12.0;
|
||||
p.shape.attackFrames = 100;
|
||||
p.shape.decayFrames = 100;
|
||||
p.shape.holdFraction = 0.5; // half of (1000 - 200) = 400 frames of hold
|
||||
PitchEnvelope e;
|
||||
e.configure(1000, p);
|
||||
e.noteOn();
|
||||
for (int i = 0; i < 100; ++i) e.tick(); // through the attack
|
||||
CHECK(e.tick() == 12.0); // frame 100: at the peak, holding
|
||||
for (int i = 0; i < 398; ++i) e.tick(); // to the last frame of the hold
|
||||
CHECK(e.tick() == 12.0); // frame 499: still holding
|
||||
CHECK(e.tick() == 12.0); // frame 500: decay's own first frame
|
||||
CHECK(std::fabs(e.tick() - 12.0 * (1.0 - 1.0 / 100.0)) < 1e-12); // frame 501: descending
|
||||
|
||||
// A live hold change mid-hold is continuous (the stage is flat) and the envelope still
|
||||
// finishes inside the span.
|
||||
PitchEnvelope f;
|
||||
f.configure(1000, p);
|
||||
f.noteOn();
|
||||
for (int i = 0; i < 300; ++i) f.tick();
|
||||
PitchEnvParams wider = p;
|
||||
wider.shape.holdFraction = 1.0;
|
||||
f.applyLive(wider);
|
||||
CHECK(f.tick() == 12.0);
|
||||
for (int i = 0; i < 1200; ++i) f.tick();
|
||||
CHECK(f.tick() == 0.0);
|
||||
}
|
||||
|
||||
// --- The fresh-note path: snap, never the phi rule ---------------------------------------
|
||||
|
||||
static void testAFreshEnvelopeTakesANewlyDialledStageTimeOutright() {
|
||||
@@ -258,9 +298,12 @@ static void testAFreshPitchEnvelopeTakesTheNewTimesOutright() {
|
||||
PitchEnvParams stale; // enabled, but every leg zero
|
||||
stale.enabled = true;
|
||||
PitchEnvelope env;
|
||||
env.configure(stale);
|
||||
env.configure(100000, stale);
|
||||
env.noteOn();
|
||||
env.snapLive(0, 1000, 12.0);
|
||||
PitchEnvParams dialled = stale;
|
||||
dialled.peakSemitones = 12.0;
|
||||
dialled.shape.decayFrames = 1000;
|
||||
env.snapLive(dialled);
|
||||
CHECK(env.tick() == 12.0); // at the top of the new decay leg, not past the envelope
|
||||
for (int i = 0; i < 499; ++i) env.tick();
|
||||
CHECK(std::fabs(env.tick() - 6.0) < 1e-12);
|
||||
@@ -412,25 +455,25 @@ static void testEveryEnvelopeStageTimeAndLevelMovesTheSoundingNote() {
|
||||
{"pitch env attack",
|
||||
[](SampleData& s) {
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.attackFrames = 48000;
|
||||
s.play.pitchEnv.decayFrames = 48000;
|
||||
s.play.pitchEnv.shape.attackFrames = 48000;
|
||||
s.play.pitchEnv.shape.decayFrames = 48000;
|
||||
s.play.pitchEnv.peakSemitones = 12.0;
|
||||
},
|
||||
[](LiveValues& v) { v.pitchEnvAttackFrames = 4000; }, -1},
|
||||
[](LiveValues& v) { v.pitchEnv.shape.attackFrames = 4000; }, -1},
|
||||
{"pitch env decay",
|
||||
[](SampleData& s) {
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.decayFrames = 48000;
|
||||
s.play.pitchEnv.shape.decayFrames = 48000;
|
||||
s.play.pitchEnv.peakSemitones = 12.0;
|
||||
},
|
||||
[](LiveValues& v) { v.pitchEnvDecayFrames = 8000; }, -1},
|
||||
[](LiveValues& v) { v.pitchEnv.shape.decayFrames = 8000; }, -1},
|
||||
{"pitch env depth",
|
||||
[](SampleData& s) {
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.decayFrames = 480000;
|
||||
s.play.pitchEnv.shape.decayFrames = 480000;
|
||||
s.play.pitchEnv.peakSemitones = 12.0;
|
||||
},
|
||||
[](LiveValues& v) { v.pitchEnvPeakSemitones = 0.0; }, -1},
|
||||
[](LiveValues& v) { v.pitchEnv.peakSemitones = 0.0; }, -1},
|
||||
};
|
||||
for (const Case& c : cases) {
|
||||
assertLiveFieldMovesTheSoundingNote(c.name, c.rig, c.mutate, c.noteOffBlock);
|
||||
@@ -646,9 +689,9 @@ static void testPitchRatioAndVelocityGainStayLatched() {
|
||||
hostile.filterKeyTrack = 2.0;
|
||||
hostile.filterSettings.cutoffNorm = 0.0f;
|
||||
hostile.filterModAmount = 1.0;
|
||||
hostile.pitchEnvAttackFrames = 4800;
|
||||
hostile.pitchEnvDecayFrames = 4800;
|
||||
hostile.pitchEnvPeakSemitones = 24.0;
|
||||
hostile.pitchEnv.shape.attackFrames = 4800;
|
||||
hostile.pitchEnv.shape.decayFrames = 4800;
|
||||
hostile.pitchEnv.peakSemitones = 24.0;
|
||||
hostile.adsr.attackFrames = 96000; // a timed stage the voice is already past
|
||||
for (int blk = 0; blk < 8; ++blk) {
|
||||
if (blk == 2) block.publish(hostile);
|
||||
@@ -696,7 +739,7 @@ static void testVelocityGainSurvivesAHostilePublishThatReallyLands() {
|
||||
filterSweep(rig);
|
||||
rig.play.filter.velAmount = 0.0;
|
||||
rig.play.pitchEnv.enabled = true;
|
||||
rig.play.pitchEnv.decayFrames = 24000;
|
||||
rig.play.pitchEnv.shape.decayFrames = 24000;
|
||||
rig.play.pitchEnv.peakSemitones = 3.0;
|
||||
|
||||
LiveValues hostile = foldLive(rig.play);
|
||||
@@ -705,9 +748,9 @@ static void testVelocityGainSurvivesAHostilePublishThatReallyLands() {
|
||||
hostile.filterModAmount = -1.0;
|
||||
hostile.filterEnv.decayFrames = 4800;
|
||||
hostile.filterEnv.sustainLevel = 0.0;
|
||||
hostile.pitchEnvAttackFrames = 4800;
|
||||
hostile.pitchEnvDecayFrames = 4800;
|
||||
hostile.pitchEnvPeakSemitones = 24.0;
|
||||
hostile.pitchEnv.shape.attackFrames = 4800;
|
||||
hostile.pitchEnv.shape.decayFrames = 4800;
|
||||
hostile.pitchEnv.peakSemitones = 24.0;
|
||||
hostile.adsr.sustainLevel = 0.4;
|
||||
|
||||
SampleData quiet = rig, loud = rig, untouched = rig;
|
||||
@@ -743,6 +786,7 @@ int main() {
|
||||
testShortenedStageStillLandsContinuously();
|
||||
testSustainLevelChangeGlides();
|
||||
testPitchEnvelopeHoldsPhaseAndGlidesDepth();
|
||||
testPitchEnvelopeHoldStagePlaysAndHoldsPhase();
|
||||
testAFreshEnvelopeTakesANewlyDialledStageTimeOutright();
|
||||
testAFreshPitchEnvelopeTakesTheNewTimesOutright();
|
||||
testCutoffMoveAcrossPrepareDoesNotStep();
|
||||
|
||||
Reference in New Issue
Block a user