119 lines
6.1 KiB
C++
119 lines
6.1 KiB
C++
// See bake_render.h.
|
|
|
|
#include "core/instrument/bake/bake_render.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "core/instrument/engine/limiter.h"
|
|
#include "core/instrument/engine/voice_engine.h"
|
|
|
|
namespace reasampler::instrument::bake {
|
|
|
|
namespace {
|
|
|
|
// 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, double masterGainLinear,
|
|
bool limiterEnabled) {
|
|
BakeAudio out;
|
|
if (!sample.playable() || plan.totalFrames <= 0 || plan.sampleRate <= 0) return out;
|
|
// Each field bounded BEFORE the sum: renderFrames() adds them, and a hand-built plan
|
|
// (planBake already bounds both — bake_plan.cpp) could otherwise carry leadInFrames
|
|
// near INT64_MAX and signed-overflow inside the guard meant to catch exactly that.
|
|
if (plan.leadInFrames < 0 || plan.leadInFrames > kMaxBakeFrames ||
|
|
plan.totalFrames > kMaxBakeFrames) {
|
|
return out;
|
|
}
|
|
if (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
|
|
// this SampleData's own play params, which is what the bake is meant to print.
|
|
sample.live = nullptr;
|
|
|
|
const int channels = sample.channelCount();
|
|
// The limiter delays its output by its lookahead, so the buffers carry that many extra
|
|
// frames and the window is read that far in — the file is the same frames it would be
|
|
// with the limiter bypassed, not the capture shifted late by 2 ms. The extra input is
|
|
// SILENCE rather than more rendered audio: the file ends at the window, so a peak past
|
|
// it is not in the capture and must not duck the frames that are.
|
|
const auto flushFrames = static_cast<std::size_t>(
|
|
limiterEnabled ? engine::limiterLookaheadSamples(plan.sampleRate) : 0);
|
|
const auto rendered = static_cast<std::size_t>(plan.renderFrames());
|
|
std::vector<AudioSample> left(rendered + flushFrames, 0.f);
|
|
std::vector<AudioSample> right(channels == 2 ? rendered + flushFrames : 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.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. 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);
|
|
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;
|
|
}
|
|
|
|
// The whole master stage is printed here rather than left for the processor, in the
|
|
// processor's own order — gain, then the limiter — because resetAfterBake hands both
|
|
// controls back neutral: a render that only summed voices would return every iteration
|
|
// shifted by 1/gain and unlimited, and a gain dialed to silence would come back at full
|
|
// level. A flat gain 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 (AudioSample& s : left) s *= gain;
|
|
for (AudioSample& s : right) s *= gain;
|
|
|
|
if (limiterEnabled) {
|
|
engine::Limiter limiter;
|
|
// Enabled BEFORE prepare, whose reset snaps to the enable target: that starts the
|
|
// render already engaged. Enabling afterwards takes process()'s live-engage path,
|
|
// which mutes for the delay-line prime and then fades in — silencing the head of the
|
|
// capture. prepare()'s allocation and transcendentals are legal here: the bake runs
|
|
// on the UI thread, never in process().
|
|
limiter.setEnabled(true);
|
|
limiter.prepare(plan.sampleRate);
|
|
// One call: kMaxBakeFrames bounds the whole buffer well inside int, and a block
|
|
// split would change nothing (the limiter carries its state across calls).
|
|
limiter.process(left.data(), channels == 2 ? right.data() : nullptr,
|
|
static_cast<int>(left.size()));
|
|
}
|
|
|
|
out.channelCount = channels;
|
|
out.sampleRate = plan.sampleRate;
|
|
const auto lead = static_cast<std::size_t>(plan.leadInFrames) + flushFrames;
|
|
const auto total = static_cast<std::size_t>(plan.totalFrames);
|
|
out.interleaved.resize(total * static_cast<std::size_t>(channels));
|
|
for (std::size_t f = 0; f < total; ++f) {
|
|
out.interleaved[f * channels] = left[lead + f];
|
|
if (channels == 2) out.interleaved[f * channels + 1] = right[lead + f];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace reasampler::instrument::bake
|