Merge ps-w8-t2-waveform: S11 waveform view + draggable loop points
This commit is contained in:
@@ -520,6 +520,115 @@ static void testAbsentLoopGoesSilent() {
|
||||
for (std::size_t i = 60; i < out.size(); ++i) CHECK(approx(out[i], 0.0, 1e-6));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// start point (S11): the voice's initial read position is SampleData::startFrame.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void testStartFrameOffsetsInitialRead() {
|
||||
// A per-frame ramp (frame i holds i*0.01) so the first rendered value pinpoints the
|
||||
// read position. startFrame = 30 -> the first output frame reads frame 30 (0.30).
|
||||
SampleData s;
|
||||
s.frames.resize(100);
|
||||
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));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127); // unity ratio, full velocity, flat gain
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 3);
|
||||
CHECK(approx(out[0], 0.30, 1e-4)); // starts at frame 30, not 0
|
||||
CHECK(approx(out[1], 0.31, 1e-4)); // advances by unity ratio
|
||||
CHECK(approx(out[2], 0.32, 1e-4));
|
||||
}
|
||||
|
||||
static void testStartFrameZeroIsUnchanged() {
|
||||
// startFrame default 0 is exactly the pre-S11 behavior: read begins at frame 0.
|
||||
SampleData s;
|
||||
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));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 1);
|
||||
CHECK(approx(out[0], 0.0, 1e-6)); // frame 0
|
||||
}
|
||||
|
||||
static void testStartFrameOutOfRangeClampsToZero() {
|
||||
// A start point at/past the sample end degrades to frame 0 (play from the top), never an
|
||||
// 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));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 5);
|
||||
// Reads from frame 0: the DC sample plays its 1.0 body rather than an immediate idle.
|
||||
CHECK(eng.activeVoiceCount() == 1);
|
||||
CHECK(approx(out[0], 1.0, 1e-4));
|
||||
}
|
||||
|
||||
static void testStartFrameWithLoop() {
|
||||
// Start point and loop compose: begin reading mid-sample, then sustain the loop region.
|
||||
SampleData s;
|
||||
s.frames.resize(40);
|
||||
for (int i = 0; i < 40; ++i) s.frames[i] = static_cast<float>(i) * 0.01f;
|
||||
for (int i = 20; i < 40; ++i) s.frames[i] = 0.5f; // loop body
|
||||
s.rootNote = 60;
|
||||
s.startFrame = 10; // begin at frame 10
|
||||
s.loop.hasLoop = true;
|
||||
s.loop.start = 20;
|
||||
s.loop.end = 40;
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 200);
|
||||
CHECK(approx(out[0], 0.10, 1e-4)); // started at frame 10
|
||||
CHECK(eng.activeVoiceCount() == 1); // loop sustains it
|
||||
for (std::size_t i = 60; i < out.size(); ++i) CHECK(approx(out[i], 0.5, 1e-4));
|
||||
}
|
||||
|
||||
static void testStartAfterLoopEndWrapsIntoLoop() {
|
||||
// Regression (S11 reviewer finding): if startFrame > loop.end (but still < frameCount),
|
||||
// the voice's initial read head is past the loop end. The wrap-while in renderFrame must
|
||||
// pull it back into [loopStart, loopEnd) on the very first frame, so the note sounds from
|
||||
// somewhere inside the loop rather than running off the sample end silently.
|
||||
//
|
||||
// Setup: 100-frame sample; loop is [20, 40); startFrame = 60 (past loop.end = 40).
|
||||
// loop body is a constant 0.5 so every frame inside it reads 0.5.
|
||||
// After wrap: readPos starts inside [20, 40), first output frame == 0.5.
|
||||
// Voice must stay active (loop sustains it) and emit the loop value, NOT go silent.
|
||||
SampleData s;
|
||||
s.frames.resize(100, 0.0f);
|
||||
for (int i = 20; i < 40; ++i) s.frames[i] = 0.5f; // loop body
|
||||
s.rootNote = 60;
|
||||
s.startFrame = 60; // > loop.end (40), < frameCount (100)
|
||||
s.loop.hasLoop = true;
|
||||
s.loop.start = 20;
|
||||
s.loop.end = 40;
|
||||
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127); // unity ratio, full velocity
|
||||
|
||||
std::vector<AudioSample> out;
|
||||
eng.render(out, 50);
|
||||
|
||||
// Voice must still be active — the usable loop keeps it alive indefinitely.
|
||||
CHECK(eng.activeVoiceCount() == 1);
|
||||
// Every frame after the first wrap must read 0.5 (the loop body). We skip the very
|
||||
// first frame because the fractional-position wrap lands somewhere in [20,40) and the
|
||||
// exact offset depends on how many loop lengths fit into 60; what matters is that the
|
||||
// voice is alive and emitting the loop value, not 0.0 (pre-loop region).
|
||||
for (std::size_t i = 5; i < out.size(); ++i) {
|
||||
CHECK(approx(out[i], 0.5, 1e-4));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// velocity -> volume.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -692,6 +801,11 @@ int main() {
|
||||
testZeroLengthLoopGoesSilent();
|
||||
testSingleFrameLoop();
|
||||
testAbsentLoopGoesSilent();
|
||||
testStartFrameOffsetsInitialRead();
|
||||
testStartFrameZeroIsUnchanged();
|
||||
testStartFrameOutOfRangeClampsToZero();
|
||||
testStartFrameWithLoop();
|
||||
testStartAfterLoopEndWrapsIntoLoop();
|
||||
testVelocityToVolume();
|
||||
testPolyphonyMixesAdditively();
|
||||
testChannelCount();
|
||||
|
||||
Reference in New Issue
Block a user