From 30bc5a9e778a579f90ce2f12c604ae2943e85fca Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Fri, 25 Sep 2026 14:29:43 -0400 Subject: [PATCH] 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. --- src/core/instrument/engine/pitch_shift.h | 2 +- src/shell/instrument/CLAUDE.md | 5 +++-- src/shell/instrument/instrument_bake.cpp | 2 +- src/shell/instrument/processor_reload.cpp | 18 +++++++++++------- src/shell/instrument/processor_snapshot.h | 16 ++++++++++++++++ src/shell/instrument/reasampler_processor.h | 20 ++++++-------------- tests/test_pitch_shift.cpp | 3 ++- 7 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/core/instrument/engine/pitch_shift.h b/src/core/instrument/engine/pitch_shift.h index d492860..e84dd34 100644 --- a/src/core/instrument/engine/pitch_shift.h +++ b/src/core/instrument/engine/pitch_shift.h @@ -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` diff --git a/src/shell/instrument/CLAUDE.md b/src/shell/instrument/CLAUDE.md index 05e78a8..baae132 100644 --- a/src/shell/instrument/CLAUDE.md +++ b/src/shell/instrument/CLAUDE.md @@ -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 diff --git a/src/shell/instrument/instrument_bake.cpp b/src/shell/instrument/instrument_bake.cpp index d49dc2b..dd84333 100644 --- a/src/shell/instrument/instrument_bake.cpp +++ b/src/shell/instrument/instrument_bake.cpp @@ -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; diff --git a/src/shell/instrument/processor_reload.cpp b/src/shell/instrument/processor_reload.cpp index fb6901b..619f000 100644 --- a/src/shell/instrument/processor_reload.cpp +++ b/src/shell/instrument/processor_reload.cpp @@ -80,10 +80,10 @@ std::optional 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, - const InstrumentParams& params, - const std::string& projectDir, - ChannelMode mode, int renderRate) { +BuiltCapture buildFromRef(const SelectedSample& sel, + const InstrumentParams& params, + const std::string& projectDir, + ChannelMode mode, int renderRate) { std::optional pcm = decodeRelative(projectDir, sel.relativePath, mode); if (!pcm) return {}; if (!sourceRateSupported(pcm->sampleRate, renderRate)) return {std::nullopt, true}; @@ -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 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 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; } diff --git a/src/shell/instrument/processor_snapshot.h b/src/shell/instrument/processor_snapshot.h index fe8805f..89df5d8 100644 --- a/src/shell/instrument/processor_snapshot.h +++ b/src/shell/instrument/processor_snapshot.h @@ -7,11 +7,27 @@ #include #include +#include +#include #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 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. diff --git a/src/shell/instrument/reasampler_processor.h b/src/shell/instrument/reasampler_processor.h index de371aa..1eeedb5 100644 --- a/src/shell/instrument/reasampler_processor.h +++ b/src/shell/instrument/reasampler_processor.h @@ -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 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:: diff --git a/tests/test_pitch_shift.cpp b/tests/test_pitch_shift.cpp index 5a84b6c..8d75a52 100644 --- a/tests/test_pitch_shift.cpp +++ b/tests/test_pitch_shift.cpp @@ -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 } }