Retire the zone system: one capture = one parameter set, and re-seam the engine and Sample face into bands

This commit is contained in:
2026-07-30 07:15:54 -04:00
parent a689fb75eb
commit 8d4ccbf841
61 changed files with 5416 additions and 8008 deletions
+177 -187
View File
@@ -17,7 +17,7 @@
// by the CMake target linking neither SDK — this file includes only sampler_core.h +
// the standard library, which is itself the compile-time proof.
#include "../src/core/instrument/engine/sampler_core.h"
#include "../src/core/instrument/engine/voice_engine.h"
#include <algorithm>
#include <cmath>
@@ -69,49 +69,42 @@ static AdsrParams flatAdsr() {
}
// ---------------------------------------------------------------------------
// 6. Keymap resolution.
// 6. Full-keyboard response over the one loaded capture.
// ---------------------------------------------------------------------------
static void testChromaticSingleRoot() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60));
CHECK(km.zones.size() == 1);
// Every note in 0..127 resolves to the single zone.
static void testEveryKeyPlaysTheLoadedCapture() {
// No key range survives: the loaded capture answers every note in 0..127, repitched
// from its root. Each note-on must take a real voice.
SampleData km = dcSample(100, 60);
VoiceEngine eng(128, km);
for (int n = 0; n <= 127; ++n) {
ZoneResolution r = km.resolve(n, 100);
CHECK(r.matched);
CHECK(r.zoneIndex == 0);
CHECK(eng.noteOn(n, 100) != VoiceEngine::kNoVoice);
}
CHECK(eng.activeVoiceCount() == 128);
}
static void testZonedRangesBoundaries() {
Keymap km;
km.samples.push_back(dcSample(100, 48)); // low sample
km.samples.push_back(dcSample(100, 72)); // high sample
// Two adjacent zones: [36,59] and [60,83]. Boundary notes 59/60 must land in the
// correct zone; a first-match order test would catch an off-by-one.
km.zones.push_back(KeyZone{36, 59, 48, 0});
km.zones.push_back(KeyZone{60, 83, 72, 1});
static void testUnplayableCaptureRefusesEveryNote() {
// Nothing decoded -> the defined no-play at every key, in both voice modes, rather
// than a voice started on an empty read span.
SampleData empty; // no frames
VoiceEngine poly(4, empty);
CHECK(poly.noteOn(60, 100) == VoiceEngine::kNoVoice);
CHECK(poly.noteOn(0, 100) == VoiceEngine::kNoVoice);
CHECK(poly.activeVoiceCount() == 0);
CHECK(km.resolve(36, 100).matched);
CHECK(km.resolve(36, 100).zoneIndex == 0);
CHECK(km.resolve(59, 100).zoneIndex == 0); // last note of zone 0
CHECK(km.resolve(60, 100).zoneIndex == 1); // first note of zone 1
CHECK(km.resolve(83, 100).zoneIndex == 1); // last note of zone 1
// Out of every zone -> defined no-play (not a match, not zone 0).
CHECK(!km.resolve(35, 100).matched);
CHECK(!km.resolve(84, 100).matched);
CHECK(!km.resolve(127, 100).matched);
VoiceEngine mono(4, empty, 0, 0, VoiceMode::Mono);
CHECK(mono.noteOn(60, 100) == VoiceEngine::kNoVoice);
CHECK(mono.activeVoiceCount() == 0);
}
static void testFirstMatchOnOverlap() {
// Overlapping zones: the earlier zone wins (documented deterministic rule).
Keymap km;
km.samples.push_back(dcSample(10, 60));
km.samples.push_back(dcSample(10, 60));
km.zones.push_back(KeyZone{0, 127, 60, 0}); // catch-all first
km.zones.push_back(KeyZone{60, 60, 60, 1}); // shadowed by the catch-all
CHECK(km.resolve(60, 100).zoneIndex == 0);
static void testOutOfRangeNotesAreRefusedInMono() {
// The mono held stack keys notes as uint8, so an out-of-range note must be rejected
// BEFORE it can alias onto a real held note.
SampleData km = dcSample(100, 60);
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono);
CHECK(eng.noteOn(-1, 100) == VoiceEngine::kNoVoice);
CHECK(eng.noteOn(128, 100) == VoiceEngine::kNoVoice);
CHECK(eng.activeVoiceCount() == 0);
}
// ---------------------------------------------------------------------------
@@ -178,7 +171,7 @@ static void testRepitchObservedPeriod() {
// Unity: played at root, observed period ~= native.
{
Keymap km = Keymap::singleSampleChromatic(sineSample(frames, cycles, 60));
SampleData km = (sineSample(frames, cycles, 60));
VoiceEngine eng(4, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -188,7 +181,7 @@ static void testRepitchObservedPeriod() {
}
// +1 octave: advances 2x, observed period halves.
{
Keymap km = Keymap::singleSampleChromatic(sineSample(frames, cycles, 60));
SampleData km = (sineSample(frames, cycles, 60));
VoiceEngine eng(4, km);
eng.noteOn(72, 127);
std::vector<AudioSample> out;
@@ -198,7 +191,7 @@ static void testRepitchObservedPeriod() {
}
// -1 octave: advances 0.5x, observed period doubles.
{
Keymap km = Keymap::singleSampleChromatic(sineSample(frames, cycles, 60));
SampleData km = (sineSample(frames, cycles, 60));
VoiceEngine eng(4, km);
eng.noteOn(48, 127);
std::vector<AudioSample> out;
@@ -217,9 +210,9 @@ static void testKeyTrackVarispeedObservedPeriod() {
auto periodAt = [&](int note, double keyTrack) -> double {
SampleData s = sineSample(frames, cycles, 60);
s.play.pitchEngine = PitchEngine::Varispeed;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
// The single zone spans the keyboard from root 60; stamp the key-track scalar on it.
km.zones[0].keyTrack = keyTrack;
km.keyTrack = keyTrack;
VoiceEngine eng(4, km);
eng.noteOn(note, 127);
std::vector<AudioSample> out;
@@ -247,8 +240,8 @@ static void testKeyTrackPreserveShiftCollapsesAtZero() {
auto renderPreserve = [&](int note, double keyTrack) -> std::vector<AudioSample> {
SampleData s = sineSample(frames, cycles, 60);
s.play.pitchEngine = PitchEngine::Preserve; // Gate, no loop -> runs to sample end
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].keyTrack = keyTrack;
SampleData km = (std::move(s));
km.keyTrack = keyTrack;
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(window));
eng.noteOn(note, 127);
std::vector<AudioSample> out;
@@ -375,7 +368,7 @@ static void testAdsrZeroAttackDecay() {
// ---------------------------------------------------------------------------
static void testPolyphonicAllocation() {
Keymap km = Keymap::singleSampleChromatic(dcSample(1000, 60));
SampleData km = (dcSample(1000, 60));
VoiceEngine eng(8, km);
// Four simultaneous notes -> four active voices, each on a distinct voice.
@@ -423,11 +416,11 @@ static void testNoteOffReleasesNewestSameNote() {
SampleData sd = dcSample(100000, 60);
sd.play.adsr = flatAdsr();
sd.play.adsr.releaseFrames = 10; // short but non-zero so voice stays active through release
Keymap km = Keymap::singleSampleChromatic(sd);
SampleData km = (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 = VelocityCurve::linear();
km.velocityCurve = VelocityCurve::linear();
VoiceEngine eng(8, km);
std::size_t first = eng.noteOn(60, velOld); // older voice, lower gain
@@ -463,17 +456,6 @@ static void testNoteOffReleasesNewestSameNote() {
CHECK(eng.activeVoiceCount() == 0);
}
static void testOutOfZoneNoteConsumesNoVoice() {
Keymap km;
km.samples.push_back(dcSample(100, 60));
km.zones.push_back(KeyZone{60, 72, 60, 0});
VoiceEngine eng(4, km);
std::size_t v = eng.noteOn(30, 100); // below the only zone
CHECK(v == VoiceEngine::kNoVoice);
CHECK(eng.activeVoiceCount() == 0); // no voice consumed
}
// ---------------------------------------------------------------------------
// 2. Voice stealing at the bound.
// ---------------------------------------------------------------------------
@@ -484,7 +466,7 @@ static void testStealsReleasingVoiceFirst() {
SampleData s = dcSample(100000, 60);
s.play.adsr = flatAdsr();
s.play.adsr.releaseFrames = 100000; // long release so a released voice stays "active"
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km);
std::size_t vA = eng.noteOn(60, 100); // startOrder 1
@@ -509,7 +491,7 @@ static void testStealsOldestWhenNoneReleasing() {
SampleData s = dcSample(100000, 60);
s.play.adsr = flatAdsr();
s.play.adsr.releaseFrames = 100000;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km);
std::size_t vA = eng.noteOn(60, 100); // startOrder 1 (oldest)
@@ -547,7 +529,7 @@ static void testLoopSustainSeamless() {
s.loop.start = 20;
s.loop.end = 40;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio, full velocity
@@ -569,7 +551,7 @@ static void testZeroLengthLoopGoesSilent() {
s.loop.hasLoop = true;
s.loop.start = 25;
s.loop.end = 25; // zero length
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
@@ -593,7 +575,7 @@ static void testSingleFrameLoop() {
s.loop.start = 5;
s.loop.end = 6; // single-frame loop: [5, 6)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio, full velocity
@@ -612,7 +594,7 @@ static void testAbsentLoopGoesSilent() {
// No loop at all: held note runs off the end and goes idle (same as zero-length).
SampleData s = dcSample(50, 60);
// s.loop.hasLoop stays false.
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -633,7 +615,7 @@ static void testStartFrameOffsetsInitialRead() {
for (int i = 0; i < 100; ++i) s.frames[i] = static_cast<float>(i) * 0.01f;
s.rootNote = 60;
s.startFrame = 30;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio, full velocity, flat gain
std::vector<AudioSample> out;
@@ -649,7 +631,7 @@ static void testStartFrameZeroIsUnchanged() {
s.frames.resize(20);
for (int i = 0; i < 20; ++i) s.frames[i] = static_cast<float>(i) * 0.05f;
s.rootNote = 60; // startFrame stays 0
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -662,7 +644,7 @@ static void testStartFrameOutOfRangeClampsToZero() {
// out-of-bounds read that would start the voice already exhausted.
SampleData s = dcSample(10, 60); // 10 frames of 1.0
s.startFrame = 10; // == frameCount: out of range
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -683,7 +665,7 @@ static void testStartFrameWithLoop() {
s.loop.hasLoop = true;
s.loop.start = 20;
s.loop.end = 40;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -712,7 +694,7 @@ static void testStartAfterLoopEndWrapsIntoLoop() {
s.loop.start = 20;
s.loop.end = 40;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio, full velocity
@@ -734,11 +716,11 @@ static void testStartAfterLoopEndWrapsIntoLoop() {
// velocity -> volume.
// ---------------------------------------------------------------------------
// S-VIEW-9 BEHAVIOR CHANGE (R10-F1 Option A): the DEFAULT velocity curve on a KeyZone is now flat
// S-VIEW-9 BEHAVIOR CHANGE (R10-F1 Option A): the DEFAULT velocity curve 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
SampleData km = (dcSample(100, 60)); // DC 1.0, flat default curve
for (int vel : {1, 64, 100, 127}) {
VoiceEngine eng(1, km);
eng.noteOn(60, vel);
@@ -751,8 +733,8 @@ static void testVelocityDefaultCurveIsFlatUnity() {
// 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
km.zones[0].velocityCurve = VelocityCurve::linear();
SampleData km = (dcSample(100, 60)); // DC 1.0
km.velocityCurve = VelocityCurve::linear();
{
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
@@ -776,10 +758,10 @@ static void testVelocityLinearCurveReproducesRamp() {
// 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
SampleData km = (dcSample(100, 60)); // DC 1.0
VelocityCurve curve = VelocityCurve::linear();
curve.addPoint(64.0, 0.9); // pull the mid-velocity response UP to 0.9
km.zones[0].velocityCurve = curve;
km.velocityCurve = curve;
VoiceEngine eng(1, km);
eng.noteOn(60, 64);
std::vector<AudioSample> out; eng.render(out, 1);
@@ -790,7 +772,7 @@ static void testVelocityShapedCurveDrivesGain() {
// Two voices summed: polyphony mixes additively.
static void testPolyphonyMixesAdditively() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
SampleData km = (dcSample(100, 60)); // DC 1.0
VoiceEngine eng(4, km);
eng.noteOn(60, 127); // gain 1.0
eng.noteOn(60, 127); // gain 1.0 (second voice, same note)
@@ -826,7 +808,7 @@ static void testChannelCount() {
static void testStereoRenderKeepsChannelsDistinct() {
// A stereo sample (L=1.0, R=-1.0) rendered stereo must emit L and R distinctly, each
// scaled by velocity (full here). If the engine copied L to both channels the R check fails.
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 1.0f, -1.0f, 60));
SampleData km = (stereoDcSample(100, 1.0f, -1.0f, 60));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
@@ -841,7 +823,7 @@ static void testStereoRenderKeepsChannelsDistinct() {
static void testMonoSamplePlaysDualMonoInStereo() {
// A MONO sample rendered through the stereo path plays dual-mono: both channels equal
// (centered), not silent on the right. The cross-mode "mono source in stereo mode" case.
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // mono, DC 1.0
SampleData km = (dcSample(100, 60)); // mono, DC 1.0
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> left(8, 0.f), right(8, 0.f);
@@ -862,7 +844,7 @@ static void testDualMonoStereoSampleRendersCentered() {
SampleData s = sineSample(600, 12.0, 60);
s.framesR = s.frames; // dual-mono: identical channels
s.play.pitchEngine = engine;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, /*preserveWindowFrames=*/128);
eng.noteOn(note, 127);
std::vector<AudioSample> left(256, 0.f), right(256, 0.f);
@@ -884,7 +866,7 @@ static void testDualMonoStereoSampleRendersCentered() {
static void testMonoRenderUnchangedByStereoData() {
// Regression: the mono render path (renderFrame) reads channel 0 ONLY and is byte-identical
// whether or not a second channel is present. A stereo sample rendered mono == its L channel.
Keymap kmS = Keymap::singleSampleChromatic(stereoDcSample(100, 0.75f, -0.25f, 60));
SampleData kmS = (stereoDcSample(100, 0.75f, -0.25f, 60));
VoiceEngine engS(1, kmS);
engS.noteOn(60, 127);
std::vector<AudioSample> mono;
@@ -909,7 +891,7 @@ static void testStereoRenderAdvancesLikeMonoRepitch() {
s.framesR[i] = v;
}
s.rootNote = 60;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(4, km);
eng.noteOn(72, 127); // +1 octave
std::vector<AudioSample> left(frames / 2, 0.f), right(frames / 2, 0.f);
@@ -920,7 +902,7 @@ static void testStereoRenderAdvancesLikeMonoRepitch() {
static void testStereoRenderSumsVoicesPerChannel() {
// Two voices on a stereo sample sum PER CHANNEL (additive polyphony holds in stereo).
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 0.5f, -0.5f, 60));
SampleData km = (stereoDcSample(100, 0.5f, -0.5f, 60));
VoiceEngine eng(4, km);
eng.noteOn(60, 127);
eng.noteOn(60, 127); // second voice, same note
@@ -931,7 +913,7 @@ static void testStereoRenderSumsVoicesPerChannel() {
}
static void testStereoRenderNullBufferIsNoOp() {
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 1.0f, -1.0f, 60));
SampleData km = (stereoDcSample(100, 1.0f, -1.0f, 60));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> buf(4, 0.f);
@@ -960,7 +942,7 @@ static void testStereoStartFrameLoopShareOneReadHead() {
s.loop.start = 20;
s.loop.end = 30; // loop [20,30): frames 20..29
CHECK(s.channelCount() == 2);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio, full velocity, flat gain
@@ -1058,7 +1040,7 @@ static SampleData triggerSample(std::size_t frames, double lengthFraction,
// --- Trigger %-length frame math: plays exactly round(frac*(frames-start)) frames then frees. ---
static void testTriggerLengthFractionFrames() {
// 200-frame sample, start 0, 50% length -> plays 100 frames then the voice frees.
Keymap km = Keymap::singleSampleChromatic(triggerSample(200, 0.5, 0, 0));
SampleData km = (triggerSample(200, 0.5, 0, 0));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity ratio
std::vector<AudioSample> out;
@@ -1072,7 +1054,7 @@ static void testTriggerLengthFractionFrames() {
// --- Trigger start point: %-length measured from the start offset. ---
static void testTriggerLengthWithStart() {
// 200 frames, start 40, 50% -> span 160, play 80 frames (frames 40..119), then free.
Keymap km = Keymap::singleSampleChromatic(triggerSample(200, 0.5, 0, 0, /*start=*/40));
SampleData km = (triggerSample(200, 0.5, 0, 0, /*start=*/40));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1086,7 +1068,7 @@ static void testTriggerLengthWithStart() {
static void testTriggerFadeShape() {
// 100 frames, 100% length, fadeIn 20, fadeOut 20. Head ramps 0->1, tail ramps 1->0, unity
// between. Equal-power: sin/cos ramps, monotonic, endpoints ~0 and ~1.
Keymap km = Keymap::singleSampleChromatic(triggerSample(100, 1.0, 20, 20));
SampleData km = (triggerSample(100, 1.0, 20, 20));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1106,7 +1088,7 @@ static void testTriggerFadeShape() {
static void testTriggerEdgeCases() {
// %=0: zero play length -> voice frees at once, no sound.
{
Keymap km = Keymap::singleSampleChromatic(triggerSample(100, 0.0, 5, 5));
SampleData km = (triggerSample(100, 0.0, 5, 5));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1117,7 +1099,7 @@ static void testTriggerEdgeCases() {
// Fades that sum beyond the play length are clamped (no crash, no negative gain, amp in [0,1]).
{
// 40 frames, 100% -> playLen 40; fadeIn 30 + fadeOut 30 = 60 > 40 -> clamped.
Keymap km = Keymap::singleSampleChromatic(triggerSample(40, 1.0, 30, 30));
SampleData km = (triggerSample(40, 1.0, 30, 30));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1127,7 +1109,7 @@ static void testTriggerEdgeCases() {
}
// %=100 plays the full post-start span.
{
Keymap km = Keymap::singleSampleChromatic(triggerSample(60, 1.0, 0, 0));
SampleData km = (triggerSample(60, 1.0, 0, 0));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1139,7 +1121,7 @@ static void testTriggerEdgeCases() {
// --- Trigger ignores note-off (S15): the one-shot plays through regardless. ---
static void testTriggerIgnoresNoteOff() {
Keymap km = Keymap::singleSampleChromatic(triggerSample(200, 0.5, 0, 0));
SampleData km = (triggerSample(200, 0.5, 0, 0));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1189,7 +1171,7 @@ static void testPreserveDurationInvariance() {
const std::size_t window = 512; // pre-size the shifters
auto lengthAt = [&](int note) -> std::size_t {
Keymap km = Keymap::singleSampleChromatic(preserveTriggerSample(frames, 1.0));
SampleData km = (preserveTriggerSample(frames, 1.0));
VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/static_cast<std::int64_t>(window));
eng.noteOn(note, 127);
return soundingLength(eng, 4000);
@@ -1216,7 +1198,7 @@ static void testVarispeedStillCouplesDuration() {
s.play.playMode = PlayMode::Trigger;
s.play.pitchEngine = PitchEngine::Varispeed;
s.play.trigger.lengthFraction = 1.0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(note, 127);
return soundingLength(eng, 4000);
@@ -1241,7 +1223,7 @@ static void testPitchEnvOffBitIdentical() {
s.play.pitchEnv.attackFrames = 0;
s.play.pitchEnv.decayFrames = 500;
}
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(67, 127); // a transposed note so ratio != 1 (exercises the ratio path)
std::vector<AudioSample> out;
@@ -1269,7 +1251,7 @@ static void testPitchEnvOnBendsVarispeed() {
s.play.pitchEnv.attackFrames = 0; // start at the peak
s.play.pitchEnv.decayFrames = 3000; // glide to base over 3000 frames
s.play.pitchEnv.peakSemitones = 12.0; // +1 octave at t=0
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // at root -> base ratio 1.0; the env supplies the bend
std::vector<AudioSample> out;
@@ -1304,7 +1286,7 @@ static void testPreserveGateStereoLoopComposes() {
s.play.playMode = PlayMode::Gate;
s.play.pitchEngine = PitchEngine::Preserve;
CHECK(s.channelCount() == 2);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 512);
eng.noteOn(67, 127); // transposed up a fifth under Preserve (duration held)
std::vector<AudioSample> left(2000, 0.f), right(2000, 0.f);
@@ -1337,7 +1319,7 @@ static void testPreserveGateStereoLoopComposes() {
static void testPreserveVoiceCap() {
SampleData s = dcSample(2000, 60);
s.play.pitchEngine = PitchEngine::Preserve; // held (Gate, no loop -> runs long enough)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
// 8 voices total, Preserve cap of 2.
VoiceEngine eng(8, km, /*preserveCap=*/2, /*window=*/256);
CHECK(eng.noteOn(62, 127) != VoiceEngine::kNoVoice); // 1st Preserve voice
@@ -1362,7 +1344,7 @@ static void testPreserveUnityEngineVoiceSpeaksImmediately() {
SampleData s = dcSample(4000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr(); // isolate the shifter onset from the amp attack
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/512);
eng.noteOn(note, 127);
std::vector<AudioSample> out;
@@ -1394,7 +1376,7 @@ static void testPreserveTransposedVoiceSpeaksImmediately() {
SampleData s = dcSample(4000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr(); // isolate the shifter onset from the amp attack
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/512);
eng.noteOn(62, 127); // +2 semitones: a real shift, NOT demoted
std::vector<AudioSample> out;
@@ -1413,7 +1395,7 @@ static void testPreserveTransposedVoiceSpeaksImmediately() {
static void testPreserveUnityVoiceCountsTowardCap() {
SampleData s = dcSample(2000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(8, km, /*preserveCap=*/2, /*window=*/256);
CHECK(eng.noteOn(60, 127) != VoiceEngine::kNoVoice); // root: a genuine Preserve voice now
CHECK(eng.noteOn(62, 127) != VoiceEngine::kNoVoice); // 2nd (at the cap)
@@ -1429,8 +1411,8 @@ static void testVelocityCurveAppliesUnderPreserve() {
auto steadyLevelAt = [&](int vel) -> double {
SampleData s = dcSample(4000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = VelocityCurve::linear();
SampleData km = (std::move(s));
km.velocityCurve = VelocityCurve::linear();
VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/256);
eng.noteOn(62, vel); // transposed: the genuine shifter path (not the unity demotion)
std::vector<AudioSample> out;
@@ -1460,7 +1442,7 @@ static void testPerZoneAdsrReachesVoiceEnvelope() {
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
s.play.pitchEngine = PitchEngine::Varispeed; // isolate from pitch engine machinery
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127); // unity pitch, full velocity -> gain 1.0
std::vector<AudioSample> out;
@@ -1483,7 +1465,7 @@ static void testZeroAdsrIsInstantSustain() {
// Default AdsrParams{}: all zeros, sustainLevel = 1.0 (struct default). No attack ramp.
s.play.adsr = AdsrParams{};
s.play.pitchEngine = PitchEngine::Varispeed;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1507,17 +1489,20 @@ static SampleData dcLevelSample(std::size_t frames, float level, int rootNote) {
return s;
}
// Two-zone keymap with DISTINCT DC levels (0.25 / 0.75) so the mono tests can read which zone
// holds the voice off the rendered value: zone A = notes [40,59] root 50 -> 0.25; zone B =
// notes [60,80] root 70 -> 0.75.
static Keymap twoLevelKeymap() {
Keymap km;
km.samples.push_back(dcLevelSample(200000, 0.25f, 50));
km.samples.push_back(dcLevelSample(200000, 0.75f, 70));
KeyZone a; a.lowNote = 40; a.highNote = 59; a.rootNote = 50; a.sampleIndex = 0;
KeyZone b; b.lowNote = 60; b.highNote = 80; b.rootNote = 70; b.sampleIndex = 1;
km.zones.push_back(a);
km.zones.push_back(b);
// The mono tests need to read WHICH NOTE holds the single voice off the rendered value, and
// a DC sample makes pitch inaudible. Velocity is the discriminator: a DC 1.0 capture with a
// curve pinned through two probe velocities renders 0.25 for a kVelLow strike and 0.75 for a
// kVelHigh one (the Hermite spline passes exactly through its control points). Each test
// then presses note 50 soft and note 70 hard, so the level names the sounding note.
static constexpr int kVelLow = 32;
static constexpr int kVelHigh = 96;
static SampleData twoLevelSample() {
SampleData km = dcLevelSample(200000, 1.0f, 60);
km.velocityCurve = VelocityCurve::fromPoints({{0.0, 0.0},
{static_cast<double>(kVelLow), 0.25},
{static_cast<double>(kVelHigh), 0.75},
{127.0, 1.0}});
return km;
}
@@ -1532,11 +1517,11 @@ static double probeFrame(VoiceEngine& eng) {
// back to the most-recent still-held note; releasing the last note gates off. Also: mono uses
// ONE voice regardless of the pool size.
static void testMonoLastNotePriorityAndFallback() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
CHECK(eng.noteOn(50, 127) == 0); // zone A sounds
CHECK(eng.noteOn(50, kVelLow) == 0); // zone A sounds
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
CHECK(eng.noteOn(70, 127) == 0); // zone B TAKES the voice (last-note priority)
CHECK(eng.noteOn(70, kVelHigh) == 0); // zone B TAKES the voice (last-note priority)
CHECK(eng.activeVoiceCount() == 1); // mono: one voice even with 4 in the pool
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
eng.noteOff(70); // top released -> FALLBACK to still-held 50
@@ -1549,10 +1534,10 @@ static void testMonoLastNotePriorityAndFallback() {
// Releasing a LOWER held note (not the sounding one) changes nothing audible; the released
// note also leaves the stack, so the final note-off truly empties it.
static void testMonoReleaseOfLowerHeldNoteIsInaudible() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(50, 127);
eng.noteOn(70, 127); // 70 sounds, 50 held beneath
eng.noteOn(50, kVelLow);
eng.noteOn(70, kVelHigh); // 70 sounds, 50 held beneath
eng.noteOff(50); // releasing the buried note: inaudible
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
eng.noteOff(70); // 50 already left the stack -> silence, no fallback
@@ -1562,11 +1547,11 @@ static void testMonoReleaseOfLowerHeldNoteIsInaudible() {
// Re-pressing a HELD note moves it to the top of the stack (it sounds again), and the note
// beneath becomes the fallback.
static void testMonoRepressHeldNoteMovesToTop() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(50, 127);
eng.noteOn(70, 127);
CHECK(eng.noteOn(50, 127) == 0); // re-press while held: back on top
eng.noteOn(50, kVelLow);
eng.noteOn(70, kVelHigh);
CHECK(eng.noteOn(50, kVelLow) == 0); // re-press while held: back on top
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
eng.noteOff(50); // falls back to 70 (now the most recent held)
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
@@ -1578,8 +1563,8 @@ static void testMonoRepressHeldNoteMovesToTop() {
// held note on the stack), not the departing note's.
static void testMonoRetriggerFallbackUsesOriginalVelocity() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = VelocityCurve::linear(); // gain = velocity/127
SampleData km = (std::move(s));
km.velocityCurve = VelocityCurve::linear(); // gain = velocity/127
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(60, 32); // soft first note
CHECK(approx(probeFrame(eng), 32.0 / 127.0, 1e-4));
@@ -1589,16 +1574,17 @@ static void testMonoRetriggerFallbackUsesOriginalVelocity() {
CHECK(approx(probeFrame(eng), 32.0 / 127.0, 1e-4));
}
// An OUT-OF-ZONE note in mono is a defined no-play: it consumes nothing, never joins the
// stack (so it can never take the voice back on a fallback), and its note-off is inert.
static void testMonoOutOfZoneNeverJoinsStack() {
Keymap km = twoLevelKeymap(); // zones cover [40,59] + [60,80] only
// An OUT-OF-RANGE note in mono is a defined no-play: it consumes nothing, never joins the
// stack (so it can never take the voice back on a fallback), and its note-off is inert. The
// stack keys notes as uint8, so an unguarded 200 would alias onto 72 and corrupt it.
static void testMonoOutOfRangeNeverJoinsStack() {
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(70, 127);
CHECK(eng.noteOn(20, 127) == VoiceEngine::kNoVoice); // out of every zone
eng.noteOn(70, kVelHigh);
CHECK(eng.noteOn(200, 127) == VoiceEngine::kNoVoice); // past the MIDI range
CHECK(eng.activeVoiceCount() == 1);
CHECK(approx(probeFrame(eng), 0.75, 1e-6)); // 70 undisturbed
eng.noteOff(20); // inert
eng.noteOff(200); // inert
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
eng.noteOff(70);
CHECK(approx(probeFrame(eng), 0.0, 1e-9));
@@ -1609,7 +1595,7 @@ static void testMonoOutOfZoneNeverJoinsStack() {
static void testMonoRetriggerRestartsEnvelope() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
s.play.adsr.attackFrames = 100; // slow linear attack: level at frame i = i/100
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1623,7 +1609,7 @@ static void testMonoRetriggerRestartsEnvelope() {
static void testMonoLegatoContinuesEnvelope() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
s.play.adsr.attackFrames = 100;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1645,8 +1631,8 @@ static void testMonoLegatoRetunesWithoutReadRestart() {
}
s.rootNote = 60;
s.play.adsr = flatAdsr();
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = VelocityCurve::linear();
SampleData km = (std::move(s));
km.velocityCurve = VelocityCurve::linear();
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127); // unity: read advances 1/frame, full gain
std::vector<AudioSample> out;
@@ -1657,16 +1643,21 @@ static void testMonoLegatoRetunesWithoutReadRestart() {
CHECK(approx(probeFrame(eng), 12.0, 1e-3)); // and now advances at ratio 2 (the new pitch)
}
// LEGATO applies only to a SAME-SAMPLE takeover: crossing into a zone playing a DIFFERENT
// sample restarts the voice (one read head cannot glide between two PCM streams).
static void testMonoLegatoCrossSampleRestarts() {
Keymap km = twoLevelKeymap();
km.samples[1].play.adsr.attackFrames = 100; // zone B has a slow attack to expose a restart
// LEGATO takeover ALWAYS glides now: with one loaded capture there is no second PCM stream
// to cross into, so the read head never has to restart mid-phrase. (The retired
// cross-sample-restart branch was the multi-zone case.)
static void testMonoLegatoAlwaysGlidesWithinThePhrase() {
SampleData km = twoLevelSample();
km.play.adsr.attackFrames = 100; // a slow attack would expose any restart
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(50, 127); // zone A (flat env): 0.25 at once
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
eng.noteOn(70, 127); // cross-sample: RESTART (attack from 0), no retune
CHECK(approx(probeFrame(eng), 0.0, 1e-6)); // zone B's fresh attack origin — not 0.25 held over
eng.noteOn(50, kVelLow);
std::vector<AudioSample> out;
eng.render(out, 50); // mid-attack: level ~0.49 * the 0.25 vel gain
CHECK(approx(out[49], 0.49 * 0.25, 1e-6));
eng.noteOn(70, kVelHigh); // takeover: envelope KEEPS running, no re-attack
// Frame 50 of the SAME attack ramp, still at the FIRST strike's velocity gain (a legato
// phrase is one gesture, one strike) — NOT 0.0 (a restart) and NOT 0.75 (a re-strike).
CHECK(approx(probeFrame(eng), 0.50 * 0.25, 1e-6));
}
// LEGATO after the last note was RELEASED re-attacks: a releasing voice's note has left the
@@ -1675,7 +1666,7 @@ static void testMonoLegatoAfterReleaseReattacks() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
s.play.adsr.attackFrames = 100;
s.play.adsr.releaseFrames = 1000; // long release keeps the voice audibly ringing
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1692,7 +1683,7 @@ static void testMonoLegatoAfterReleaseReattacks() {
static void testMonoIgnoresPreserveCap() {
SampleData s = dcSample(4000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(4, km, /*preserveCap=*/1, /*window=*/256,
VoiceMode::Mono, MonoTrigger::Retrigger);
CHECK(eng.noteOn(62, 127) == 0); // 1st Preserve note: at the cap
@@ -1719,7 +1710,7 @@ static SampleData rampSample(std::size_t frames, int rootNote) {
static void testMonoLegatoTriggerReattacksAfterKeyUp() {
SampleData s = rampSample(200000, 60);
s.play.playMode = PlayMode::Trigger; // default TriggerParams: full length, no fades
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127); // unity: read advances 1/frame
std::vector<AudioSample> out;
@@ -1739,7 +1730,7 @@ static void testMonoLegatoTriggerReattacksAfterKeyUp() {
static void testMonoLegatoTriggerHeldKeyStillRetunes() {
SampleData s = rampSample(200000, 60);
s.play.playMode = PlayMode::Trigger;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
@@ -1751,7 +1742,7 @@ static void testMonoLegatoTriggerHeldKeyStillRetunes() {
// MAJOR-2: allNotesOff releases every gated poly voice (flat release -> instant silence).
static void testAllNotesOffReleasesPolyVoices() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(4, km);
eng.noteOn(60, 127);
eng.noteOn(62, 127);
@@ -1765,16 +1756,16 @@ static void testAllNotesOffReleasesPolyVoices() {
// MAJOR-2, the STUCK-NOTE path: allNotesOff clears the mono held stack, so a phantom entry
// (simulating a LOST note-off) can never be resurrected by the fallback afterwards.
static void testAllNotesOffClearsMonoHeldStack() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(50, 127); // 50's note-off will never arrive (phantom)
eng.noteOn(70, 127); // 70 sounds, phantom 50 buried on the stack
eng.noteOn(50, kVelLow); // 50's note-off will never arrive (phantom)
eng.noteOn(70, kVelHigh); // 70 sounds, phantom 50 buried on the stack
eng.allNotesOff(); // PANIC
CHECK(approx(probeFrame(eng), 0.0, 1e-9));
CHECK(eng.activeVoiceCount() == 0);
// The stack is empty: a fresh press + release gates off cleanly, with NO fallback
// restart of the phantom (pre-fix, noteOff(70) here re-struck 50 -> 0.25 forever).
eng.noteOn(70, 127);
eng.noteOn(70, kVelHigh);
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
eng.noteOff(70);
CHECK(approx(probeFrame(eng), 0.0, 1e-9));
@@ -1790,7 +1781,7 @@ static void testAllSoundsOffStopsTriggerOneShot() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
s.play.playMode = PlayMode::Trigger;
s.play.trigger.lengthFraction = 1.0; // full length — would ring for 200000 frames
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
CHECK(eng.activeVoiceCount() == 1);
@@ -1812,7 +1803,7 @@ static void testAllSoundsOffStopsTriggerOneShot() {
static void testAllNotesOffStillReleasesGateVoices() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
// Default Gate mode, instant release (releaseFrames 0).
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(4, km);
eng.noteOn(60, 127);
eng.noteOn(62, 127);
@@ -1829,7 +1820,7 @@ static void testAllNotesOffStillReleasesGateVoices() {
// rather than retune. This is the correct fresh-phrase behavior documented in the comment.
static void testMonoLegatoSameNoteRepressReattacks() {
SampleData s = rampSample(200000, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127); // first press; read starts at 0
std::vector<AudioSample> out;
@@ -1845,7 +1836,7 @@ static void testMonoLegatoSameNoteRepressReattacks() {
// losing its fallback. Note-ons out of [0,127] are a defined no-play.
static void testMonoOutOfRangeNotesRejected() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
CHECK(eng.noteOn(128, 127) == VoiceEngine::kNoVoice);
CHECK(eng.noteOn(-1, 127) == VoiceEngine::kNoVoice);
@@ -1865,7 +1856,7 @@ static void testMonoOutOfRangeNotesRejected() {
// notes and steals (never grows) on the N+1th; 0 clamps to the documented 1-voice degenerate.
static void testVoiceCountBoundsPolyphony() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine e3(3, km);
CHECK(e3.maxVoices() == 3);
e3.noteOn(60, 127);
@@ -1894,7 +1885,7 @@ static void testMonoRetrigTakeoverDeclicksRestart() {
s.play.adsr.attackFrames = 100; // real attack: the new tone starts near 0
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -1930,7 +1921,7 @@ static void testMonoRetrigFallbackDeclicksRestart() {
s.play.adsr.attackFrames = 100;
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -1965,7 +1956,7 @@ static void testMonoDeclickOnlyOnTakeover() {
s.play.adsr.attackFrames = 100;
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -1993,7 +1984,7 @@ static void testPolyStealDeclicksRestart() {
s.play.adsr.attackFrames = 100;
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Poly, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2026,7 +2017,7 @@ static void testSameBlockDoubleTakeoverKeepsDeclickSeed() {
s.play.adsr.attackFrames = 100;
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Poly, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2062,7 +2053,7 @@ static void testZeroAttackTakeoverNeverExceedsFullScale() {
s.play.adsr.attackFrames = 0; // zero-attack: amp == 1 on the very first frame
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2107,7 +2098,7 @@ static double maxDeltaAcross(double lastPre, const std::vector<AudioSample>& pos
static void testMonoRetrigTriggerZoneDeclicksRestart() {
SampleData s = sineSample(48000, 100.0, 60); // period 480 frames; slope <= ~0.013/frame
s.play.playMode = PlayMode::Trigger; // default fades: NO fade-in -> amp 1 at frame 0
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2134,7 +2125,7 @@ static void testZeroAttackGateRetrigNoStep() {
s.play.adsr.attackFrames = 0; // instant-unity attack
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 0;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2158,7 +2149,7 @@ static void testZeroAttackGateRetrigNoStep() {
// preview's exact shape (same note, root, full pool of 1).
static void testPreviewReauditionDeclicksViaEngineSteal() {
SampleData s = sineSample(48000, 100.0, 60); // default ADSR: instant unity (worst case)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Poly, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2186,7 +2177,7 @@ static void testOverCapChordStealsExactlyOne() {
s.play.adsr.sustainLevel = 1.0;
s.play.adsr.releaseFrames = 2880; // 60 ms @ 48k
s.play.pitchEngine = PitchEngine::Preserve;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
// Mirrors the processor: kPreserveVoiceCap = 8, 50 ms OLA window at 48k = 2400 frames.
VoiceEngine eng(3, km, /*preserveVoiceCap=*/8, /*preserveWindowFrames=*/2400);
@@ -2238,7 +2229,7 @@ static void testOverCapChordStealsExactlyOne() {
// path. This is the processor's mailbox-drain contract, pinned in the pure core.
static void testPreviewNoteObeysVoicing() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(2, km);
eng.noteOn(60, 127);
eng.noteOn(62, 127); // the pool is now FULL
@@ -2260,11 +2251,11 @@ static void testPreviewNoteObeysVoicing() {
// Pins the processor's mailbox-drain contract for Mono the way testPreviewNoteObeysVoicing
// pins it for Poly steal.
static void testPreviewNoteJoinsMonoHeldStack() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
CHECK(eng.noteOn(50, 127) == 0); // the host-MIDI note: zone A sounds
CHECK(eng.noteOn(50, kVelLow) == 0); // the host-MIDI note: zone A sounds
CHECK(approx(probeFrame(eng), 0.25, 1e-6));
CHECK(eng.noteOn(70, 127) == 0); // the preview press: TAKES the voice
CHECK(eng.noteOn(70, kVelHigh) == 0); // the preview press: TAKES the voice
CHECK(eng.activeVoiceCount() == 1); // still mono — the preview is no side-car
CHECK(approx(probeFrame(eng), 0.75, 1e-6));
eng.noteOff(70); // preview release: FALLBACK to the held note
@@ -2281,10 +2272,10 @@ static void testPreviewNoteJoinsMonoHeldStack() {
// the drain engine must release its voice (otherwise the old-snapshot preview would
// sustain until the next reload hard-cut it).
static void testPreviewNoteOffRoutesToDrainEngine() {
Keymap km = twoLevelKeymap();
SampleData km = twoLevelSample();
VoiceEngine drainEng(2, km); // was live when the preview fired
VoiceEngine liveEng(2, km); // the post-reload fresh snapshot: no voices
CHECK(drainEng.noteOn(70, 127) != VoiceEngine::kNoVoice);
CHECK(drainEng.noteOn(70, kVelHigh) != VoiceEngine::kNoVoice);
CHECK(approx(probeFrame(drainEng), 0.75, 1e-6)); // the preview rings in the old snapshot
CHECK(liveEng.activeVoiceCount() == 0);
// The preview release, drained to BOTH engines like a host note-off:
@@ -2313,7 +2304,7 @@ static void testDeclickBoundedBlendNoOvershoot() {
const double kCycles = 6000.0; // period = 8 frames
SampleData s = sineSample(kFrames, kCycles, 60);
s.play.playMode = PlayMode::Trigger; // no fade-in -> amp 1 on frame 0 (worst case)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger,
/*takeoverDeclick=*/true);
@@ -2402,7 +2393,7 @@ static void testPreserveTailFinalWindowGapFree() {
SampleData s = tailSine(frames, f0, 60);
s.play.pitchEngine = PitchEngine::Preserve; // Gate, no loop -> runs to the sample end
s.play.adsr = flatAdsr(); // held: amp 1 to the end (isolates the DSP)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(w));
eng.noteOn(note, 127);
std::vector<AudioSample> out;
@@ -2430,7 +2421,7 @@ static void testPreserveTailReleaseContinuous() {
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr();
s.play.adsr.releaseFrames = static_cast<std::int64_t>(w); // release spans the final window
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(w));
eng.noteOn(67, 127);
std::vector<AudioSample> out;
@@ -2462,7 +2453,7 @@ static void testPreserveTriggerTailGapFree() {
s.play.pitchEngine = PitchEngine::Preserve;
s.play.playMode = PlayMode::Trigger;
s.play.trigger.lengthFraction = 0.8; // playEnd = 6554 (~40 exact cycles: ends near zero)
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(w));
eng.noteOn(67, 127);
const std::size_t playEnd = 6554; // round(0.8 * 8192)
@@ -2497,7 +2488,7 @@ static void testPreservePrimeStopsAtTriggerPlayEnd() {
s.play.playMode = PlayMode::Trigger;
s.play.pitchEngine = PitchEngine::Preserve;
s.play.trigger.lengthFraction = 0.0625; // exactly 500 / 8000
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(w));
eng.noteOn(72, 127); // +1 octave: the tap outruns the read head into
// the deepest primed history the ring holds
@@ -2525,7 +2516,7 @@ static void testPreserveSubWindowSampleNoZeroPadInRing() {
SampleData s = tailSine(frames, f0, 60);
s.play.pitchEngine = PitchEngine::Preserve;
s.play.adsr = flatAdsr();
Keymap km = Keymap::singleSampleChromatic(std::move(s));
SampleData km = (std::move(s));
VoiceEngine eng(1, km, /*preserveCap=*/0, static_cast<std::int64_t>(w));
eng.noteOn(72, 127); // +1 octave up-shift (tap sweeps the whole ring)
std::vector<AudioSample> out;
@@ -2538,9 +2529,9 @@ static void testPreserveSubWindowSampleNoZeroPadInRing() {
}
int main() {
testChromaticSingleRoot();
testZonedRangesBoundaries();
testFirstMatchOnOverlap();
testEveryKeyPlaysTheLoadedCapture();
testUnplayableCaptureRefusesEveryNote();
testOutOfRangeNotesAreRefusedInMono();
testPitchRatioMath();
testKeyTrackedRatioMath();
testRepitchObservedPeriod();
@@ -2551,7 +2542,6 @@ int main() {
testAdsrZeroAttackDecay();
testPolyphonicAllocation();
testNoteOffReleasesNewestSameNote();
testOutOfZoneNoteConsumesNoVoice();
testStealsReleasingVoiceFirst();
testStealsOldestWhenNoneReleasing();
testLoopSustainSeamless();
@@ -2611,11 +2601,11 @@ int main() {
testMonoReleaseOfLowerHeldNoteIsInaudible();
testMonoRepressHeldNoteMovesToTop();
testMonoRetriggerFallbackUsesOriginalVelocity();
testMonoOutOfZoneNeverJoinsStack();
testMonoOutOfRangeNeverJoinsStack();
testMonoRetriggerRestartsEnvelope();
testMonoLegatoContinuesEnvelope();
testMonoLegatoRetunesWithoutReadRestart();
testMonoLegatoCrossSampleRestarts();
testMonoLegatoAlwaysGlidesWithinThePhrase();
testMonoLegatoAfterReleaseReattacks();
testMonoIgnoresPreserveCap();
testMonoLegatoTriggerReattacksAfterKeyUp();