Γ-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:
@@ -11,7 +11,8 @@ The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`,
|
||||
`sample_map`, `component_state_io`, `play_params.h`, `editor_geometry`, `sample_bands`,
|
||||
`sample_chrome`, `keyboard_strip`, `waveform_view`, `capture_browser`, `browser_scroll`,
|
||||
`param_slider`, `param_taper`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
|
||||
`deck_groups`, `deck_values`, `bake_hold`, `curve_popup`, `spline_edit`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`deck_groups`, `deck_values`, `bake_hold`, `curve_popup`, `spline_edit`, `master_gain`,
|
||||
`limiter`, `meter_ballistics`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`core/wire` and is documented there — this directory consumes it but does not own it.
|
||||
|
||||
## Invariants
|
||||
@@ -104,7 +105,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
## Modules
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. Both PROVE the write by reading the key back (`wire::extStateWriteLanded`) — `SetProjExtState`'s own return cannot speak for one key, so testing it was a guard that could never fire, and the bake's "could not publish" refusal was consequently unreachable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. **The master bus:** the summed output runs `voice mixer → master gain → limiter (core/instrument/engine/limiter) → output bus`, with the meter tapped at the bus output POST-limiter and published per block as relaxed atomics (per-channel peak, latched clip, the block's smallest limiter gain). The limiter's enable is persisted in the parameter set (params payload v14) and mirrored onto the audio thread by `setInstrumentParams`, the single funnel every writer already goes through. That mirror is also what `getLatencySamples()` answers from — the plugin's FIRST latency reporting: 0 bypassed, the lookahead engaged. `setLimiterEnabled` requests the host's `restartComponent(kLatencyChanged)`, UI thread only and never from `process()`; it is a LATENCY restart with the bus untouched, NOT the retired per-mode `kIoChanged` bus renegotiation the invariant above forbids.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
|
||||
@@ -89,7 +89,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
waveform_view bank_sync browser_scroll param_slider tooltip
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||
knob_deck deck_groups deck_values curve_popup spline_edit master_gain sample_usage
|
||||
bake_hold
|
||||
limiter meter_ballistics bake_hold
|
||||
file_bytes curve_law stroke_aa
|
||||
curve_tessellate
|
||||
bake_plan bake_render bake_reset bake_wire wav_codec)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#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
|
||||
@@ -148,8 +149,46 @@ InstrumentParams ReaSamplerProcessor::instrumentParams() {
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
|
||||
std::lock_guard<std::mutex> lock(paramsMutex_);
|
||||
params_ = params;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(paramsMutex_);
|
||||
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.
|
||||
publishLimiterEnabled(params.limiterEnabled);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishLimiterEnabled(bool on) {
|
||||
limiterEnabled_.store(on, std::memory_order_relaxed);
|
||||
limiter_.setEnabled(on);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::setLimiterEnabled(bool on) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(paramsMutex_);
|
||||
if (params_.limiterEnabled == on) return; // no change: no restart to request
|
||||
params_.limiterEnabled = on;
|
||||
}
|
||||
publishLimiterEnabled(on);
|
||||
// The SDK requires this on the UI thread and answers getLatencySamples only after the host's
|
||||
// own deactivate/reactivate — so the flag above is already 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 the two.
|
||||
if (componentHandler) componentHandler->restartComponent(kLatencyChanged);
|
||||
}
|
||||
|
||||
MasterBusMeter ReaSamplerProcessor::masterBusMeter() const {
|
||||
MasterBusMeter m;
|
||||
m.peakL = meterPeakL_.load(std::memory_order_relaxed);
|
||||
m.peakR = meterPeakR_.load(std::memory_order_relaxed);
|
||||
m.minGain = meterMinGain_.load(std::memory_order_relaxed);
|
||||
m.clip = meterClip_.load(std::memory_order_relaxed);
|
||||
return m;
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::clearMasterBusClip() {
|
||||
meterClip_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishLiveParams() {
|
||||
|
||||
@@ -95,6 +95,11 @@ tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) {
|
||||
// project's ext-state parses, nothing retries until the next activation or editor
|
||||
// tick — open a pre-v10 instrument once after upgrading if it restores silent.
|
||||
reloadInstrument();
|
||||
// The host performs this deactivate/reactivate whenever it acts on a kLatencyChanged
|
||||
// request, so the limiter starts each activation with an empty delay line and snapped
|
||||
// to its persisted state — no crossfade, because there is nothing sounding to be
|
||||
// continuous with once the block above has destroyed every voice.
|
||||
limiter_.reset();
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
// Free EVERYTHING, including live_: its voices are frozen mid-flight, and if it
|
||||
@@ -108,6 +113,12 @@ tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) {
|
||||
return kResultOk;
|
||||
}
|
||||
|
||||
uint32 PLUGIN_API ReaSamplerProcessor::getLatencySamples() {
|
||||
if (!limiterEnabled_.load(std::memory_order_relaxed)) return 0;
|
||||
return static_cast<uint32>(
|
||||
instrument::engine::limiterLookaheadSamples(sampleRate_));
|
||||
}
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) {
|
||||
sampleRate_ = setup.sampleRate;
|
||||
maxBlockSize_ = setup.maxSamplesPerBlock;
|
||||
@@ -116,6 +127,8 @@ tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) {
|
||||
if (sampleRate_ > 0.0) {
|
||||
gainRampStep_ = static_cast<float>(1.0 / (kGainRampSeconds * sampleRate_));
|
||||
}
|
||||
// Every limiter allocation and transcendental happens here, off the audio thread.
|
||||
limiter_.prepare(sampleRate_);
|
||||
return SingleComponentEffect::setupProcessing(setup);
|
||||
}
|
||||
|
||||
@@ -238,7 +251,7 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
}
|
||||
|
||||
if (data.numOutputs <= 0 || !data.outputs || data.numSamples <= 0) {
|
||||
embedPeak_.store(0.f, std::memory_order_relaxed);
|
||||
publishSilentMeterBlock();
|
||||
return kResultOk;
|
||||
}
|
||||
AudioBusBuffers& out = data.outputs[0];
|
||||
@@ -247,7 +260,7 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// 64-bit host processing is not supported by the mono float core; emit silence
|
||||
// rather than mis-render. REAPER runs 32-bit float by default.
|
||||
if (data.symbolicSampleSize != kSample32) {
|
||||
embedPeak_.store(0.f, std::memory_order_relaxed);
|
||||
publishSilentMeterBlock();
|
||||
for (int32 ch = 0; ch < out.numChannels; ++ch) {
|
||||
if (double* buf = out.channelBuffers64[ch]) {
|
||||
for (int32 i = 0; i < frames; ++i) buf[i] = 0.0;
|
||||
@@ -294,21 +307,26 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The chain's last stage before the bus, after the gain above.
|
||||
const float minGain = limiter_.process(ch0, ch1, frames);
|
||||
meterMinGain_.store(minGain, std::memory_order_relaxed);
|
||||
// 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]) {
|
||||
for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i];
|
||||
}
|
||||
}
|
||||
// Block peak (max across L/R) for the embed strip's level indicator.
|
||||
float peak = 0.f;
|
||||
// Meter tap: the bus output, post-limiter. Raw per-channel block peaks only.
|
||||
float peakL = 0.f, peakR = 0.f;
|
||||
for (int32 i = 0; i < frames; ++i) {
|
||||
const float a0 = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
||||
const float a1 = ch1[i] < 0.f ? -ch1[i] : ch1[i];
|
||||
if (a0 > peak) peak = a0;
|
||||
if (a1 > peak) peak = a1;
|
||||
if (a0 > peakL) peakL = a0;
|
||||
if (a1 > peakR) peakR = a1;
|
||||
}
|
||||
embedPeak_.store(peak, std::memory_order_relaxed);
|
||||
meterPeakL_.store(peakL, std::memory_order_relaxed);
|
||||
meterPeakR_.store(peakR, std::memory_order_relaxed);
|
||||
if (peakL >= 1.f || peakR >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||
} else if (ch0) {
|
||||
// Mono: render into channel 0, replicate to any extra channels (defensive).
|
||||
for (int32 i = 0; i < frames; ++i) ch0[i] = 0.f;
|
||||
@@ -335,17 +353,23 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const float minGain = limiter_.process(ch0, nullptr, frames);
|
||||
meterMinGain_.store(minGain, std::memory_order_relaxed);
|
||||
float peak = 0.f;
|
||||
for (int32 i = 0; i < frames; ++i) {
|
||||
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
||||
if (a > peak) peak = a;
|
||||
}
|
||||
embedPeak_.store(peak, std::memory_order_relaxed);
|
||||
meterPeakL_.store(peak, std::memory_order_relaxed);
|
||||
meterPeakR_.store(peak, std::memory_order_relaxed);
|
||||
if (peak >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
|
||||
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
||||
if (float* buf = out.channelBuffers32[ch]) {
|
||||
for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
publishSilentMeterBlock();
|
||||
}
|
||||
|
||||
// Report silence only when nothing is loaded (lets the host optimize when idle); with
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user