// Standalone audit of the DERIVED bake window: does defaultBakeProgram's window hold the // whole audible result of the dialed sound, in both play modes? Every case renders through // the real chain (defaultBakeProgram -> resolveNote -> planBake -> renderBake) and then // re-renders the SAME sound with a longer window, so "what fell outside" is measured rather // than argued. Trailing silence is a pass; signal past the derived end is a truncation. #include "../src/core/instrument/bake/bake_plan.h" #include "../src/core/instrument/bake/bake_render.h" #include #include using namespace reasampler; using namespace reasampler::instrument::bake; using namespace reasampler::instrument::note; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) namespace { constexpr int kRate = 48000; constexpr double kBpm = 120.0; // a quarter note is 0.5 s == 24000 frames constexpr double kUnity = 1.0; constexpr double kSilence = 1e-6; Tempo tempo() { const std::optional t = Tempo::fromBpm(kBpm); if (!t) { std::printf("FAIL: fixture tempo rejected\n"); ++g_fail; } return t.value_or(Tempo::fromBpm(120.0).value()); } // Flat DC so a level reading is unambiguous: any departure from 0.5 is the envelope, the // filter or a ring-out, never the source's own shape. SampleData dcSample(std::size_t frames) { SampleData s; s.frames.assign(frames, 0.5f); s.sampleRate = kRate; s.rootNote = 60; return s; } double peakAt(const BakeAudio& audio, std::int64_t from, std::int64_t to) { double peak = 0.0; if (from < 0) from = 0; for (std::int64_t f = from; f < to && f < audio.frameCount(); ++f) { const double v = std::fabs(static_cast( audio.interleaved[static_cast(f * audio.channelCount)])); if (v > peak) peak = v; } return peak; } // The derived program, optionally lengthened: `extraMs` widens ONLY the end offset (the same // sound, a longer window), `length` overrides the programmed note length. Both leave the // derivation itself untouched, which is what makes the comparison a measurement of the // derived end rather than of a second derivation. NoteProgram derivedProgram(const SampleData& s, double extraMs) { NoteProgram p = defaultBakeProgram(s, kRate, tempo()); if (extraMs != 0.0) p.end = EndOffset(offsetFromMs(offsetMs(p.end.amount(), tempo()) + extraMs)); return p; } std::optional planOf(const NoteProgram& p) { return planBake(resolveNote(p, tempo()), kRate, 60); } // The render the shell would produce, plus `extraMs` of extra window. BakeAudio bakeWith(const SampleData& s, double extraMs) { const std::optional plan = planOf(derivedProgram(s, extraMs)); if (!plan) { std::printf("FAIL: fixture window refused\n"); ++g_fail; return BakeAudio{}; } return renderBake(s, *plan, kUnity); } std::int64_t derivedFrames(const SampleData& s) { const std::optional plan = planOf(derivedProgram(s, 0.0)); return plan ? plan->totalFrames : -1; } } // namespace int main() { // ================================ GATE ========================================== // --- Gate, no loop, staged AHDSR: the derived end is EXACT on the release ---------- // note-off at the quarter note, release_frames after it, and not one frame of signal // past that — the end offset is the release, so the two must coincide exactly. { SampleData s = dcSample(96000); s.play.playMode = PlayMode::Gate; s.play.adsr.releaseFrames = 4800; // 100 ms CHECK(derivedFrames(s) == 28800); // 24000 (quarter) + 4800 (release) const BakeAudio wide = bakeWith(s, /*extraMs=*/200.0); CHECK(wide.frameCount() == 38400); // Still releasing on the last ten frames the derived window would have kept… CHECK(peakAt(wide, 28790, 28800) > kSilence); // …and EXACTLY silent from there on: nothing was cut. CHECK(peakAt(wide, 28800, 38400) == 0.0); } // --- Gate with a sustain loop: there is NO intrinsic end to derive ----------------- // The window holds the whole programmed note, but the note length itself is a constant // quarter note — hold the same dialed sound longer and it keeps sounding, indefinitely. // This is the one number a derivation cannot supply. { SampleData s = dcSample(48000); s.loop = SampleLoop{true, 0, 24000}; s.play.playMode = PlayMode::Gate; s.play.adsr.releaseFrames = 4800; const BakeAudio derived = bakeWith(s, 0.0); CHECK(derived.frameCount() == 28800); // The derived window does bound the render: full level while the note is held, and // down to the release's own floor at the end — the loop is not left cycling. CHECK(peakAt(derived, 20000, 24000) > 0.4); CHECK(peakAt(derived, 28790, 28800) < 0.01); // The SAME dialed sound held for a whole note instead of a quarter: four times the // audio, all of it real, none of it reachable from the derivation. NoteProgram longer = derivedProgram(s, 0.0); longer.length = makeDivision(2, DivisionModifier::Straight); // 4 beats == 2 s const std::optional plan = planOf(longer); CHECK(plan.has_value()); const BakeAudio held = renderBake(s, *plan, kUnity); CHECK(held.frameCount() == 100800); // 96000 + 4800 CHECK(peakAt(held, 28800, 96000) > 0.4); // full level, well past the derived end } // --- Gate: the constant quarter-note hold TRUNCATES a slow attack ------------------ // The window is correct for the note it programs; the note is what is wrong. A 2 s // attack never reaches its peak, so the bake prints a sound the user never dialed. { SampleData s = dcSample(192000); s.play.playMode = PlayMode::Gate; s.play.adsr.attackFrames = 96000; // 2 s, four times the programmed note s.play.adsr.releaseFrames = 0; const BakeAudio derived = bakeWith(s, 0.0); CHECK(derived.frameCount() == 24000); // A quarter of the way up the attack at most. const double derivedPeak = peakAt(derived, 0, 24000); CHECK(derivedPeak > 0.1 && derivedPeak < 0.14); NoteProgram longer = derivedProgram(s, 0.0); longer.length = makeDivision(3, DivisionModifier::Straight); // 8 beats == 4 s const std::optional plan = planOf(longer); CHECK(plan.has_value()); const BakeAudio held = renderBake(s, *plan, kUnity); CHECK(peakAt(held, 0, held.frameCount()) > 0.49); // the dialed sound, in full } // --- Gate + Preserve: the terminal ring-out is cut when the source ends AT the window // Preserve rings its last real output out (~4 ms) instead of hard-cutting it; that ramp // lands past the derived end whenever the voice's own end lands on it. { SampleData s = dcSample(24000); // exhausts exactly at the quarter-note note-off s.play.playMode = PlayMode::Gate; s.play.pitchEngine = PitchEngine::Preserve; s.play.adsr.releaseFrames = 0; CHECK(derivedFrames(s) == 24000); const BakeAudio wide = bakeWith(s, /*extraMs=*/50.0); CHECK(peakAt(wide, 23990, 24000) > 0.4); // hard cut at full level… CHECK(peakAt(wide, 24000, 24100) > 0.05); // …with the ring-out outside the window CHECK(peakAt(wide, 24400, wide.frameCount()) < kSilence); // and it is bounded } // --- The resonant filter cannot ring past the amp gate ------------------------------ // pitch -> filter -> amp: the amp multiply is last, so a high-Q filter's ring-out is // gated by the release the window already holds. Not a tail contributor. { SampleData s = dcSample(96000); s.play.playMode = PlayMode::Gate; s.play.adsr.releaseFrames = 4800; s.play.filter.enabled = true; s.play.filter.settings.cutoffNorm = 0.05f; s.play.filter.settings.resonanceNorm = 1.0f; const BakeAudio wide = bakeWith(s, /*extraMs=*/500.0); CHECK(peakAt(wide, 0, 28800) > kSilence); // the filtered note sounded CHECK(peakAt(wide, 28800, wide.frameCount()) == 0.0); // and nothing rang past it } // ================================ TRIGGER ======================================= // --- Trigger, Varispeed, a constant deep downward pitch offset: UPPER BOUND --------- // The window is scaled by the deepest reachable offset, so a shallower excursion leaves // trailing silence — long, but never short. { SampleData s = dcSample(48000); s.play.playMode = PlayMode::Trigger; s.play.pitchEngine = PitchEngine::Varispeed; s.play.pitchEnv.enabled = true; s.play.pitchEnv.peakSemitones = -24.0; // two octaves down s.play.pitchEnv.shape.holdFraction = 1.0; // held down for the whole span // 1 s of source stretched by 2^(24/12) == 4. CHECK(derivedFrames(s) == 192000); const BakeAudio derived = bakeWith(s, 0.0); // The offset only holds for the envelope's own span, so the read finishes near // 84000 frames — inside the window, with the balance as trailing silence. CHECK(peakAt(derived, 80000, 84000) > 0.4); CHECK(peakAt(derived, 90000, 192000) < kSilence); } // --- Trigger + Preserve (the product-default engine): the ring-out is TRUNCATED ----- // Preserve's read reaches playEnd on the exact frame the derived window closes, and the // ~4 ms terminal declick that follows is entirely outside it. The baked file therefore // ends on a hard cut at full level — reintroducing the click the ramp exists to remove. { SampleData s = dcSample(48000); s.play.playMode = PlayMode::Trigger; s.play.pitchEngine = PitchEngine::Preserve; CHECK(derivedFrames(s) == 48000); const BakeAudio wide = bakeWith(s, /*extraMs=*/50.0); CHECK(peakAt(wide, 47990, 48000) > 0.4); // full level on the derived last frame CHECK(peakAt(wide, 48000, 48100) > 0.05); // real signal past the derived end // Measured: the ramp opens at the last in-window level (0.500) and reaches exact // zero 180 frames later — the whole 3.75 ms sits outside the derived window. CHECK(peakAt(wide, 48000, 48001) > 0.49); CHECK(peakAt(wide, 48179, 48180) > 0.0); CHECK(peakAt(wide, 48180, wide.frameCount()) == 0.0); } // --- Trigger + a DRAWN amp EG: the window keeps a %-length the engine IGNORES ------- // A drawn contour covers the full sample length, so Voice::start forces lengthFraction // to 1.0 (splineActive, play_params.h) — but defaultBakeProgram reads the stored, // UI-inert knob straight through triggerPlayLength. The window is a quarter of the take. { SampleData s = dcSample(48000); s.play.playMode = PlayMode::Trigger; s.play.trigger.lengthFraction = 0.25; // inert in the engine, live in the derivation s.play.ampSpline.mode = EnvMode::Spline; s.play.ampSpline.contour = VelocityCurve::flat(); // full level across the sample CHECK(derivedFrames(s) == 12000); // 0.25 s of a 1 s take const BakeAudio wide = bakeWith(s, /*extraMs=*/1000.0); // The engine really did play the whole sample: full level right up to 48000… CHECK(peakAt(wide, 12000, 48000) > 0.4); CHECK(peakAt(wide, 47000, 48000) > 0.4); // …and stopped at its own span end, so the 36000 lost frames are the take, not tail. CHECK(peakAt(wide, 48200, wide.frameCount()) < kSilence); } // --- A legitimate dialed sound past the frame ceiling is REFUSED, not cut ----------- // 1.1e6 source frames stretched by 2^(48/12) == 16 exceeds kMaxBakeFrames. The refusal // is the safe direction (the shell reports it), but it is a sound that cannot be baked. { SampleData s = dcSample(1'100'000); s.play.playMode = PlayMode::Trigger; s.play.pitchEngine = PitchEngine::Varispeed; s.play.pitchEnv.enabled = true; s.play.pitchEnv.peakSemitones = -48.0; s.play.pitchEnv.shape.holdFraction = 1.0; CHECK(!planOf(derivedProgram(s, 0.0)).has_value()); // One octave shallower is inside the ceiling — the refusal above is the window, not // the fixture. s.play.pitchEnv.peakSemitones = -24.0; CHECK(planOf(derivedProgram(s, 0.0)).has_value()); } if (g_fail == 0) std::printf("bake_window: all tests passed\n"); return g_fail ? 1 : 0; }