instrument: deliver continuous playback params live to sounding voices via a seqlock block, holding normalized stage position across time edits

This commit is contained in:
2026-07-30 21:03:05 -04:00
parent 7bd911d58b
commit 1dade0bfcf
25 changed files with 1352 additions and 66 deletions
+54 -3
View File
@@ -92,24 +92,41 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
pitchEnv_.configure(p.pitchEnv);
pitchEnv_.noteOn();
// A restart lands every live glide back on the new note's own values, at a step derived
// from this sample's rate rather than any assumed one.
filterRamping_ = false;
const double rampStep = instrument::engine::liveRampStep(
static_cast<double>(sample.sampleRate));
rBaseCutoff_.step = rampStep;
rModAmount_.step = rampStep;
rResonance_.step = rampStep;
rMorph_.step = rampStep;
rDrive_.step = rampStep;
// Filter: reset() clears integrator state for the new note (prepare() preserves it —
// voice_filter.h / filter/CLAUDE.md). Velocity maps through the curve once here, off the
// per-frame path, exactly as the amp's velocityGain_ does.
filterOn_ = p.filter.enabled;
if (filterOn_) {
filterSettings_ = p.filter.settings;
filterCutoffNorm_ = static_cast<double>(p.filter.settings.cutoffNorm);
filterModAmount_ = p.filter.modAmount;
filterKeyTrack_ = p.filter.keyTrack;
filterVelOffset_ =
p.filter.velAmount * p.filter.velocityCurve.eval(static_cast<double>(velocity));
filterRate_ = static_cast<double>(sample.sampleRate);
rModAmount_.set(p.filter.modAmount);
rResonance_.set(static_cast<double>(p.filter.settings.resonanceNorm));
rMorph_.set(static_cast<double>(p.filter.settings.morphNorm));
rDrive_.set(static_cast<double>(p.filter.settings.driveNorm));
filterEnv_.configure(p.filter.env);
filterEnv_.noteOn();
filter_.reset();
updateFilterCutoffBase(note);
// The note's ONE full solve — Q, morph and drive are constants for its lifetime, so
// every later re-solve is the cheap cutoff-only path. A modulated voice supersedes this
// cutoff in tickFilterCutoff on its first frame, before any sample reaches the kernel.
// The note's ONE full solve — Q, morph and drive are constants for its lifetime unless
// a live move glides them, so every later re-solve is the cheap cutoff-only path. A
// modulated voice supersedes this cutoff in tickFilterCutoff on its first frame,
// before any sample reaches the kernel.
instrument::engine::filter::FilterSettings s = p.filter.settings;
s.cutoffNorm = filterBaseCutoff_;
filter_.prepare(s, filterRate_);
@@ -178,6 +195,40 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
ratio_ = baseRatio_; // seeded; advanceFrame recomputes per frame under the active engine.
}
void Voice::applyLive(const instrument::engine::LiveValues& live, bool snap) {
// Gate's amplitude envelope is the AHDSR; Trigger's fade shape is anchored to a play span
// resolved at note-on and is not a live control, so it is deliberately untouched here.
if (playMode_ == PlayMode::Gate) env_.applyLive(live.adsr);
pitchEnv_.applyLive(live.pitchEnvAttackFrames, live.pitchEnvDecayFrames,
live.pitchEnvPeakSemitones);
if (!filterOn_) return; // filter enable is a discrete toggle: it travels by reload
filterEnv_.applyLive(live.filterEnv);
filterCutoffNorm_ = static_cast<double>(live.filterSettings.cutoffNorm);
filterKeyTrack_ = live.filterKeyTrack;
filterSettings_.morphLaw = live.filterSettings.morphLaw;
const double baseTarget = filterCutoffBaseTarget(note_);
if (snap) {
rBaseCutoff_.set(baseTarget);
rModAmount_.set(live.filterModAmount);
rResonance_.set(static_cast<double>(live.filterSettings.resonanceNorm));
rMorph_.set(static_cast<double>(live.filterSettings.morphNorm));
rDrive_.set(static_cast<double>(live.filterSettings.driveNorm));
filterBaseCutoff_ = static_cast<float>(baseTarget);
filterModAmount_ = live.filterModAmount;
filterRamping_ = false;
prepareFilterFromRamps();
return;
}
rBaseCutoff_.aim(baseTarget);
rModAmount_.aim(live.filterModAmount);
rResonance_.aim(static_cast<double>(live.filterSettings.resonanceNorm));
rMorph_.aim(static_cast<double>(live.filterSettings.morphNorm));
rDrive_.aim(static_cast<double>(live.filterSettings.driveNorm));
filterRamping_ = rBaseCutoff_.moving() || rModAmount_.moving() || rResonance_.moving() ||
rMorph_.moving() || rDrive_.moving();
}
void Voice::retune(int note) {
// Mono legato takeover: move the pitch, touch NOTHING else — the amplitude envelope keeps
// running (no re-attack), the read head keeps its position, the shifter keeps its ring