Retire the zone system: one capture = one parameter set, and re-seam the engine and Sample face into bands
This commit is contained in:
+177
-187
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user