Ξ-W2-T1 remediation: print master gain into the bake, derive the window from the dialed sound, reset play mode to Trigger
This commit is contained in:
+69
-12
@@ -4,8 +4,9 @@
|
||||
// Covers: Gate termination WITH a sustain loop active (the render must end at the window,
|
||||
// and the tail must be silent because the gate actually released — not merely because the
|
||||
// buffer ran out); Trigger termination on its own play span; channel-count preservation
|
||||
// with no stereo fold; byte-identical repeats; and the refusals (unplayable sample, empty
|
||||
// window).
|
||||
// with no stereo fold; the master gain being PRINTED into the output; a lead-in rendered
|
||||
// and discarded; byte-identical repeats; and the refusals (unplayable sample, empty
|
||||
// window, a window past the frame ceiling).
|
||||
|
||||
#include "../src/core/instrument/bake/bake_render.h"
|
||||
|
||||
@@ -48,9 +49,11 @@ double peakAt(const BakeAudio& audio, std::int64_t from, std::int64_t to) {
|
||||
return peak;
|
||||
}
|
||||
|
||||
BakePlan planOf(std::int64_t total, std::int64_t noteOn, std::int64_t noteOff) {
|
||||
BakePlan planOf(std::int64_t total, std::int64_t noteOn, std::int64_t noteOff,
|
||||
std::int64_t leadIn = 0) {
|
||||
BakePlan p;
|
||||
p.totalFrames = total;
|
||||
p.leadInFrames = leadIn;
|
||||
p.noteOnFrame = noteOn;
|
||||
p.noteOffFrame = noteOff;
|
||||
p.note = 60;
|
||||
@@ -59,6 +62,8 @@ BakePlan planOf(std::int64_t total, std::int64_t noteOn, std::int64_t noteOff) {
|
||||
return p;
|
||||
}
|
||||
|
||||
constexpr double kUnity = 1.0;
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -72,7 +77,7 @@ int main() {
|
||||
s.play.adsr.releaseFrames = 480; // 10 ms — short enough to finish inside the tail
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/9600, /*noteOn=*/0, /*noteOff=*/4800);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
const BakeAudio audio = renderBake(s, plan, kUnity);
|
||||
|
||||
CHECK(audio.frameCount() == 9600); // bounded, not a runaway
|
||||
CHECK(audio.channelCount == 1);
|
||||
@@ -90,7 +95,7 @@ int main() {
|
||||
s.play.trigger.lengthFraction = 0.5; // 500 source frames at unity ratio
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/2000, /*noteOn=*/0, /*noteOff=*/100);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
const BakeAudio audio = renderBake(s, plan, kUnity);
|
||||
|
||||
CHECK(audio.frameCount() == 2000);
|
||||
// Still sounding past the note-off Trigger ignores…
|
||||
@@ -105,7 +110,7 @@ int main() {
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/500, /*noteOn=*/0, /*noteOff=*/500);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
const BakeAudio audio = renderBake(s, plan, kUnity);
|
||||
|
||||
CHECK(audio.channelCount == 2);
|
||||
CHECK(audio.frameCount() == 500);
|
||||
@@ -117,6 +122,54 @@ int main() {
|
||||
CHECK(audio.interleaved[201] > -0.3f);
|
||||
}
|
||||
|
||||
// --- The master gain is PRINTED into the file ---------------------------------------
|
||||
// The reset hands the control back at unity, so a render that summed voices alone would
|
||||
// shift every iteration by 1/gain — and a gain dialed to silence would come back loud.
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/true, /*frames=*/1000);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
const BakePlan plan = planOf(/*total=*/500, /*noteOn=*/0, /*noteOff=*/500);
|
||||
|
||||
const BakeAudio unity = renderBake(s, plan, kUnity);
|
||||
const BakeAudio quiet = renderBake(s, plan, 0.25);
|
||||
const BakeAudio loud = renderBake(s, plan, 4.0);
|
||||
const BakeAudio silent = renderBake(s, plan, 0.0);
|
||||
|
||||
CHECK(unity.interleaved.size() == quiet.interleaved.size());
|
||||
bool scaled = !unity.interleaved.empty();
|
||||
for (std::size_t i = 0; scaled && i < unity.interleaved.size(); ++i) {
|
||||
scaled = std::fabs(quiet.interleaved[i] - unity.interleaved[i] * 0.25f) < 1e-6f &&
|
||||
std::fabs(loud.interleaved[i] - unity.interleaved[i] * 4.0f) < 1e-5f;
|
||||
}
|
||||
CHECK(scaled);
|
||||
// Both channels, not just the one the peak helper reads.
|
||||
CHECK(quiet.interleaved[201] < 0.f && quiet.interleaved[201] > -0.1f);
|
||||
// A gain of zero prints silence rather than returning the sound at full level.
|
||||
CHECK(peakAt(silent, 0, 500) == 0.0);
|
||||
CHECK(peakAt(unity, 0, 500) > 0.4);
|
||||
}
|
||||
|
||||
// --- A lead-in is rendered and then discarded ---------------------------------------
|
||||
// A positive start offset trims the note's head: the frames before the window must be
|
||||
// produced (so the envelope really is mid-flight when the file opens) and dropped.
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/false, /*frames=*/4000);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.trigAhd.attackFrames = 1000; // still climbing when the window opens
|
||||
|
||||
const BakePlan trimmed = planOf(/*total=*/1000, /*noteOn=*/0, /*noteOff=*/2000,
|
||||
/*leadIn=*/1000);
|
||||
const BakeAudio audio = renderBake(s, trimmed, kUnity);
|
||||
CHECK(audio.frameCount() == 1000); // the FILE is the window, not the render
|
||||
|
||||
// Frame 0 of the file is frame 1000 of the render — the attack's end, not its
|
||||
// start. A clamped-away lead-in would put the attack's silent onset here instead.
|
||||
const BakeAudio whole = renderBake(s, planOf(/*total=*/2000, 0, 2000), kUnity);
|
||||
CHECK(peakAt(audio, 0, 1) > peakAt(whole, 0, 1));
|
||||
CHECK(std::fabs(static_cast<double>(audio.interleaved[0]) -
|
||||
static_cast<double>(whole.interleaved[1000])) < 1e-6);
|
||||
}
|
||||
|
||||
// --- Bit-identical repeats ---------------------------------------------------------
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/true, /*frames=*/1000);
|
||||
@@ -125,8 +178,8 @@ int main() {
|
||||
s.play.adsr.releaseFrames = 211;
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/4096, /*noteOn=*/13, /*noteOff=*/2731);
|
||||
const BakeAudio a = renderBake(s, plan);
|
||||
const BakeAudio b = renderBake(s, plan);
|
||||
const BakeAudio a = renderBake(s, plan, kUnity);
|
||||
const BakeAudio b = renderBake(s, plan, kUnity);
|
||||
|
||||
CHECK(a.interleaved.size() == b.interleaved.size());
|
||||
CHECK(!a.interleaved.empty());
|
||||
@@ -160,7 +213,7 @@ int main() {
|
||||
}
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/1000, /*noteOn=*/0, /*noteOff=*/1000);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
const BakeAudio audio = renderBake(s, plan, kUnity);
|
||||
|
||||
CHECK(s.live == &block); // the caller's own snapshot was not detached
|
||||
// At frame 100 the dialed instant attack is at full level; the published 900-frame
|
||||
@@ -170,7 +223,7 @@ int main() {
|
||||
// And it matches a render from a block-free copy exactly.
|
||||
SampleData detached = s;
|
||||
detached.live = nullptr;
|
||||
const BakeAudio reference = renderBake(detached, plan);
|
||||
const BakeAudio reference = renderBake(detached, plan, kUnity);
|
||||
bool identical = audio.interleaved.size() == reference.interleaved.size();
|
||||
for (std::size_t i = 0; identical && i < audio.interleaved.size(); ++i)
|
||||
identical = (audio.interleaved[i] == reference.interleaved[i]);
|
||||
@@ -181,10 +234,14 @@ int main() {
|
||||
{
|
||||
SampleData empty; // nothing decoded
|
||||
empty.sampleRate = kRate;
|
||||
CHECK(renderBake(empty, planOf(1000, 0, 500)).empty());
|
||||
CHECK(renderBake(empty, planOf(1000, 0, 500), kUnity).empty());
|
||||
|
||||
SampleData s = makeSample(false);
|
||||
CHECK(renderBake(s, planOf(0, 0, 0)).empty());
|
||||
CHECK(renderBake(s, planOf(0, 0, 0), kUnity).empty());
|
||||
// The ceiling planBake enforces is re-checked here: a hand-built plan must not be
|
||||
// able to walk the render into an allocation it cannot hold.
|
||||
CHECK(renderBake(s, planOf(kMaxBakeFrames, 0, 0, /*leadIn=*/1), kUnity).empty());
|
||||
CHECK(renderBake(s, planOf(1000, 0, 500, /*leadIn=*/-1), kUnity).empty());
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_render: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user