instrument: snap live params onto a fresh voice, roll a live drag back on capture loss, serialize the seqlock's two writers
This commit is contained in:
@@ -90,6 +90,17 @@ public:
|
||||
stagePos_ = 0.0;
|
||||
}
|
||||
|
||||
// Live parameter delivery to a fresh voice — one that has NOT yet rendered a frame, whose
|
||||
// latched copy may predate the newest edit. It takes the params outright: there is no
|
||||
// phase to hold and nothing to be continuous with. applyLive cannot serve here in either
|
||||
// direction — with a stale duration of 0 its phi rule reads stagePos_ == 0 as a COMPLETED
|
||||
// stage and discards the newly-dialled time, and with a stale duration > 0 against a new 0
|
||||
// it absorbs a full-scale step into a voice that has emitted nothing, fading the onset in.
|
||||
void snapLive(const AdsrParams& params) {
|
||||
params_ = params;
|
||||
smooth_.clear();
|
||||
}
|
||||
|
||||
// Live parameter delivery to a SOUNDING voice. The mid-stage rule is HOLD NORMALIZED
|
||||
// STAGE POSITION: phi = elapsed/duration is kept fixed across the change, so this frame's
|
||||
// level is unchanged by construction and the remainder of the stage takes its share of the
|
||||
@@ -117,6 +128,11 @@ public:
|
||||
// Once Release completes the envelope latches Finished and returns 0.0 forever (until
|
||||
// the next noteOn). A single, monotonic per-frame step — the caller pulls one value per
|
||||
// output frame.
|
||||
//
|
||||
// While the smoother runs the return may sit OUTSIDE [0,1] by the offset it is decaying
|
||||
// (bounded by the step it absorbed). finished() ignores that residue, so a Release that
|
||||
// completes with an offset still decaying is hard-cut when the voice frees — the audible
|
||||
// remainder of a step the smoother had already taken most of.
|
||||
double tick() {
|
||||
const double out = tickStage();
|
||||
return smooth_.active() ? out + smooth_.advance() : out;
|
||||
@@ -127,8 +143,11 @@ public:
|
||||
double level() const { return level_; }
|
||||
|
||||
private:
|
||||
// The level tick() would emit right now under `params`, without advancing anything — the
|
||||
// prediction applyLive compares across the change to size the smoother.
|
||||
// The level tick() would emit right now under `params` without advancing anything. THE one
|
||||
// home for every segment's shape: tickStage owns only the advance and the stage
|
||||
// transitions and reads its output from here, so a per-segment curve added later lands in
|
||||
// one place and the smoother can never size a step against a different curve than the
|
||||
// output takes.
|
||||
double stageLevel(const AdsrParams& params) const {
|
||||
switch (stage_) {
|
||||
case Stage::Attack: {
|
||||
@@ -178,12 +197,7 @@ private:
|
||||
return 0.0;
|
||||
|
||||
case Stage::Attack: {
|
||||
if (params_.attackFrames <= 0) {
|
||||
level_ = 1.0;
|
||||
} else {
|
||||
level_ = stagePos_ / static_cast<double>(params_.attackFrames);
|
||||
if (level_ > 1.0) level_ = 1.0;
|
||||
}
|
||||
level_ = stageLevel(params_);
|
||||
const double out = level_;
|
||||
stagePos_ += 1.0;
|
||||
if (stagePos_ >= static_cast<double>(params_.attackFrames)) {
|
||||
@@ -208,7 +222,7 @@ private:
|
||||
// smoother is applied exactly once per frame.
|
||||
return tickStage();
|
||||
}
|
||||
level_ = 1.0;
|
||||
level_ = stageLevel(params_);
|
||||
const double out = level_;
|
||||
stagePos_ += 1.0;
|
||||
if (stagePos_ >= static_cast<double>(params_.holdFrames)) {
|
||||
@@ -220,12 +234,7 @@ private:
|
||||
}
|
||||
|
||||
case Stage::Decay: {
|
||||
if (params_.decayFrames <= 0) {
|
||||
level_ = params_.sustainLevel;
|
||||
} else {
|
||||
const double t = stagePos_ / static_cast<double>(params_.decayFrames);
|
||||
level_ = 1.0 + (params_.sustainLevel - 1.0) * t;
|
||||
}
|
||||
level_ = stageLevel(params_);
|
||||
const double out = level_;
|
||||
stagePos_ += 1.0;
|
||||
if (stagePos_ >= static_cast<double>(params_.decayFrames)) {
|
||||
@@ -237,7 +246,7 @@ private:
|
||||
}
|
||||
|
||||
case Stage::Sustain:
|
||||
level_ = params_.sustainLevel;
|
||||
level_ = stageLevel(params_);
|
||||
return level_;
|
||||
|
||||
case Stage::Release: {
|
||||
@@ -246,9 +255,7 @@ private:
|
||||
stage_ = Stage::Finished;
|
||||
return 0.0;
|
||||
}
|
||||
const double t = stagePos_ / static_cast<double>(params_.releaseFrames);
|
||||
level_ = releaseFrom_ * (1.0 - t);
|
||||
if (level_ < 0.0) level_ = 0.0;
|
||||
level_ = stageLevel(params_);
|
||||
const double out = level_;
|
||||
stagePos_ += 1.0;
|
||||
if (stagePos_ >= static_cast<double>(params_.releaseFrames)) {
|
||||
@@ -348,6 +355,15 @@ public:
|
||||
void configure(const PitchEnvParams& params) { params_ = params; pos_ = 0.0; }
|
||||
void noteOn() { pos_ = 0.0; smooth_.clear(); }
|
||||
|
||||
// Peer of AdsrEnvelope::snapLive (see it for why the two paths cannot share code): a voice
|
||||
// that has rendered nothing takes the new times and depth outright.
|
||||
void snapLive(std::int64_t attackFrames, std::int64_t decayFrames, double peakSemitones) {
|
||||
params_.attackFrames = attackFrames;
|
||||
params_.decayFrames = decayFrames;
|
||||
params_.peakSemitones = peakSemitones;
|
||||
smooth_.clear();
|
||||
}
|
||||
|
||||
// Live parameter delivery, same rule as AdsrEnvelope::applyLive: hold the normalized
|
||||
// position within whichever leg the envelope is in, and absorb the depth step (peak is a
|
||||
// level, not a duration). `enabled` is a discrete toggle and travels by reload, so it is
|
||||
|
||||
Reference in New Issue
Block a user