Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
+56
-17
@@ -2,7 +2,8 @@
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: the default program's window derived from the dialed sound (Gate's hold to source
|
||||
// exhaustion plus its release, a Trigger play span, and the Varispeed read-stretch bound);
|
||||
// exhaustion plus its release, a start point, a Trigger play span, and the Varispeed
|
||||
// read-stretch bound on both branches that take it);
|
||||
// the frame window and both event frames against hand-computed values; a capture opening
|
||||
// BEFORE note-on and one opening AFTER it; the refusals and what each one reports — a
|
||||
// collapsed window, a non-positive rate, a window that rounds to nothing, and one past the
|
||||
@@ -45,8 +46,10 @@ SampleData dialedSample(std::size_t frames = 96000) {
|
||||
// The Hold default; read only where the window is underivable, which is nowhere in this file.
|
||||
Division oneBar() { return makeDivision(2, DivisionModifier::Straight); }
|
||||
|
||||
NoteProgram derived(const SampleData& s, Tempo t) {
|
||||
return defaultBakeProgram(s, kRate, t, oneBar(), Velocity{});
|
||||
// `t` is unused by the derivation itself — it takes no tempo — but every caller here resolves
|
||||
// against the same one, so threading it keeps each case's two halves visibly paired.
|
||||
NoteProgram derived(const SampleData& s, Tempo) {
|
||||
return defaultBakeProgram(s, kRate, oneBar(), Velocity{});
|
||||
}
|
||||
|
||||
bool near(double a, double b) { return a > b - 1e-6 && a < b + 1e-6; }
|
||||
@@ -60,8 +63,8 @@ int main() {
|
||||
s.play.playMode = PlayMode::Gate;
|
||||
s.play.adsr.releaseFrames = kRate * 3 / 2; // 1.5 s — past any fixed tail
|
||||
const ResolvedNote r = resolveNote(derived(s, at(120.0)), at(120.0));
|
||||
// The note is rounded up to the shortest length outlasting the source — 2 s exactly,
|
||||
// one bar at 120 BPM — instead of the constant quarter note that released it early.
|
||||
// The note is held to exhaustion — 2 s exactly — instead of the constant quarter note
|
||||
// that released it early.
|
||||
CHECK(near(r.noteOffSeconds, 2.0));
|
||||
CHECK(r.captureStartSeconds == 0.0);
|
||||
CHECK(near(r.captureEndSeconds, 2.0 + 1.5 + kPadSeconds));
|
||||
@@ -72,14 +75,50 @@ int main() {
|
||||
const ResolvedNote shorter = resolveNote(derived(s, at(120.0)), at(120.0));
|
||||
CHECK(near(shorter.captureEndSeconds, 2.0 + 0.1 + kPadSeconds));
|
||||
|
||||
// A source that does not land on a rung rounds UP. 2.5 s is 5 beats, and the shortest
|
||||
// rung at or above that is the 2/1 triplet (8 * 2/3 == 5.33 beats) — NOT the dotted
|
||||
// half above it, which is why picker order is not length order.
|
||||
// The length is EXACT, not rounded onto the ladder: 2.5 s is 5 beats, which no rung
|
||||
// hits — the ladder's nearest never-short answer is the 2/1 triplet at 5.33 beats, and
|
||||
// taking it would buy a third of a second of silence for nothing. The tempo is not an
|
||||
// input to a derived length at all, so the same sound derives the same seconds at any.
|
||||
SampleData odd = dialedSample(static_cast<std::size_t>(kRate * 5 / 2)); // 2.5 s
|
||||
odd.play.playMode = PlayMode::Gate;
|
||||
const ResolvedNote up = resolveNote(derived(odd, at(120.0)), at(120.0));
|
||||
CHECK(up.noteOffSeconds >= 2.5); // the property that matters: never short
|
||||
CHECK(near(up.noteOffSeconds, at(120.0).beatsToSeconds(8.0 * 2.0 / 3.0)));
|
||||
CHECK(near(resolveNote(derived(odd, at(120.0)), at(120.0)).noteOffSeconds, 2.5));
|
||||
CHECK(near(resolveNote(derived(odd, at(97.0)), at(97.0)).noteOffSeconds, 2.5));
|
||||
}
|
||||
|
||||
// --- Gate with no loop, a START POINT set: only the post-start span is held ----------
|
||||
// effectiveStart's whole reason to exist. A start marker means the head begins there, so
|
||||
// holding the note for the WHOLE source would pad the window with silence the voice
|
||||
// already finished; holding it for the source minus the start is exact.
|
||||
{
|
||||
SampleData s = dialedSample(/*frames=*/kRate * 2); // 2 s
|
||||
s.play.playMode = PlayMode::Gate;
|
||||
s.play.adsr.releaseFrames = 0;
|
||||
s.startFrame = kRate / 2; // start half a second in -> 1.5 s of sound left
|
||||
CHECK(near(resolveNote(derived(s, at(120.0)), at(120.0)).noteOffSeconds, 1.5));
|
||||
|
||||
// Voice::start's own degenerate rule: a start at or past the end plays from the top,
|
||||
// so the window covers the whole source rather than nothing.
|
||||
s.startFrame = kRate * 2;
|
||||
CHECK(near(resolveNote(derived(s, at(120.0)), at(120.0)).noteOffSeconds, 2.0));
|
||||
s.startFrame = -1;
|
||||
CHECK(near(resolveNote(derived(s, at(120.0)), at(120.0)).noteOffSeconds, 2.0));
|
||||
}
|
||||
|
||||
// --- Gate with no loop under Varispeed: the read-stretch bound applies here too -------
|
||||
// The Trigger branch has its own coverage below; this pins that the Gate exhaustion length
|
||||
// takes the same bound, which is the branch a downward offset would otherwise truncate.
|
||||
{
|
||||
SampleData s = dialedSample(/*frames=*/kRate); // 1 s
|
||||
s.play.playMode = PlayMode::Gate;
|
||||
s.play.adsr.releaseFrames = 0;
|
||||
s.play.pitchEngine = PitchEngine::Varispeed;
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.peakSemitones = -12.0; // an octave down == half speed at the peak
|
||||
CHECK(near(resolveNote(derived(s, at(120.0)), at(120.0)).noteOffSeconds, 2.0));
|
||||
|
||||
// Preserve reads at the source rate, so the same dial bounds nothing.
|
||||
s.play.pitchEngine = PitchEngine::Preserve;
|
||||
CHECK(near(resolveNote(derived(s, at(120.0)), at(120.0)).noteOffSeconds, 1.0));
|
||||
}
|
||||
|
||||
// --- Trigger: the window is the play span, which ignores the note's length ----------
|
||||
@@ -91,8 +130,8 @@ int main() {
|
||||
CHECK(r.captureStartSeconds == 0.0);
|
||||
CHECK(near(r.captureEndSeconds, 1.5 + kPadSeconds));
|
||||
|
||||
// A span SHORTER than the quarter note closes the window early rather than padding
|
||||
// it out to note-off — the sound is over, and a negative end offset is legal.
|
||||
// A shorter %-length is a shorter window, with no floor under it: the sound is over
|
||||
// when the span is, and note-off means nothing to a Trigger voice.
|
||||
s.play.trigger.lengthFraction = 0.1; // 0.2 s
|
||||
const ResolvedNote brief = resolveNote(derived(s, at(120.0)), at(120.0));
|
||||
CHECK(!brief.windowCollapsed);
|
||||
@@ -127,9 +166,9 @@ int main() {
|
||||
{{0.0, -0.5}, {127.0, 0.0}}, reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
|
||||
const NoteProgram soft =
|
||||
defaultBakeProgram(s, kRate, at(120.0), oneBar(), Velocity::of(1));
|
||||
defaultBakeProgram(s, kRate, oneBar(), Velocity::of(1));
|
||||
const NoteProgram hard =
|
||||
defaultBakeProgram(s, kRate, at(120.0), oneBar(), Velocity::of(127));
|
||||
defaultBakeProgram(s, kRate, oneBar(), Velocity::of(127));
|
||||
CHECK(soft.velocity.value() == 1);
|
||||
CHECK(hard.velocity.value() == 127);
|
||||
const ResolvedNote softR = resolveNote(soft, at(120.0));
|
||||
@@ -148,9 +187,9 @@ int main() {
|
||||
CHECK(bakeWindowNeedsHold(s));
|
||||
// The window follows Hold rather than the source, so a longer Hold is a longer file.
|
||||
const ResolvedNote bar = resolveNote(
|
||||
defaultBakeProgram(s, kRate, at(120.0), oneBar(), Velocity{}), at(120.0));
|
||||
defaultBakeProgram(s, kRate, oneBar(), Velocity{}), at(120.0));
|
||||
const ResolvedNote twoBars = resolveNote(
|
||||
defaultBakeProgram(s, kRate, at(120.0),
|
||||
defaultBakeProgram(s, kRate,
|
||||
makeDivision(3, DivisionModifier::Straight), Velocity{}),
|
||||
at(120.0));
|
||||
CHECK(near(bar.noteOffSeconds, 2.0));
|
||||
|
||||
Reference in New Issue
Block a user