Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// See bake_render.h.
|
||||
|
||||
#include "core/instrument/bake/bake_render.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/instrument/engine/voice_engine.h"
|
||||
|
||||
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.
|
||||
constexpr std::int64_t kBlockFrames = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan) {
|
||||
BakeAudio out;
|
||||
if (!sample.playable() || plan.totalFrames <= 0 || plan.sampleRate <= 0) 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
|
||||
// this SampleData's own play params, which is what the bake is meant to print.
|
||||
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);
|
||||
|
||||
// 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.
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * static_cast<double>(plan.sampleRate) / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
VoiceEngine engine(/*maxVoices=*/1, sample, /*preserveVoiceCap=*/0, preserveWindow,
|
||||
VoiceMode::Poly, MonoTrigger::Retrigger, /*takeoverDeclick=*/false);
|
||||
|
||||
for (std::int64_t pos = 0; pos < plan.totalFrames;) {
|
||||
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);
|
||||
if (chunk <= 0) break; // unreachable while limit > pos; a guard, not a path
|
||||
|
||||
const auto at = static_cast<std::size_t>(pos);
|
||||
const auto n = static_cast<std::size_t>(chunk);
|
||||
if (channels == 2) engine.render(left.data() + at, right.data() + at, n);
|
||||
else engine.render(left.data() + at, n);
|
||||
pos += chunk;
|
||||
}
|
||||
|
||||
out.channelCount = channels;
|
||||
out.sampleRate = plan.sampleRate;
|
||||
out.interleaved.resize(total * static_cast<std::size_t>(channels));
|
||||
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];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::bake
|
||||
Reference in New Issue
Block a user