Tidy reasampler_processor.h ceiling, restamp contour publish, tighten pitch-shift test bound

Splits BuiltCapture/sourceRateRefusalText into processor_snapshot.h; fixes stale lock-nesting notes; restamp now also publishes liveContours_ like reload; tightens a slack test bound and one drifted comment.
This commit is contained in:
2026-09-25 14:29:43 -04:00
parent 648fa515ad
commit 30bc5a9e77
7 changed files with 40 additions and 26 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ struct SpliceEvent {
std::int64_t jump = 0; // the clamped nominal jump actually applied (signed)
std::int64_t lag = 0; // correlation best integer lag
double frac = 0.0; // parabolic sub-sample refinement, [-0.5, 0.5]
std::int64_t fadeLen = 0; // live (ratio-scaled) crossfade length chosen
std::int64_t fadeLen = 0; // live crossfade length chosen (drift-scaled, either direction)
};
// The nominal splice jump for a source whose period is known: the multiple of `periodFrames`
+3 -2
View File
@@ -247,8 +247,9 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
`LoadedInstrument`) and the automation lane's own state. All three are split out —
`processor_snapshot.h` and `automation_channel.h`, on the same grounds `editor_interaction.h`
was split out of `reasampler_editor.h` in this directory: neither is behaviour. What remains is
the class declaration itself, a handful of lines over the ceiling since the live-contour block
(`liveContours_` + `publishLiveContours`) landed beside `liveParams_` under the same ordering
the class declaration itself, 630 lines (measured post-split) — over the ceiling since the
live-contour block (`liveContours_` + `publishLiveContours`) landed beside `liveParams_` under
the same ordering
constraint. No further seam exists that isn't behaviour: every remaining aggregate-shaped chunk
(the preview mailbox, the drain-slot triple) is read and written inline by `process()` on the
audio thread, so splitting it into its own header would either duplicate the RT-discipline
+1 -1
View File
@@ -128,7 +128,7 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
// The snapshot comes first: the default program's window is derived from the sound it
// carries (a Gate release, a Trigger play span), not from a constant.
ReaSamplerProcessor::BuiltCapture built = processor.bakeSnapshot(sampleRate);
BuiltCapture built = processor.bakeSnapshot(sampleRate);
if (built.rateRefused) return fail("cannot render: " + sourceRateRefusalText());
if (!built.sample) return fail("the loaded capture could not be decoded for the render");
SampleData& snapshot = *built.sample;
+8 -4
View File
@@ -80,7 +80,7 @@ std::optional<DecodedPcm> decodeRelative(const std::string& projectDir,
// The ONE decode + build the reload and the bake snapshot share, so a baked render can
// never be built from a different fold than the one the user is hearing.
ReaSamplerProcessor::BuiltCapture buildFromRef(const SelectedSample& sel,
BuiltCapture buildFromRef(const SelectedSample& sel,
const InstrumentParams& params,
const std::string& projectDir,
ChannelMode mode, int renderRate) {
@@ -172,8 +172,9 @@ std::string ReaSamplerProcessor::reloadInstrument() {
// collapses every automated envelope stage to zero frames.
builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed);
{
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here;
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here, the
// same nesting restampHostRateLocked takes under its own reloadMutex_-held
// callers; publishLiveParams never holds reloadMutex_.
std::lock_guard<std::mutex> lp(livePublishMutex_);
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
// The model's contours, so a reload-tier contour edit (a point added, deleted or
@@ -206,7 +207,7 @@ std::string ReaSamplerProcessor::usageInstanceGuid() {
return instanceGuid_;
}
ReaSamplerProcessor::BuiltCapture ReaSamplerProcessor::bakeSnapshot(int renderRate) {
BuiltCapture ReaSamplerProcessor::bakeSnapshot(int renderRate) {
const std::string selId = selectedSampleId();
if (selId.empty()) return {};
const InstrumentParams params = instrumentParams();
@@ -443,6 +444,9 @@ bool ReaSamplerProcessor::restampHostRateLocked(SampleData& sample, int renderRa
builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed);
std::lock_guard<std::mutex> lp(livePublishMutex_);
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
// Symmetric with reloadInstrument's own publish pair: a re-stamp is a rebuild too, so the
// drain's sounding voices need the model's contours refreshed beside the live params.
liveContours_.publish(instrument::engine::foldContours(sample.play));
return true;
}
+16
View File
@@ -7,11 +7,27 @@
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include "core/instrument/engine/voice_engine.h"
namespace reasampler::vst {
// What the editor and the bake print for a refused file rate (sourceRateSupported), so the
// limit is spelled in one place.
std::string sourceRateRefusalText();
// The dialed sound as plain data for the offline bake (ReaSamplerProcessor::bakeSnapshot):
// empty when nothing is loaded or the WAV is unreadable; `rateRefused` distinguishes a
// decoded file whose rate is refused against the host's from that plain no-play.
struct BuiltCapture {
std::optional<SampleData> sample;
// The file decoded, but sourceRateSupported (play_params.h) refuses its rate against
// the host's — a distinct failure the user is told about, not an unreadable file.
bool rateRefused = false;
};
// 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.
+6 -14
View File
@@ -39,10 +39,6 @@ using instrument::map::kPreviewVelocityDefault;
class ReaSamplerEmbed; // embedded TCP/MCP UI shell (owned below; see queryInterface)
// What the editor and the bake print for a refused file rate (sourceRateSupported), so the
// limit is spelled in one place.
std::string sourceRateRefusalText();
class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
public:
ReaSamplerProcessor() = default;
@@ -157,13 +153,8 @@ public:
// rather than copied off the live snapshot because a tier-3 live edit leaves that
// snapshot's own play params stale on purpose — copying it would bake the pre-drag values.
// Empty when nothing is loaded, the WAV is unreadable, or its rate is refused (then
// `rateRefused` says so). Off the audio thread (file I/O).
struct BuiltCapture {
std::optional<SampleData> sample;
// The file decoded, but sourceRateSupported (play_params.h) refuses its rate against
// the host's — a distinct failure the user is told about, not an unreadable file.
bool rateRefused = false;
};
// `rateRefused` says so). Off the audio thread (file I/O). See BuiltCapture
// (processor_snapshot.h).
BuiltCapture bakeSnapshot(int renderRate);
// Adopt a landed bake in ONE act: the new capture becomes this instance's ref and
@@ -442,9 +433,10 @@ private:
// The host lane's state and its release protocol; automation_channel.h owns the mechanism and
// this directory's CLAUDE.md the authority model it enforces.
AutomationChannel automation_;
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams) and of
// liveContours_ (reloadInstrument, publishLiveContours) only — separate from reloadMutex_ so
// a drag's publish never blocks behind a reload's WAV decode. The audio thread never takes
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams,
// restampHostRateLocked) and of liveContours_ (reloadInstrument, publishLiveContours,
// restampHostRateLocked) only — separate from reloadMutex_ so a drag's publish never blocks
// behind a reload's WAV decode. The audio thread never takes
// this; it only reads via the seqlock's lock-free retry.
std::mutex livePublishMutex_;
// The output rate the loaded capture's play params were resolved at (SampleData::
+2 -1
View File
@@ -1092,7 +1092,8 @@ static void testCadenceCornerIsUnmovedByAPitchSynchronousSplice() {
std::printf(" [cadence corner, PSOLA] period %.0f (want %.0f, metric floor %.2f%%): "
"excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor,
pctOn - floor);
CHECK(pctOn - floor < 1.0); // the corner's real excess, PSOLA or not
CHECK(pctOff - floor < 0.1); // the corner's real excess, PSOLA or not
CHECK(pctOn - floor < 0.1); // measured 0.00% either way
CHECK(pctOn < pctOff + 0.05); // and PSOLA costs it nothing
}
}