instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter
Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
@@ -1766,7 +1766,8 @@ static SampleData twoLevelSample() {
|
||||
km.velocityCurve = VelocityCurve::fromPoints({{0.0, 0.0},
|
||||
{static_cast<double>(kVelLow), 0.25},
|
||||
{static_cast<double>(kVelHigh), 0.75},
|
||||
{127.0, 1.0}});
|
||||
{127.0, 1.0}},
|
||||
instrument::engine::CurveDomain::Unipolar);
|
||||
return km;
|
||||
}
|
||||
|
||||
@@ -1966,6 +1967,65 @@ static SampleData rampSample(std::size_t frames, int rootNote) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- velocity -> pitch ---------------------------------------------------------
|
||||
//
|
||||
// A ramp sample reads its own position, so the value on frame N IS the accumulated read rate:
|
||||
// the transpose is directly observable rather than inferred from a spectrum.
|
||||
|
||||
static void testVelocityPitchIsExactlyOffByDefault() {
|
||||
// The modulation VALUE at every velocity, not a rendered approximation of it: the default
|
||||
// bipolar curve must yield the identity ratio exactly, so nothing detunes by a hair.
|
||||
const PlayParams def;
|
||||
for (int v = 0; v <= 127; ++v) {
|
||||
CHECK(velocityPitchRatio(def.pitchVelocityCurve, v) == 1.0);
|
||||
}
|
||||
// And at the render: two strikes of very different velocity read the ramp identically.
|
||||
SampleData s = rampSample(4096, 60);
|
||||
Voice soft;
|
||||
Voice hard;
|
||||
soft.start(60, 1, s);
|
||||
hard.start(60, 127, s);
|
||||
for (int i = 0; i < 64; ++i) CHECK(soft.renderFrame() == hard.renderFrame());
|
||||
}
|
||||
|
||||
static void testDrawnVelocityPitchCurveTransposesBothWays() {
|
||||
using instrument::engine::CurveDomain;
|
||||
SampleData s = rampSample(4096, 60);
|
||||
const double full = std::pow(2.0, kVelocityPitchRangeSemitones / 12.0);
|
||||
|
||||
// A curve pinned at +1 across the domain: every velocity transposes UP by the full scale.
|
||||
s.play.pitchVelocityCurve =
|
||||
VelocityCurve::fromPoints({{0.0, 1.0}, {127.0, 1.0}}, CurveDomain::Bipolar);
|
||||
Voice up;
|
||||
up.start(60, 100, s);
|
||||
CHECK(approx(static_cast<double>(up.renderFrame()), 0.0, 1e-9));
|
||||
CHECK(approx(static_cast<double>(up.renderFrame()), full, 1e-4));
|
||||
|
||||
// Pinned at -1: DOWN by the same scale — the half of the domain the old unipolar curve
|
||||
// could not express at all.
|
||||
s.play.pitchVelocityCurve =
|
||||
VelocityCurve::fromPoints({{0.0, -1.0}, {127.0, -1.0}}, CurveDomain::Bipolar);
|
||||
Voice down;
|
||||
down.start(60, 100, s);
|
||||
down.renderFrame();
|
||||
CHECK(approx(static_cast<double>(down.renderFrame()), 1.0 / full, 1e-4));
|
||||
|
||||
// And it follows the curve: a rising ramp gives a soft hit less transpose than a hard one.
|
||||
s.play.pitchVelocityCurve =
|
||||
VelocityCurve::fromPoints({{0.0, 0.0}, {127.0, 1.0}}, CurveDomain::Bipolar);
|
||||
Voice q;
|
||||
Voice f;
|
||||
q.start(60, 20, s);
|
||||
f.start(60, 120, s);
|
||||
q.renderFrame();
|
||||
f.renderFrame();
|
||||
const double quiet = static_cast<double>(q.renderFrame());
|
||||
const double loud = static_cast<double>(f.renderFrame());
|
||||
CHECK(quiet > 1.0);
|
||||
CHECK(loud > quiet);
|
||||
CHECK(loud < full); // velocity 120 is short of the +1 endpoint
|
||||
}
|
||||
|
||||
// MAJOR-1 regression: MONO+LEGATO with a TRIGGER zone RE-ATTACKS after the last key is up.
|
||||
// Trigger ignores note-off (Voice::release() is a no-op, so releasing_ never latches), so a
|
||||
// legato guard keyed on `active && !releasing` saw a ringing one-shot as "still held" and
|
||||
@@ -2801,6 +2861,8 @@ int main() {
|
||||
testRepitchObservedPeriod();
|
||||
testKeyTrackVarispeedObservedPeriod();
|
||||
testKeyTrackPreserveShiftCollapsesAtZero();
|
||||
testVelocityPitchIsExactlyOffByDefault();
|
||||
testDrawnVelocityPitchCurveTransposesBothWays();
|
||||
testAdsrShape();
|
||||
testAdsrReleaseBeforeSustain();
|
||||
testAdsrZeroAttackDecay();
|
||||
|
||||
Reference in New Issue
Block a user