Γ-W1-T2: the master bus — a true-peak limiter whose ceiling is a theorem, the meter's published half, and the plugin's first PDC report

This commit is contained in:
2026-08-01 19:05:57 -04:00
parent 4fa021edae
commit 3baf4ee50b
17 changed files with 1187 additions and 65 deletions
+61 -6
View File
@@ -20,6 +20,7 @@
#include "shell/instrument/reaper_bridge.h"
#include "core/instrument/map/sample_map.h" // InstrumentParams (the one parameter set)
#include "core/instrument/map/component_state_io.h" // ComponentState codec
#include "core/instrument/engine/limiter.h" // the master bus's post-gain limiter
#include "core/instrument/engine/live_params.h" // LiveParams (the live-parameter block)
#include "core/instrument/engine/voice_engine.h"
@@ -33,6 +34,16 @@ using instrument::map::kPreviewVelocityDefault;
class ReaSamplerEmbed; // embedded TCP/MCP UI shell (owned below; see queryInterface)
// What the audio thread publishes about the OUTPUT BUS, post-limiter, once per block. Raw
// magnitudes only — the UI converts to dB and runs the ballistics (engine/meter_ballistics),
// because a hold timer or a log on the audio thread would be per-block work that buys nothing.
struct MasterBusMeter {
float peakL = 0.f; // max |x| this block
float peakR = 0.f;
float minGain = 1.f; // smallest limiter gain applied this block; 1 = no reduction
bool clip = false; // LATCHED at a block peak >= 0 dBFS; only clearMasterBusClip lowers it
};
// The decoded capture + the voice engine playing it. The engine holds a reference to the
// sample, so both must live/die together at a stable address — heap-allocated,
// non-copyable, non-movable. process() only ever reads this through an atomic pointer.
@@ -92,6 +103,12 @@ public:
Steinberg::tresult PLUGIN_API process(
Steinberg::Vst::ProcessData& data) override;
// The plugin's PDC report: 0 with the limiter bypassed, the limiter's lookahead with it
// engaged. Read from the PERSISTED enable, never from a transient — the SDK's contract
// (pluginterfaces/vst/ivsteditcontroller.h, kLatencyChanged) is that the host asks this
// AFTER the deactivate/reactivate it performs, and setActive(false) clears the engine.
Steinberg::uint32 PLUGIN_API getLatencySamples() override;
// Fixed stereo output bus — channel mode is a decode policy, never a bus fact; mono
// renders dual-mono through it. Do not reintroduce per-instance bus renegotiation.
// Accepts only a single stereo output proposal; otherwise rejects and keeps stereo.
@@ -108,12 +125,18 @@ public:
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
void** obj) override;
// The embedded-strip activity level (0..1) for the embed shell, UI thread. Backed by
// embedPeak_, a lock-free relaxed atomic the audio thread writes each block.
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The loudest of
// the two published bus peaks — one publication serves the strip and the meter.
double embedActivityLevel() const {
return static_cast<double>(embedPeak_.load(std::memory_order_relaxed));
const float l = meterPeakL_.load(std::memory_order_relaxed);
const float r = meterPeakR_.load(std::memory_order_relaxed);
return static_cast<double>(l > r ? l : r);
}
// What the audio thread published about the output bus last block. UI thread.
MasterBusMeter masterBusMeter() const;
void clearMasterBusClip();
// Resolves the selection against the instance-owned SampleRefs, decodes its WAV
// off-thread, and publishes the built instrument via atomic swap — no bank read
// required. When the bank blob is readable it's first folded into the refs table
@@ -208,6 +231,15 @@ public:
}
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
// The master-bus limiter's single enable (persisted in the parameter set). UI thread only:
// the setter requests the host's kLatencyChanged restart, which the SDK requires be issued
// from the UI thread and which process() must therefore never trigger. Setting the value it
// already holds is a no-op, so repeated clicks on one segment cost no restart.
bool limiterEnabled() const {
return limiterEnabled_.load(std::memory_order_relaxed);
}
void setLimiterEnabled(bool on);
// Fires a one-shot preview note-on/off through the live VoiceEngine — the same
// noteOn/noteOff host MIDI uses, so a preview is a real voice (counts against voice
// count, can steal/be stolen, respects Poly/Mono + Retrigger/Legato). Off the audio
@@ -248,6 +280,19 @@ private:
// Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
// Publishes a silent block to the meter. EVERY process() path that emits no audio calls
// this, or the bar freezes at the last peak it saw. The clip latch is deliberately not
// touched — it survives silence until the user clears it.
void publishSilentMeterBlock() {
meterPeakL_.store(0.f, std::memory_order_relaxed);
meterPeakR_.store(0.f, std::memory_order_relaxed);
meterMinGain_.store(1.f, std::memory_order_relaxed);
}
// Mirrors the persisted limiter enable onto the audio thread and the latency reader. Called
// from every writer of the parameter set, so the three views can never disagree.
void publishLimiterEnabled(bool on);
// Publishes this instance's held captures to its per-instance ext-state key
// ("rsusage_<instanceGuid>") so the extension's prune can never reclaim them. Called at
// the tail of every reloadInstrument, off the audio thread. Mints instanceGuid_ on
@@ -401,9 +446,19 @@ private:
// unique_ptr, so its own refcount is a no-op.
std::unique_ptr<ReaSamplerEmbed> embed_;
// Per-block mono peak the audio thread stores relaxed; embedActivityLevel() reads it
// for the embed strip's level indicator. Advisory only.
std::atomic<float> embedPeak_{0.f};
// The master-bus limiter, applied post-gain over the summed output. Its own enable target
// is the mirror of params_.limiterEnabled; limiterEnabled_ is the lock-free copy
// getLatencySamples answers from.
instrument::engine::Limiter limiter_;
std::atomic<bool> limiterEnabled_{false};
// What the audio thread publishes about the output bus each block, relaxed — peaks, the
// latched clip, and the limiter's smallest gain. No dB, no ballistics, no hold timer here;
// the UI runs those off these values and its own elapsed time.
std::atomic<float> meterPeakL_{0.f};
std::atomic<float> meterPeakR_{0.f};
std::atomic<float> meterMinGain_{1.f};
std::atomic<bool> meterClip_{false};
};
} // namespace reasampler::vst