Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
@@ -118,6 +118,13 @@ void ReaSamplerEditor::onMouseUp(int x, int y) {
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
// A bare click on the Hold cell — no drag, no value change — would otherwise buy a bridge
|
||||
// read, a WAV re-decode and an engine rebuild for a parameter the engine never reads.
|
||||
if (kind == DragKind::kDeckKnob && paramId == kBakeHoldKnobId &&
|
||||
params_.bakeHold == dragStartParams_.bakeHold) {
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
// A live control already reached the voices during the drag; its release commits the
|
||||
// final value through the same tier.
|
||||
if (dragCommitsLive(kind, paramId)) {
|
||||
|
||||
@@ -109,9 +109,13 @@ bool ReaSamplerEditor::doubleClickChrome(const FaceLayout& fl, int x, int y) {
|
||||
if (selectedId_.empty() || !processor_) return false;
|
||||
if (bakeHoldNeeded_ && inKnobFace(fl.chrome.holdKnob, x, y)) {
|
||||
// The default is READ off a default-constructed parameter set, so there is no second
|
||||
// table of defaults to drift from the codec's own lift.
|
||||
params_.bakeHold = InstrumentParams{}.bakeHold;
|
||||
commitAndReload();
|
||||
// table of defaults to drift from the codec's own lift. A reset that changes nothing
|
||||
// commits nothing — the same rule the release path applies.
|
||||
const instrument::note::Division reset = InstrumentParams{}.bakeHold;
|
||||
if (params_.bakeHold != reset) {
|
||||
params_.bakeHold = reset;
|
||||
commitAndReload();
|
||||
}
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -156,9 +156,11 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
|
||||
: (hov ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
drawKnobFace(bmp, cr.holdKnob, bakeHoldNorm(), st);
|
||||
// "Bake Hold", not "Hold": the AMP deck's AHDSR Hold knob is on screen in the same
|
||||
// frame, and two knobs labelled the same are two knobs the user has to guess between.
|
||||
const std::string label = dragging || hov
|
||||
? instrument::note::divisionLabel(params_.bakeHold)
|
||||
: std::string("Hold");
|
||||
: std::string("Bake Hold");
|
||||
kitTextCentered(bmp, cr.holdLabel, label.c_str(), Font::Micro, Role::TextDim);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor";
|
||||
|
||||
// The change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread
|
||||
// timer: responsive enough that a recapture/ingest/assign refreshes within a bounded
|
||||
// cadence, yet cheap — three small ext-state reads per tick, coalescing many bumps
|
||||
// between ticks into one reload. 500 ms is a deliberate build-time residual. The id is a
|
||||
// per-window SetTimer id (any nonzero).
|
||||
// cadence, yet cheap — three small ext-state reads per tick in the steady state, coalescing
|
||||
// many bumps between ticks into one reload. Anything costlier a tick answers (the bake-Hold
|
||||
// predicate's bank parse) is memoized against its inputs, so keep it that way rather than
|
||||
// letting a per-tick full read back in. 500 ms is a deliberate build-time residual. The id is
|
||||
// a per-window SetTimer id (any nonzero).
|
||||
constexpr UINT_PTR kSyncTimerId = 1;
|
||||
constexpr UINT kSyncTimerIntervalMs = 500;
|
||||
} // namespace
|
||||
|
||||
@@ -57,6 +57,7 @@ void ReaSamplerEditor::refreshFromBank() {
|
||||
// Main/UI thread only — reads the live bank over the bridge (allocates, calls REAPER).
|
||||
thumbCache_.clear(); // a bank edit may have re-captured/removed a sample; drop stale peaks
|
||||
pcmCache_.clear(); // and its decoded PCM (the waveform + snap source)
|
||||
holdNeedValid_ = false; // …and the bake-Hold answer derived from the bank's loop intrinsic
|
||||
channelPcmId_.clear();
|
||||
channelPcm_ = ChannelPcm{};
|
||||
if (!processor_) {
|
||||
@@ -262,16 +263,43 @@ ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t fram
|
||||
return m;
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::HoldNeedKey::operator==(const HoldNeedKey& o) const {
|
||||
if (sampleId != o.sampleId || crossfade != o.crossfade) return false;
|
||||
if (loopOverride.has_value() != o.loopOverride.has_value()) return false;
|
||||
if (!loopOverride) return true;
|
||||
return loopOverride->hasLoop == o.loopOverride->hasLoop &&
|
||||
loopOverride->start == o.loopOverride->start &&
|
||||
loopOverride->end == o.loopOverride->end;
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::resolveBakeHoldNeeded() {
|
||||
// The mode test comes first so the common Trigger case never pays pickedMarkers' bridge
|
||||
// read. `m.hasLoop` alone is not the answer — the engine's fold also refuses a span that
|
||||
// reaches outside the PCM, which is exactly what the pure predicate is asked for.
|
||||
if (selectedId_.empty() || params_.play.playMode != PlayMode::Gate) return false;
|
||||
// effectivePlayMode, not the raw field: the bake reads the mode AFTER the drawn-EG fold
|
||||
// (resolvePlay, via bakeSnapshot), so a restored or foreign blob carrying Gate together
|
||||
// with an active spline would otherwise paint a control for a sound that bakes as Trigger.
|
||||
// The mode test comes first so the common Trigger case never pays the resolve below.
|
||||
const PlayMode mode = effectivePlayMode(params_.play);
|
||||
if (selectedId_.empty() || mode != PlayMode::Gate) return false;
|
||||
|
||||
// Everything past here costs a WAV decode and — with no loop override set — a bridge read
|
||||
// plus a bank parse, and the sync tick asks twice a second for as long as an editor is
|
||||
// open. Answer once per distinct input; refreshFromBank drops the memo along with the rest
|
||||
// of the bank-derived caches, which is the same edge a bank generation bump would give.
|
||||
// The mode is not part of the key: the resolve below always asks about Gate, and the test
|
||||
// above is what keeps a Trigger sound from reaching it.
|
||||
const HoldNeedKey key{selectedId_, params_.loopOverride, params_.loopCrossfadeFrames};
|
||||
if (holdNeedValid_ && key == holdNeedKey_) return holdNeedAnswer_;
|
||||
holdNeedValid_ = true;
|
||||
holdNeedKey_ = key;
|
||||
holdNeedAnswer_ = false;
|
||||
|
||||
const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
|
||||
if (frames <= 0) return false;
|
||||
// `m.hasLoop` alone is not the answer — the engine's fold also refuses a span that reaches
|
||||
// outside the PCM, which is exactly what the pure predicate is asked for.
|
||||
const SetupMarkers m = pickedMarkers(frames);
|
||||
return instrument::bake::bakeWindowNeedsHold(
|
||||
holdNeedAnswer_ = instrument::bake::bakeWindowNeedsHold(
|
||||
PlayMode::Gate, SampleLoop{m.hasLoop, m.loopStart, m.loopEnd}, m.crossfade, frames);
|
||||
return holdNeedAnswer_;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) {
|
||||
|
||||
@@ -132,8 +132,7 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
// are live, so a sound dialed at 127 does not bake as one dialed at 40.
|
||||
const Velocity velocity = Velocity::of(processor.previewVelocity());
|
||||
const instrument::bake::PlannedBake planned = planBake(
|
||||
resolveNote(defaultBakeProgram(*snapshot, sampleRate, *tempo, dialed.bakeHold,
|
||||
velocity),
|
||||
resolveNote(defaultBakeProgram(*snapshot, sampleRate, dialed.bakeHold, velocity),
|
||||
*tempo),
|
||||
sampleRate, rootNote);
|
||||
if (!planned.plan) {
|
||||
|
||||
@@ -342,9 +342,22 @@ private:
|
||||
|
||||
// Whether the loaded sound's bake window needs the user's Hold — the pure predicate
|
||||
// (bake_plan.h) answered against the markers this face is showing. Decodes and reads the
|
||||
// bank, so it is called on the sync tick, not per paint.
|
||||
// bank, so it is called on the sync tick, not per paint, and memoized against the inputs
|
||||
// below on top of that.
|
||||
bool resolveBakeHoldNeeded();
|
||||
|
||||
// What that answer was last computed against. Invalidated wholesale by refreshFromBank,
|
||||
// which is where the bank half of the input changes.
|
||||
struct HoldNeedKey {
|
||||
std::string sampleId;
|
||||
std::optional<SampleLoop> loopOverride;
|
||||
std::int64_t crossfade = 0;
|
||||
bool operator==(const HoldNeedKey& other) const;
|
||||
};
|
||||
HoldNeedKey holdNeedKey_;
|
||||
bool holdNeedValid_ = false;
|
||||
bool holdNeedAnswer_ = false;
|
||||
|
||||
// Writes `m` into params_ as the loop/start override. Does NOT call commitAndReload —
|
||||
// callers decide live-drag vs final commit.
|
||||
void applyMarkers(const SetupMarkers& m);
|
||||
|
||||
Reference in New Issue
Block a user