Restore the bank fold and usage publish to the resume path, guard setActive against repeats, and make the meter fold's bound literal

The resume also hands back to a full reload when the fold moves the loaded capture's decode source, so the refs table and the audio cannot skew.
This commit is contained in:
2026-08-02 13:15:47 -04:00
parent 5c6525fb91
commit da14509ab5
15 changed files with 218 additions and 76 deletions
+18 -15
View File
@@ -1,8 +1,8 @@
// 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.
// thread's per-block path. The folds are templated on the accumulator ONLY so the
// drain-inside-the-fold interleave below can be pinned deterministically instead of raced for.
#pragma once
@@ -30,36 +30,39 @@ inline constexpr float kMeterGainIdentity = 1.f;
// 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.
// exit however the two interleave. STRONG, so the loop is bounded by the interference it is
// written against: the audio thread is the only writer besides the UI's single consume, and
// weak's permitted spurious failure would make an unbounded retry count reachable with no
// interference at all. Three calls per block, so the strong form costs nothing measurable.
// Relaxed throughout: the accumulators are advisory and order no other state. `Accumulator` is
// templated only so a test can pin the interleave; it must behave as std::atomic<float>.
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)) {
while (!acc.compare_exchange_strong(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)) {
while (!acc.compare_exchange_strong(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) {
// Concrete: only the folds have the interleave a test seam buys, and a template over one
// instantiation models nothing.
inline float consumePeak(std::atomic<float>& acc) {
return acc.exchange(kMeterPeakIdentity, std::memory_order_relaxed);
}
template <class Accumulator>
inline float consumeMinGain(Accumulator& acc) {
inline float consumeMinGain(std::atomic<float>& acc) {
return acc.exchange(kMeterGainIdentity, std::memory_order_relaxed);
}
+6
View File
@@ -104,6 +104,12 @@ void refreshRefsFromBank(SampleRefs& refs, const std::string& banksJson,
}
}
bool sameDecodeSource(const SelectedSample& a, const SelectedSample& b) {
return a.relativePath == b.relativePath && a.rootNote == b.rootNote &&
a.channelCount == b.channelCount && a.loop.hasLoop == b.loop.hasLoop &&
a.loop.start == b.loop.start && a.loop.end == b.loop.end;
}
LegacyLiftDecision legacyLiftDecision(const std::optional<std::string>& banksJson,
const std::vector<std::string>& ids) {
if (!banksJson || banksJson->empty()) return LegacyLiftDecision::Retry;
+7
View File
@@ -85,6 +85,13 @@ std::vector<std::string> referencedSampleIds(const std::string& selectionId);
void refreshRefsFromBank(SampleRefs& refs, const std::string& banksJson,
const std::vector<std::string>& ids);
// True when two refs would build the same SampleData: path plus every intrinsic
// resolveCapture folds. displayName is excluded on purpose — it is a label, never a decode
// input. Exists so a caller holding an ALREADY-DECODED sample can ask whether a refresh moved
// what that sample was decoded from; comparing the fields at the call site instead would go
// stale the first time this struct gains one.
bool sameDecodeSource(const SelectedSample& a, const SelectedSample& b);
// Legacy-lift terminating decision: can a refs lift make progress against this bank blob
// for the ids the instance references?
// * Retry — blob absent/empty/unparseable: not readable yet, keep retrying.
+4 -2
View File
@@ -91,8 +91,10 @@ 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". Raising it hides genuine catches, since the limiter's
// ceiling is only −0.3 dBTP.
// spec asks only for "a small floor". It is bounded on BOTH sides: raising it hides genuine
// catches, since the limiter's ceiling is only −0.3 dBTP; lowering it turns the lamp into a
// "some sample crossed the ceiling" light, because the gain law is ceiling/peak and so reports
// an arbitrarily small reduction for a peak arbitrarily close to the ceiling.
inline constexpr double kGrLampFloorDb = 0.5;
bool grLampLit(const MasterMeterUi& m);