Ξ-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:
@@ -11,16 +11,17 @@ namespace reasampler::instrument::bake {
|
||||
|
||||
namespace {
|
||||
|
||||
// A fixed render block, deliberately independent of the host's: the block boundary is
|
||||
// where the engine observes live parameters and re-checks voice state, so pinning it here
|
||||
// is what keeps two bakes of one dialed sound byte-identical on different hosts.
|
||||
// A fixed render block rather than the host's. A block boundary is where the engine
|
||||
// re-observes live state, and the detach below leaves it nothing to observe — so this is
|
||||
// defence in depth against a future block-boundary read, not the reason two bakes agree.
|
||||
constexpr std::int64_t kBlockFrames = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan) {
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainLinear) {
|
||||
BakeAudio out;
|
||||
if (!sample.playable() || plan.totalFrames <= 0 || plan.sampleRate <= 0) return out;
|
||||
if (plan.leadInFrames < 0 || plan.renderFrames() > kMaxBakeFrames) return out;
|
||||
|
||||
// The live block is the audio thread's moving target; a render that observed it would
|
||||
// depend on what the user happened to be dragging. The dialed values are already in
|
||||
@@ -28,9 +29,9 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan) {
|
||||
sample.live = nullptr;
|
||||
|
||||
const int channels = sample.channelCount();
|
||||
const auto total = static_cast<std::size_t>(plan.totalFrames);
|
||||
std::vector<AudioSample> left(total, 0.f);
|
||||
std::vector<AudioSample> right(channels == 2 ? total : 0u, 0.f);
|
||||
const auto rendered = static_cast<std::size_t>(plan.renderFrames());
|
||||
std::vector<AudioSample> left(rendered, 0.f);
|
||||
std::vector<AudioSample> right(channels == 2 ? rendered : 0u, 0.f);
|
||||
|
||||
// Pre-size the Preserve shifters here, off any audio thread, exactly as the processor
|
||||
// does for its live engine — a cold shifter would smear the onset.
|
||||
@@ -40,17 +41,18 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan) {
|
||||
VoiceEngine engine(/*maxVoices=*/1, sample, /*preserveVoiceCap=*/0, preserveWindow,
|
||||
VoiceMode::Poly, MonoTrigger::Retrigger, /*takeoverDeclick=*/false);
|
||||
|
||||
for (std::int64_t pos = 0; pos < plan.totalFrames;) {
|
||||
for (std::int64_t pos = 0; pos < plan.renderFrames();) {
|
||||
if (pos == plan.noteOnFrame) engine.noteOn(plan.note, plan.velocity);
|
||||
// Trigger ignores note-off by design; in Gate this is the release the programmed
|
||||
// note length bounds.
|
||||
if (pos == plan.noteOffFrame) engine.noteOff(plan.note);
|
||||
|
||||
// Stop the block at the next event frame so both land sample-accurately.
|
||||
std::int64_t limit = plan.totalFrames;
|
||||
if (pos < plan.noteOnFrame) limit = plan.noteOnFrame;
|
||||
else if (pos < plan.noteOffFrame) limit = plan.noteOffFrame;
|
||||
const std::int64_t chunk = std::min(limit - pos, kBlockFrames);
|
||||
// Stop the block at the next event frame so both land sample-accurately. An event
|
||||
// past the window (a capture that closes before the note) never bounds anything.
|
||||
std::int64_t limit = plan.renderFrames();
|
||||
if (pos < plan.noteOnFrame) limit = (std::min)(limit, plan.noteOnFrame);
|
||||
else if (pos < plan.noteOffFrame) limit = (std::min)(limit, plan.noteOffFrame);
|
||||
const std::int64_t chunk = (std::min)(limit - pos, kBlockFrames);
|
||||
if (chunk <= 0) break; // unreachable while limit > pos; a guard, not a path
|
||||
|
||||
const auto at = static_cast<std::size_t>(pos);
|
||||
@@ -62,10 +64,15 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan) {
|
||||
|
||||
out.channelCount = channels;
|
||||
out.sampleRate = plan.sampleRate;
|
||||
const auto lead = static_cast<std::size_t>(plan.leadInFrames);
|
||||
const auto total = static_cast<std::size_t>(plan.totalFrames);
|
||||
out.interleaved.resize(total * static_cast<std::size_t>(channels));
|
||||
// A flat multiply, not the processor's per-sample ramp: the gain is constant for the
|
||||
// whole render, which is exactly what that ramp exists to converge to.
|
||||
const auto gain = static_cast<AudioSample>(masterGainLinear);
|
||||
for (std::size_t f = 0; f < total; ++f) {
|
||||
out.interleaved[f * channels] = left[f];
|
||||
if (channels == 2) out.interleaved[f * channels + 1] = right[f];
|
||||
out.interleaved[f * channels] = left[lead + f] * gain;
|
||||
if (channels == 2) out.interleaved[f * channels + 1] = right[lead + f] * gain;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user