Phase S FB1 (r11): Sample-view recomposition — knob deck + elastic full-width hero + curve popup w/ right-click delete + post-mixer master gain (ComponentState v8)

This commit is contained in:
2026-07-27 23:08:04 -04:00
parent 27b2661ef2
commit 43155cf320
17 changed files with 1942 additions and 324 deletions
+34
View File
@@ -22,6 +22,7 @@
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract)
#include "master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp)
#include "reasampler_editor.h"
#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there)
#include "sample_map.h" // selectSample, resolvePerformance, buildZonedKeymap, state (de)ser
@@ -224,6 +225,10 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
voiceMode_ = cs.voiceMode;
monoTrigger_ = cs.monoTrigger;
}
// FB1: restore the post-mixer master gain (v8; older blobs lift to unity in
// deserializeComponentState — pre-FB1 output). One atomic store; the audio thread picks
// it up at the next block start.
setMasterGainLinear(cs.masterGainLinear);
// Rebuild from the restored state (off-thread — setState is a load-time call).
reloadFromBank();
return kResultOk;
@@ -252,6 +257,7 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
state_out.voiceMode = voiceMode_;
state_out.monoTrigger = monoTrigger_;
}
state_out.masterGainLinear = masterGainLinear(); // FB1: persist the post-mixer gain (v8)
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
if (!bytes.empty()) {
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
@@ -351,6 +357,16 @@ void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
rebuildVoiceEngine();
}
void ReaSamplerProcessor::setMasterGainLinear(double linear) {
// Clamp to the control's legal span (the master_gain taper: 0 = -inf/silence, cap =
// +24 dB). One relaxed atomic store — the audio thread reads it at the next block start;
// no rebuild, no lock (a post-sum output trim is not a keymap fact).
if (!(linear >= 0.0)) linear = 0.0; // also catches NaN
const double maxLin = masterGainMaxLinear();
if (linear > maxLin) linear = maxLin;
masterGain_.store(static_cast<float>(linear), std::memory_order_relaxed);
}
void ReaSamplerProcessor::previewNoteOn(int note) {
if (note < 0) note = 0;
if (note > 127) note = 127;
@@ -877,6 +893,16 @@ 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).
{
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; }
}
}
// Any channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
for (int32 ch = 2; ch < out.numChannels; ++ch) {
if (float* buf = out.channelBuffers32[ch]) {
@@ -904,6 +930,14 @@ 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.
{
const float g = masterGain_.load(std::memory_order_relaxed);
if (g != 1.f) {
for (int32 i = 0; i < frames; ++i) ch0[i] *= g;
}
}
float peak = 0.f;
for (int32 i = 0; i < frames; ++i) {
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];