S-VIEW-9: velocity->amp transfer curve (pure velocity_curve module + zones-payload v7 + Voice::start apply)

Default flat y=1 (R10-F1 Option A) replaces the linear velocity/127 at note-on — a deliberate, non-back-compat behavior change; v1-v6 blobs lift to the flat default.
This commit is contained in:
2026-07-27 14:18:01 -04:00
parent 203005e6cf
commit a1f9dcf6f8
10 changed files with 829 additions and 34 deletions
+43 -10
View File
@@ -422,6 +422,10 @@ static void testNoteOffReleasesNewestSameNote() {
sd.play.adsr = flatAdsr();
sd.play.adsr.releaseFrames = 10; // short but non-zero so voice stays active through release
Keymap km = Keymap::singleSampleChromatic(sd);
// A LINEAR velocity curve keeps the two velocities distinguishable (velocity/127). The default
// flat y=1 curve (S-VIEW-9 R10-F1) would render both at unity, collapsing the distinction this
// note-off-selection test relies on — so we opt this zone back to the linear response.
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
VoiceEngine eng(8, km);
std::size_t first = eng.noteOn(60, velOld); // older voice, lower gain
@@ -728,33 +732,60 @@ static void testStartAfterLoopEndWrapsIntoLoop() {
// velocity -> volume.
// ---------------------------------------------------------------------------
static void testVelocityToVolume() {
// S-VIEW-9 BEHAVIOR CHANGE (R10-F1 Option A): the DEFAULT velocity curve on a KeyZone is now flat
// y=1, so EVERY velocity plays at unity — NOT the old linear velocity/127. singleSampleChromatic
// builds a zone with the flat default, so the DC-1 sample renders 1.0 at any velocity.
static void testVelocityDefaultCurveIsFlatUnity() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0, flat default curve
for (int vel : {1, 64, 100, 127}) {
VoiceEngine eng(1, km);
eng.noteOn(60, vel);
std::vector<AudioSample> out;
eng.render(out, 1);
CHECK(approx(out[0], 1.0, 1e-4)); // flat y=1: any velocity -> unity gain
}
}
// A LINEAR curve on the zone reproduces the pre-r10 velocity/127 ramp exactly — proving the curve
// (not a hardcoded map) drives the gain, and that eval is applied at note-on.
static void testVelocityLinearCurveReproducesRamp() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
// Full velocity -> full gain; half velocity -> ~half gain (flat envelope so the
// rendered value is exactly velocity/127 on a DC-1 sample).
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
{
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
eng.render(out, 1);
std::vector<AudioSample> out; eng.render(out, 1);
CHECK(approx(out[0], 1.0, 1e-4));
}
{
VoiceEngine eng(1, km);
eng.noteOn(60, 64);
std::vector<AudioSample> out;
eng.render(out, 1);
std::vector<AudioSample> out; eng.render(out, 1);
CHECK(approx(out[0], 64.0 / 127.0, 1e-4));
}
{
VoiceEngine eng(1, km);
eng.noteOn(60, 1);
std::vector<AudioSample> out;
eng.render(out, 1);
std::vector<AudioSample> out; eng.render(out, 1);
CHECK(approx(out[0], 1.0 / 127.0, 1e-4));
}
}
// A shaped curve (a single interior knot) drives the gain through eval — a mid velocity reads the
// curve's shaped value, not the linear one. Proves the whole curve, not just the endpoints, applies.
static void testVelocityShapedCurveDrivesGain() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
vst::VelocityCurve curve = vst::VelocityCurve::linear();
curve.addPoint(64.0, 0.9); // pull the mid-velocity response UP to 0.9
km.zones[0].velocityCurve = curve;
VoiceEngine eng(1, km);
eng.noteOn(60, 64);
std::vector<AudioSample> out; eng.render(out, 1);
// At exactly velocity 64 the curve passes through the knot -> gain 0.9 (well above the linear
// 64/127 ~= 0.504), so the rendered DC value is the shaped 0.9.
CHECK(approx(out[0], 0.9, 1e-4));
}
// Two voices summed: polyphony mixes additively.
static void testPolyphonyMixesAdditively() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
@@ -1348,7 +1379,9 @@ int main() {
testStartFrameOutOfRangeClampsToZero();
testStartFrameWithLoop();
testStartAfterLoopEndWrapsIntoLoop();
testVelocityToVolume();
testVelocityDefaultCurveIsFlatUnity();
testVelocityLinearCurveReproducesRamp();
testVelocityShapedCurveDrivesGain();
testPolyphonyMixesAdditively();
testChannelCount();
testStereoRenderKeepsChannelsDistinct();