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 jump = 0; // the clamped nominal jump actually applied (signed)
std::int64_t lag = 0; // correlation best integer lag std::int64_t lag = 0; // correlation best integer lag
double frac = 0.0; // parabolic sub-sample refinement, [-0.5, 0.5] 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` // 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 — `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` `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 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 the class declaration itself, 630 lines (measured post-split) — over the ceiling since the
(`liveContours_` + `publishLiveContours`) landed beside `liveParams_` under the same ordering 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 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 (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 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 // 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. // 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.rateRefused) return fail("cannot render: " + sourceRateRefusalText());
if (!built.sample) return fail("the loaded capture could not be decoded for the render"); if (!built.sample) return fail("the loaded capture could not be decoded for the render");
SampleData& snapshot = *built.sample; 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 // 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. // 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 InstrumentParams& params,
const std::string& projectDir, const std::string& projectDir,
ChannelMode mode, int renderRate) { ChannelMode mode, int renderRate) {
@@ -172,8 +172,9 @@ std::string ReaSamplerProcessor::reloadInstrument() {
// collapses every automated envelope stage to zero frames. // collapses every automated envelope stage to zero frames.
builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed); builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed);
{ {
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here; // reloadMutex_ (held for this whole function) nests livePublishMutex_ here, the
// publishLiveParams never holds reloadMutex_, so this is the only nesting. // same nesting restampHostRateLocked takes under its own reloadMutex_-held
// callers; publishLiveParams never holds reloadMutex_.
std::lock_guard<std::mutex> lp(livePublishMutex_); std::lock_guard<std::mutex> lp(livePublishMutex_);
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack)); liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
// The model's contours, so a reload-tier contour edit (a point added, deleted or // 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_; return instanceGuid_;
} }
ReaSamplerProcessor::BuiltCapture ReaSamplerProcessor::bakeSnapshot(int renderRate) { BuiltCapture ReaSamplerProcessor::bakeSnapshot(int renderRate) {
const std::string selId = selectedSampleId(); const std::string selId = selectedSampleId();
if (selId.empty()) return {}; if (selId.empty()) return {};
const InstrumentParams params = instrumentParams(); const InstrumentParams params = instrumentParams();
@@ -443,6 +444,9 @@ bool ReaSamplerProcessor::restampHostRateLocked(SampleData& sample, int renderRa
builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed); builtSampleRate_.store(sample.outputRate(), std::memory_order_relaxed);
std::lock_guard<std::mutex> lp(livePublishMutex_); std::lock_guard<std::mutex> lp(livePublishMutex_);
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack)); 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; return true;
} }
+16
View File
@@ -7,11 +7,27 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <optional>
#include <string>
#include "core/instrument/engine/voice_engine.h" #include "core/instrument/engine/voice_engine.h"
namespace reasampler::vst { 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 // 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), // 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. // 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) 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 { class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
public: public:
ReaSamplerProcessor() = default; ReaSamplerProcessor() = default;
@@ -157,13 +153,8 @@ public:
// rather than copied off the live snapshot because a tier-3 live edit leaves that // 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. // 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 // 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). // `rateRefused` says so). Off the audio thread (file I/O). See BuiltCapture
struct BuiltCapture { // (processor_snapshot.h).
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;
};
BuiltCapture bakeSnapshot(int renderRate); BuiltCapture bakeSnapshot(int renderRate);
// Adopt a landed bake in ONE act: the new capture becomes this instance's ref and // 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 // 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. // this directory's CLAUDE.md the authority model it enforces.
AutomationChannel automation_; AutomationChannel automation_;
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams) and of // Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams,
// liveContours_ (reloadInstrument, publishLiveContours) only — separate from reloadMutex_ so // restampHostRateLocked) and of liveContours_ (reloadInstrument, publishLiveContours,
// a drag's publish never blocks behind a reload's WAV decode. The audio thread never takes // 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. // this; it only reads via the seqlock's lock-free retry.
std::mutex livePublishMutex_; std::mutex livePublishMutex_;
// The output rate the loaded capture's play params were resolved at (SampleData:: // 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%%): " std::printf(" [cadence corner, PSOLA] period %.0f (want %.0f, metric floor %.2f%%): "
"excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor, "excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor,
pctOn - 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 CHECK(pctOn < pctOff + 0.05); // and PSOLA costs it nothing
} }
} }