bfaa0f2614
42 of 44 ids issued: pitch key-track and Trigger length stay reserved pending a live path. Master gain reclassified Live — it never reloaded.
385 lines
18 KiB
C++
385 lines
18 KiB
C++
// processor_state.cpp — ReaSamplerProcessor's component-state I/O (setState/getState
|
|
// against the component_state_io codec) and its UI-thread parameter accessors/setters
|
|
// (selection, the one parameter set, channel mode, preview velocity, voice-system params,
|
|
// master gain, preview-note mailbox posts). Everything here runs off the audio thread;
|
|
// setters hand work to the reload family (processor_reload.cpp) or store atomics
|
|
// process() picks up at block start.
|
|
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <vector>
|
|
|
|
#include "pluginterfaces/base/ibstream.h"
|
|
#include "pluginterfaces/vst/ivsteditcontroller.h" // RestartFlags::kLatencyChanged
|
|
|
|
#include "core/instrument/engine/master_gain.h" // masterGainMaxLinear (post-mixer gain clamp)
|
|
#include "core/instrument/map/component_state_io.h" // the ComponentState codec
|
|
#include "core/instrument/map/sample_map.h" // retainRefs / referencedSampleIds
|
|
|
|
using namespace Steinberg;
|
|
using namespace Steinberg::Vst;
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using namespace instrument::map; // the codec + resolution vocabulary this TU marshals
|
|
using instrument::engine::masterGainMaxLinear; // taper ceiling
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
|
if (!state) return kResultFalse;
|
|
// The blob is small; read it in one shot into a growable buffer.
|
|
std::vector<std::uint8_t> bytes;
|
|
std::uint8_t chunk[256];
|
|
int32 got = 0;
|
|
while (state->read(chunk, sizeof(chunk), &got) == kResultOk && got > 0) {
|
|
bytes.insert(bytes.end(), chunk, chunk + got);
|
|
}
|
|
// Component state is {loaded capture id, one parameter set}. deserializeComponentState
|
|
// lifts older blobs cleanly — including the retired zone payloads, which adopt zone
|
|
// one's capture into cs.selectionId. sampleRate_ is the real host rate here (REAPER
|
|
// calls setupProcessing before setState on load), which the legacy v3 payload's
|
|
// frames->seconds conversion needs.
|
|
const ComponentState cs = deserializeComponentState(bytes, sampleRate_);
|
|
// A LOAD is not an edit: the SDK is explicit that a controller must never pass a restored
|
|
// value back to the host through IComponentHandler. The push into the controller happens
|
|
// once at the tail instead, through syncParamsFromModel.
|
|
paramNotifySuppressed_ = true;
|
|
setSelectedSampleId(cs.selectionId);
|
|
setInstrumentParams(cs.params);
|
|
// Restore the last-consumed assignment generation so a re-open does not re-apply a
|
|
// stale assign_request.
|
|
{
|
|
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
|
|
lastConsumedAssignGeneration_ = cs.lastConsumedAssignGeneration;
|
|
}
|
|
// The output bus is fixed stereo (see initialize) — the mode only governs decode below.
|
|
{
|
|
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
|
channelMode_ = cs.channelMode;
|
|
channelModeExplicit_ = cs.channelModeExplicit;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(previewMutex_);
|
|
previewVelocity_ = cs.previewVelocity;
|
|
}
|
|
// Restore before the reload so the rebuilt engine is born with the saved polyphony/mode.
|
|
{
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
voiceCount_ = cs.voiceCount;
|
|
voiceMode_ = cs.voiceMode;
|
|
monoTrigger_ = cs.monoTrigger;
|
|
}
|
|
setMasterGainLinear(cs.masterGainLinear);
|
|
// Restore the instance-owned sample refs before the reload so it decodes straight from
|
|
// them — no bank read required. A pre-v10 blob lifts to an empty table; the reload
|
|
// resolves nothing until the bank blob becomes readable (opportunistic refresh, or
|
|
// pollBankSync's legacy lift), after which the next save is self-contained.
|
|
{
|
|
std::lock_guard<std::mutex> lock(refsMutex_);
|
|
sampleRefs_ = cs.sampleRefs;
|
|
}
|
|
// Restore the publish identity (pre-v11 lifts to empty, minted on first publish).
|
|
// usageNonce_ resets: a restored blob is a new lifetime, so this incarnation can never
|
|
// be mistaken for the previous one's writes or a copy-sibling's.
|
|
{
|
|
std::lock_guard<std::mutex> lock(usageMutex_);
|
|
instanceGuid_ = cs.instanceGuid;
|
|
usageNonce_.clear();
|
|
}
|
|
// A new blob is new facts — the legacy lift gets one fresh run per restored state.
|
|
legacyLiftConcluded_.store(false, std::memory_order_relaxed);
|
|
reloadInstrument();
|
|
paramNotifySuppressed_ = false;
|
|
// Every exposed parameter now reads the blob's value. Ordering against the host's first
|
|
// parameter block is irrelevant BY CONSTRUCTION rather than by assumption: there is one
|
|
// model and one funnel per control, so whichever of the two writes last simply wins, and
|
|
// the host's display follows the model either way.
|
|
syncParamsFromModel();
|
|
// This caller has no editor to flush for it. At the TAIL on purpose: a host that services the
|
|
// restart synchronously deactivates/reactivates, and our setActive(true) resumes or reloads
|
|
// against the refs above, which are only fully restored once this function has run to here.
|
|
flushLatencyRestart();
|
|
return kResultOk;
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
|
if (!state) return kResultFalse;
|
|
// Persists the full instance state — never written to the "reasampler" bank ext-state.
|
|
// No pick serializes to {"", default params}, restoring as silence (never auto-playing
|
|
// sample #1).
|
|
ComponentState state_out;
|
|
state_out.selectionId = selectedSampleId();
|
|
state_out.params = instrumentParams();
|
|
{
|
|
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
|
state_out.channelMode = channelMode_;
|
|
state_out.channelModeExplicit = channelModeExplicit_;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
|
|
state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_;
|
|
}
|
|
state_out.previewVelocity = previewVelocity();
|
|
{
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
state_out.voiceCount = voiceCount_;
|
|
state_out.voiceMode = voiceMode_;
|
|
state_out.monoTrigger = monoTrigger_;
|
|
}
|
|
state_out.masterGainLinear = masterGainLinear();
|
|
// Persist the owned sample refs — the saved blob decodes + plays with no extension
|
|
// present. Filtered (snapshot copy only) to what the instance currently plays, so the
|
|
// table cannot grow with browsing history.
|
|
state_out.sampleRefs = sampleRefs();
|
|
retainRefs(state_out.sampleRefs, referencedSampleIds(state_out.selectionId));
|
|
// Persist the publish identity so the usage key is stable across sessions.
|
|
{
|
|
std::lock_guard<std::mutex> lock(usageMutex_);
|
|
state_out.instanceGuid = instanceGuid_;
|
|
}
|
|
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()),
|
|
static_cast<int32>(bytes.size()), nullptr);
|
|
if (wr != kResultOk) return wr;
|
|
}
|
|
return kResultOk;
|
|
}
|
|
|
|
std::string ReaSamplerProcessor::selectedSampleId() {
|
|
std::lock_guard<std::mutex> lock(selectionMutex_);
|
|
return selectedSampleId_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setSelectedSampleId(const std::string& id) {
|
|
std::lock_guard<std::mutex> lock(selectionMutex_);
|
|
selectedSampleId_ = id;
|
|
}
|
|
|
|
InstrumentParams ReaSamplerProcessor::instrumentParams() {
|
|
std::lock_guard<std::mutex> lock(paramsMutex_);
|
|
return params_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
|
|
InstrumentParams before;
|
|
{
|
|
std::lock_guard<std::mutex> lock(paramsMutex_);
|
|
before = params_;
|
|
params_ = params;
|
|
}
|
|
// Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
|
|
// funnels through here, so mirroring the limiter flag at this one point is what keeps the
|
|
// audio thread's copy and the latency report from ever lagging what is persisted. The MIRROR
|
|
// is inline, because that is the sound the user clicked for; the host notification is not,
|
|
// because this funnel is reachable from inside a mouse handler and restartComponent is not
|
|
// safe there (see this directory's CLAUDE.md).
|
|
publishLimiterEnabled(params.limiterEnabled);
|
|
// Armed AFTER the mirror, so getLatencySamples already answers the new value for the whole
|
|
// window the arm stays outstanding. Compared against the last ANNOUNCED enable rather than
|
|
// against the previous parameter set: off->on->off inside one tick ends at the latency the
|
|
// host already knows, and a restart rebuilds the instance, so announcing a latency that
|
|
// never changed is pure cost. Any number of changes before one flush still cost at most one
|
|
// restart, and this store is the only one that raises OR lowers the arm.
|
|
latencyRestartPending_.store(
|
|
params.limiterEnabled != latencyAnnounced_.load(std::memory_order_relaxed),
|
|
std::memory_order_release);
|
|
// The host-notification obligation, at the same one funnel and for the same reason the
|
|
// limiter mirror sits here: an internal write that skipped it would leave the host
|
|
// displaying — and, on the next touch, re-imposing — the superseded value.
|
|
notifyParamsFromModel(before, params);
|
|
}
|
|
|
|
void ReaSamplerProcessor::flushLatencyRestart() {
|
|
// Cleared only once it can actually be delivered — an arm raised before the host connected
|
|
// its handler waits for a later flush instead of evaporating.
|
|
if (!componentHandler) return;
|
|
if (!latencyRestartPending_.exchange(false, std::memory_order_acquire)) return;
|
|
// Latched BEFORE the call: a host that services the restart synchronously re-enters this
|
|
// object inside it, so the next commit must compare against the value the host is about to
|
|
// read, not against the one it held before.
|
|
const bool previouslyAnnounced = latencyAnnounced_.exchange(
|
|
limiterEnabled_.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
|
// The SDK requires this on the UI thread and answers getLatencySamples only after the host's
|
|
// own deactivate/reactivate — so the flag is long committed by the time the host asks. This
|
|
// is a kLatencyChanged restart with the bus untouched, NOT the retired per-mode kIoChanged
|
|
// bus renegotiation (see initialize()); do not conflate.
|
|
if (componentHandler->restartComponent(kLatencyChanged) == kResultOk) return;
|
|
// A refused restart leaves the host's delay compensation on the OLD value, so the latch has
|
|
// to come back off it: announcing a value the host never took would let a later toggle BACK
|
|
// to that value arm nothing, stranding the host's view permanently. Re-armed instead, which
|
|
// costs one retry per drain in a host that always refuses. The SDK documents no refusal
|
|
// semantics, so whether any host returns non-kResultOk here is `[verify — DAW]`.
|
|
latencyAnnounced_.store(previouslyAnnounced, std::memory_order_relaxed);
|
|
latencyRestartPending_.store(true, std::memory_order_release);
|
|
}
|
|
|
|
void ReaSamplerProcessor::publishLimiterEnabled(bool on) {
|
|
limiterEnabled_.store(on, std::memory_order_relaxed);
|
|
limiter_.setEnabled(on);
|
|
}
|
|
|
|
void ReaSamplerProcessor::setLimiterEnabled(bool on) {
|
|
// Rebased off the PROCESSOR's copy rather than taking a caller-supplied set: an editor
|
|
// snapshot may carry edits it has not committed, and writing one back here would clobber
|
|
// them. Everything else is setInstrumentParams', the one funnel every writer agrees through.
|
|
InstrumentParams params = instrumentParams();
|
|
params.limiterEnabled = on;
|
|
setInstrumentParams(params);
|
|
}
|
|
|
|
MasterBusMeter ReaSamplerProcessor::masterBusMeter() {
|
|
MasterBusMeter m;
|
|
// Consuming: each read takes the window and reinstalls its identity element, which is what
|
|
// starts the next one. The audio thread's fold is an unconditional CAS against exactly that
|
|
// (meter_accumulate.h owns the argument), so a fold interleaved with these exchanges lands
|
|
// in one window or the other and is never dropped between them.
|
|
m.peakL = instrument::engine::consumePeak(meterPeakL_);
|
|
m.peakR = instrument::engine::consumePeak(meterPeakR_);
|
|
m.minGain = instrument::engine::consumeMinGain(meterMinGain_);
|
|
// NOT consumed: the clip is a latch the user clears, not a window.
|
|
m.clip = meterClip_.load(std::memory_order_relaxed);
|
|
return m;
|
|
}
|
|
|
|
void ReaSamplerProcessor::clearMasterBusClip() {
|
|
meterClip_.store(false, std::memory_order_relaxed);
|
|
}
|
|
|
|
void ReaSamplerProcessor::publishLiveParams() {
|
|
const int rate = builtSampleRate_.load(std::memory_order_relaxed);
|
|
if (rate <= 0) return;
|
|
const instrument::engine::LiveValues block =
|
|
instrument::engine::foldLive(resolvePlay(instrumentParams().play, rate));
|
|
// livePublishMutex_ enforces the seqlock's single-writer contract (live_params.h) against
|
|
// reloadInstrument's publish — held for the publish call only, not the fold above.
|
|
std::lock_guard<std::mutex> lock(livePublishMutex_);
|
|
liveParams_.publish(block);
|
|
}
|
|
|
|
SampleRefs ReaSamplerProcessor::sampleRefs() {
|
|
std::lock_guard<std::mutex> lock(refsMutex_);
|
|
return sampleRefs_;
|
|
}
|
|
|
|
ChannelMode ReaSamplerProcessor::channelMode() {
|
|
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
|
return channelMode_;
|
|
}
|
|
|
|
std::uint8_t ReaSamplerProcessor::previewVelocity() {
|
|
std::lock_guard<std::mutex> lock(previewMutex_);
|
|
return previewVelocity_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setPreviewVelocity(std::uint8_t velocity) {
|
|
// Clamp to [1,127] — 0 would be a note-off by convention, and a preview strike must sound.
|
|
if (velocity < 1) velocity = 1;
|
|
if (velocity > 127) velocity = 127;
|
|
std::lock_guard<std::mutex> lock(previewMutex_);
|
|
previewVelocity_ = velocity;
|
|
}
|
|
|
|
int ReaSamplerProcessor::voiceCount() {
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
return voiceCount_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setVoiceCount(int count) {
|
|
// Clamp to the shared pure-core range so the engine, state bytes, and editor control
|
|
// can never disagree about the legal polyphony span.
|
|
if (count < kMinVoiceCount) count = kMinVoiceCount;
|
|
if (count > kMaxVoiceCount) count = kMaxVoiceCount;
|
|
{
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
if (voiceCount_ == count) return; // no-op: don't churn a rebuild
|
|
voiceCount_ = count;
|
|
}
|
|
// Light rebuild through the drain-slot swap (no bridge re-read, no WAV re-decode) so a
|
|
// voice-param change never cuts a sounding tail. Same contract below.
|
|
rebuildVoiceEngine();
|
|
}
|
|
|
|
VoiceMode ReaSamplerProcessor::voiceMode() {
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
return voiceMode_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setVoiceMode(VoiceMode mode) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
if (voiceMode_ == mode) return;
|
|
voiceMode_ = mode;
|
|
}
|
|
rebuildVoiceEngine();
|
|
}
|
|
|
|
MonoTrigger ReaSamplerProcessor::monoTrigger() {
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
return monoTrigger_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(voiceParamsMutex_);
|
|
if (monoTrigger_ == trigger) return;
|
|
monoTrigger_ = trigger;
|
|
}
|
|
rebuildVoiceEngine();
|
|
}
|
|
|
|
void ReaSamplerProcessor::setMasterGainLinear(double linear) {
|
|
// Clamp to the master_gain taper (0 = silence, cap = +24 dB). One relaxed atomic
|
|
// store — no rebuild, no lock (a post-sum 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;
|
|
const float value = static_cast<float>(linear);
|
|
const float previous = masterGain_.exchange(value, std::memory_order_relaxed);
|
|
// Gain's own notification funnel — it is the one exposed control that does not ride the
|
|
// parameter set, so setInstrumentParams' diff cannot see it. Compared for a real change so a
|
|
// reload's republish of an unmoved gain writes nothing into a host's automation lane.
|
|
if (paramNotifySuppressed_ || previous == value) return;
|
|
notifyParamChanged(instrument::param::kParamMasterGain,
|
|
instrument::engine::masterGainNormFromLinear(linear));
|
|
}
|
|
|
|
void ReaSamplerProcessor::previewNoteOn(int note) {
|
|
if (note < 0) note = 0;
|
|
if (note > 127) note = 127;
|
|
const std::uint8_t vel = previewVelocity(); // latch the current knob value into the request
|
|
// Advance the sequence (wrapping; process compares for inequality — 16 bits gives 65535
|
|
// posts between collisions, unreachable at UI-click rates).
|
|
const std::uint16_t seq = ++previewOnSeq_ == 0 ? ++previewOnSeq_ : previewOnSeq_;
|
|
const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
|
|
(static_cast<std::uint32_t>(vel) << 8) |
|
|
static_cast<std::uint32_t>(note & 0xFF);
|
|
previewOnRequest_.store(packed, std::memory_order_release);
|
|
}
|
|
|
|
void ReaSamplerProcessor::previewNoteOff(int note) {
|
|
if (note < 0) note = 0;
|
|
if (note > 127) note = 127;
|
|
const std::uint16_t seq = ++previewOffSeq_ == 0 ? ++previewOffSeq_ : previewOffSeq_;
|
|
const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
|
|
static_cast<std::uint32_t>(note & 0xFF);
|
|
previewOffRequest_.store(packed, std::memory_order_release);
|
|
}
|
|
|
|
void ReaSamplerProcessor::setChannelMode(ChannelMode mode) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
|
// A deliberate choice either way: latch explicit even on a same-mode click so
|
|
// auto-default stops fighting it.
|
|
channelModeExplicit_ = true;
|
|
if (channelMode_ == mode) return; // no decode change: don't churn a reload
|
|
channelMode_ = mode;
|
|
}
|
|
// The output bus is fixed stereo (no bus repoint): reloading re-decodes off-thread
|
|
// under the new mode and the RT path just keeps rendering.
|
|
reloadInstrument();
|
|
}
|
|
|
|
} // namespace reasampler::vst
|