Decouple the instrument reload from VST3 activation, and make the master meter's accumulate exact
This commit is contained in:
@@ -87,3 +87,9 @@ reasampler_test(limiter LINK limiter)
|
||||
|
||||
reasampler_pure_library(meter_ballistics SOURCES meter_ballistics.cpp)
|
||||
reasampler_test(meter_ballistics LINK meter_ballistics)
|
||||
|
||||
# The meter's ACCUMULATE half, beside the ballistics that consume it. Header-only (the folds
|
||||
# sit on the audio thread's per-block path), hence INTERFACE.
|
||||
add_library(meter_accumulate INTERFACE)
|
||||
target_include_directories(meter_accumulate INTERFACE ${REASAMPLER_SRC_DIR})
|
||||
reasampler_test(meter_accumulate LINK meter_accumulate)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// meter_accumulate.h — the master meter's ACCUMULATE half: the audio thread's block-rate fold
|
||||
// into the two windows the UI drains, and the drain that starts the next window. The ballistics
|
||||
// that run on what comes out are meter_ballistics'. Header-only — the folds sit on the audio
|
||||
// thread's per-block path. Templated on the accumulator ONLY so the drain-inside-the-fold
|
||||
// interleave below can be pinned deterministically instead of raced for.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace reasampler::instrument::engine {
|
||||
|
||||
// A lock-backed std::atomic<float> would put a mutex on the audio thread; assert the freedom
|
||||
// rather than assume it.
|
||||
static_assert(std::atomic<float>::is_always_lock_free,
|
||||
"the meter folds run on the audio thread and must be lock-free");
|
||||
|
||||
// The two windows' identity elements: a peak window that has seen nothing reports silence, a
|
||||
// gain window that has seen nothing reports no reduction. They are what a consume reinstalls,
|
||||
// so they live beside the folds rather than at the reader.
|
||||
inline constexpr float kMeterPeakIdentity = 0.f;
|
||||
inline constexpr float kMeterGainIdentity = 1.f;
|
||||
|
||||
// Folds one block's reading into its accumulator — a running max for a peak, a running min for
|
||||
// the limiter's gain — so the ~47 blocks that elapse between two 500 ms UI frames at 48 kHz/512
|
||||
// all reach the meter instead of the one it happened to sample.
|
||||
//
|
||||
// An UNCONDITIONAL read-modify-write, and that is the whole point. The UI's consume is an
|
||||
// exchange that can land between a plain load and its store, and a load-compare-store fold
|
||||
// would then drop the block outright: it decided against storing by comparing with a window the
|
||||
// UI has since taken, so that block's reading enters neither the old window nor the new one.
|
||||
// The CAS retries against whatever the consume left, which makes `acc >= blockPeak` hold on
|
||||
// exit however the two interleave. Bounded — the audio thread is this accumulator's only other
|
||||
// writer, so one interfering consume costs one retry. Relaxed throughout: the accumulators are
|
||||
// advisory and order no other state. Block rate, never per frame.
|
||||
template <class Accumulator>
|
||||
inline void foldPeak(Accumulator& acc, float blockPeak) {
|
||||
float seen = acc.load(std::memory_order_relaxed);
|
||||
while (!acc.compare_exchange_weak(seen, seen > blockPeak ? seen : blockPeak,
|
||||
std::memory_order_relaxed,
|
||||
std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
|
||||
template <class Accumulator>
|
||||
inline void foldMinGain(Accumulator& acc, float blockMinGain) {
|
||||
float seen = acc.load(std::memory_order_relaxed);
|
||||
while (!acc.compare_exchange_weak(seen, seen < blockMinGain ? seen : blockMinGain,
|
||||
std::memory_order_relaxed,
|
||||
std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Takes what the window accumulated and reinstalls the identity element, which IS what starts
|
||||
// the next window — so exactly one reader may consume (the shell's MasterBusMeter states who).
|
||||
template <class Accumulator>
|
||||
inline float consumePeak(Accumulator& acc) {
|
||||
return acc.exchange(kMeterPeakIdentity, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
template <class Accumulator>
|
||||
inline float consumeMinGain(Accumulator& acc) {
|
||||
return acc.exchange(kMeterGainIdentity, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::engine
|
||||
@@ -58,7 +58,7 @@ struct MasterMeterUi {
|
||||
double reductionDb = 0.0; // how far the limiter is pulling gain down; 0 = not working
|
||||
// The lamp's hold, on the SAME principle (and the same window) as the peak tick's: without
|
||||
// it a catch smaller than kMeterFallDbPerSecond x the UI period is fully decayed by the
|
||||
// next frame and the lamp never draws lit at all.
|
||||
// next frame, so the lamp is dark again after the single repaint the catch landed on.
|
||||
double reductionHoldSeconds = 0.0;
|
||||
};
|
||||
|
||||
@@ -91,8 +91,8 @@ bool meterDrawEqual(const MasterMeterUi& a, const MasterMeterUi& b);
|
||||
|
||||
// The lamp reports "the limiter is working", not "a sample grazed the threshold", so it needs
|
||||
// a floor rather than a bare non-zero test. 0.5 dB is a CHOSEN floor, not a measurement — the
|
||||
// spec asks only for "a small floor". Lowering it makes the lamp flicker on limiting too slight
|
||||
// to hear; raising it hides genuine catches, since the limiter's ceiling is only −0.3 dBTP.
|
||||
// spec asks only for "a small floor". Raising it hides genuine catches, since the limiter's
|
||||
// ceiling is only −0.3 dBTP.
|
||||
inline constexpr double kGrLampFloorDb = 0.5;
|
||||
bool grLampLit(const MasterMeterUi& m);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user