Print the limiter through the bake's master stage, compensating its lookahead so an engaged bake is the approved sound and a bypassed one is unchanged
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/instrument/engine/limiter.h"
|
||||
#include "core/instrument/engine/voice_engine.h"
|
||||
|
||||
namespace reasampler::instrument::bake {
|
||||
@@ -18,7 +19,8 @@ constexpr std::int64_t kBlockFrames = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainLinear) {
|
||||
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
|
||||
@@ -36,9 +38,16 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainL
|
||||
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, 0.f);
|
||||
std::vector<AudioSample> right(channels == 2 ? rendered : 0u, 0.f);
|
||||
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.
|
||||
@@ -69,20 +78,39 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainL
|
||||
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);
|
||||
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));
|
||||
// Printed here rather than left for the processor: resetAfterBake hands master gain
|
||||
// back to unity, so a render that only summed voices would return every iteration
|
||||
// shifted by 1/gain, and a gain dialed to silence would come back at full level. 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[lead + f] * gain;
|
||||
if (channels == 2) out.interleaved[f * channels + 1] = right[lead + f] * gain;
|
||||
out.interleaved[f * channels] = left[lead + f];
|
||||
if (channels == 2) out.interleaved[f * channels + 1] = right[lead + f];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user