fix(vst): master-gain per-sample ramp (no zipper/click); voice-count commits once on release; minor review items

This commit is contained in:
2026-07-27 23:30:27 -04:00
parent 43155cf320
commit e2c73e30d6
7 changed files with 116 additions and 43 deletions
+45 -12
View File
@@ -43,6 +43,13 @@ namespace {
// FIXED so raising the voice count never multiplies shifter CPU past the profiled budget.
constexpr std::size_t kPreserveVoiceCap = 8;
// FB1 post-mixer gain ramp rate (per sample). gainCurrent_ converges to masterGain_ at this
// linear step; it ramps from 0 to unity (or vice versa) in ~20 ms at 48 kHz. The early-out
// (|current - target| < threshold) snaps to the target and avoids the ramp loop on idle blocks.
// kGainRampSnap is the threshold below which we snap to the target (avoids long sub-LSB creep).
constexpr float kGainRampRate = 1.0f / 960.0f; // 960 samples @ 48 kHz ≈ 20 ms
constexpr float kGainRampSnap = kGainRampRate * 0.5f;
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
// any failure — the caller treats an unreadable WAV as "nothing to play".
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
@@ -893,14 +900,28 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
drain->preview.render(ch0, ch1, static_cast<std::size_t>(frames));
}
// FB1 post-mixer master gain: ONE relaxed load per block, applied AFTER the voice sum
// (engine + drain + preview) and BEFORE the extra-channel mirror + peak, so the mirror
// and the level indicator both see the actual output. A cheap multiply — no per-voice
// cost, no alloc, no lock (RT discipline).
// FB1 post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so
// continuous knob drags produce no zipper noise and the true-zero bottom causes no click.
// Applied AFTER the voice sum and BEFORE the extra-channel mirror + peak so both see the
// actual output. Branch-free inner loop; early-out when already at target. RT-safe.
{
const float g = masterGain_.load(std::memory_order_relaxed);
if (g != 1.f) {
for (int32 i = 0; i < frames; ++i) { ch0[i] *= g; ch1[i] *= g; }
const float gTarget = masterGain_.load(std::memory_order_relaxed);
const float diff = gTarget - gainCurrent_;
if (diff < -kGainRampSnap || diff > kGainRampSnap) {
// Ramp toward target: step per sample, then apply the per-sample gain.
for (int32 i = 0; i < frames; ++i) {
const float d = gTarget - gainCurrent_;
if (d > kGainRampRate) gainCurrent_ += kGainRampRate;
else if (d < -kGainRampRate) gainCurrent_ -= kGainRampRate;
else gainCurrent_ = gTarget;
ch0[i] *= gainCurrent_;
ch1[i] *= gainCurrent_;
}
} else {
gainCurrent_ = gTarget;
if (gTarget != 1.f) {
for (int32 i = 0; i < frames; ++i) { ch0[i] *= gTarget; ch1[i] *= gTarget; }
}
}
}
// Any channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
@@ -930,12 +951,24 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
drain->engine.render(ch0, static_cast<std::size_t>(frames));
drain->preview.render(ch0, static_cast<std::size_t>(frames));
}
// FB1 post-mixer master gain (mono path) — same contract as the stereo branch above:
// post-sum, pre-peak/replicate, one relaxed load, RT-safe.
// FB1 post-mixer master gain (mono path) — same ramp contract as the stereo branch:
// post-sum, pre-peak/replicate, per-sample gainCurrent_ ramp toward target, RT-safe.
{
const float g = masterGain_.load(std::memory_order_relaxed);
if (g != 1.f) {
for (int32 i = 0; i < frames; ++i) ch0[i] *= g;
const float gTarget = masterGain_.load(std::memory_order_relaxed);
const float diff = gTarget - gainCurrent_;
if (diff < -kGainRampSnap || diff > kGainRampSnap) {
for (int32 i = 0; i < frames; ++i) {
const float d = gTarget - gainCurrent_;
if (d > kGainRampRate) gainCurrent_ += kGainRampRate;
else if (d < -kGainRampRate) gainCurrent_ -= kGainRampRate;
else gainCurrent_ = gTarget;
ch0[i] *= gainCurrent_;
}
} else {
gainCurrent_ = gTarget;
if (gTarget != 1.f) {
for (int32 i = 0; i < frames; ++i) ch0[i] *= gTarget;
}
}
}
float peak = 0.f;