Make spline EG drags audible live: held and new notes follow a dragged knot
Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include "core/instrument/engine/envelopes.h"
|
||||
#include "core/instrument/engine/filter/filter_params.h"
|
||||
#include "core/instrument/engine/filter/voice_filter.h"
|
||||
#include "core/instrument/engine/live_contours.h"
|
||||
#include "core/instrument/engine/live_params.h"
|
||||
#include "core/instrument/engine/loop/loop_span.h"
|
||||
#include "core/instrument/engine/pitch_shift.h"
|
||||
@@ -179,6 +180,14 @@ public:
|
||||
// pitch ratio, and the decoded PCM stay latched at note-on.
|
||||
void applyLive(const instrument::engine::LiveValues& live, bool snap);
|
||||
|
||||
// Re-points every drawn EG this note is ALREADY reading at `contours` — a cursor start()
|
||||
// left cleared stays cleared, because Staged|Spline is a note-on fact. Block-boundary only,
|
||||
// like applyLive. Without `snap` the level step at the current read position is absorbed
|
||||
// by that EG's StepSmoother, the same way a live reshape of a positional AHD is; with it
|
||||
// (a voice that has rendered nothing) the new contour is taken outright. `contours` must
|
||||
// outlive the binding or be rebound first — VoiceEngine's double buffer owns that.
|
||||
void applyContours(const instrument::engine::LiveContours& contours, bool snap);
|
||||
|
||||
// Pre-sizes this voice's Preserve pitch shifters (both channels) to `windowFrames`, off
|
||||
// the audio thread (allocates; also sizes the prime scratch buffer), so start() — which
|
||||
// runs inside process() — never allocates. <= 1 leaves the shifters pass-through.
|
||||
@@ -268,6 +277,14 @@ private:
|
||||
// its opening value.
|
||||
double splinePhase() const { return readPos_ * splineScale_; }
|
||||
|
||||
// One drawn EG's value this frame: the contour at the read position plus whatever step a
|
||||
// live contour swap left to absorb (applyContours). The smoother is inactive at rest, so a
|
||||
// contour nobody is editing costs one predicted branch over the bare cursor read.
|
||||
double readSpline(SplineCursor& cursor, StepSmoother& smooth) {
|
||||
const double v = cursor.eval(splinePhase());
|
||||
return smooth.active() ? v + smooth.advance() : v;
|
||||
}
|
||||
|
||||
// This frame's amplitude in [0,1] from the active envelope. Spline: the drawn contour read
|
||||
// at the normalized position (one cached-segment compare per frame). Gate: AHDSR ticks once
|
||||
// per output frame (envelope time is wall-clock, independent of read rate). Trigger: the AHD
|
||||
@@ -284,7 +301,7 @@ private:
|
||||
if (ampSplineCur_.active() && playMode_ == PlayMode::Trigger) {
|
||||
// Early-free at a genuine permanent terminus (the spline analogue of a staged AHD's
|
||||
// finished()) — onFinalSegment()/segmentEndValue()'s own doc comments own the why.
|
||||
amp = ampSplineCur_.eval(splinePhase());
|
||||
amp = readSpline(ampSplineCur_, ampSplineSmooth_);
|
||||
if (amp == 0.0 && ampSplineCur_.onFinalSegment() &&
|
||||
ampSplineCur_.segmentEndValue() == 0.0) {
|
||||
amplitudeDone_ = true;
|
||||
@@ -321,7 +338,7 @@ private:
|
||||
// the source-offset AHD in Trigger. playMode_ is fixed for the note's lifetime, so the
|
||||
// branch is perfectly predicted.
|
||||
const double envOut = filterSplineCur_.active()
|
||||
? filterSplineCur_.eval(splinePhase())
|
||||
? readSpline(filterSplineCur_, filterSplineSmooth_)
|
||||
: ((playMode_ == PlayMode::Gate)
|
||||
? filterEnv_.tick()
|
||||
: filterAhd_.amplitudeAt(sourceOffset()));
|
||||
@@ -514,9 +531,10 @@ private:
|
||||
seedTerminalDeclick();
|
||||
}
|
||||
const double gain = amp * velocityGain_;
|
||||
const double pitchEnvSemis = pitchSplineCur_.active()
|
||||
? pitchSplineDepth_ * pitchSplineCur_.eval(splinePhase())
|
||||
: pitchEnv_.tick();
|
||||
const double pitchEnvSemis =
|
||||
pitchSplineCur_.active()
|
||||
? pitchSplineDepth_ * readSpline(pitchSplineCur_, pitchSplineSmooth_)
|
||||
: pitchEnv_.tick();
|
||||
|
||||
// 2^(semis/12); when the envelope is off (semis exactly 0) this is 1.0 and skips the
|
||||
// pow entirely — no per-frame transcendental on the common path.
|
||||
@@ -748,13 +766,18 @@ private:
|
||||
bool amplitudeDone_ = false; // set when the active amplitude envelope finished
|
||||
|
||||
// The three drawn contours, bound at note-on to the loaded capture's own point arrays (the
|
||||
// SampleData outlives the voice — same contract as sample_). A Staged EG leaves its cursor
|
||||
// inactive, so a purely staged instrument's per-sample path gains three predicted branches
|
||||
// and nothing else. splineScale_ is 1/frameCount, the readPos -> [0,1] map every contour
|
||||
// shares; pitchSplineDepth_ is the pitch envelope's peak, zero while it is disabled.
|
||||
// SampleData outlives the voice — same contract as sample_) and, when the engine observes a
|
||||
// contour block, re-pointed at its engine's live copy (applyContours). A Staged EG leaves its
|
||||
// cursor inactive, so a purely staged instrument's per-sample path gains three predicted
|
||||
// branches and nothing else. splineScale_ is 1/frameCount, the readPos -> [0,1] map every
|
||||
// contour shares; pitchSplineDepth_ is the pitch envelope's peak, zero while it is disabled.
|
||||
// Each smoother holds the step a live contour swap left, in the contour's own y domain.
|
||||
SplineCursor ampSplineCur_;
|
||||
SplineCursor pitchSplineCur_;
|
||||
SplineCursor filterSplineCur_;
|
||||
StepSmoother ampSplineSmooth_;
|
||||
StepSmoother pitchSplineSmooth_;
|
||||
StepSmoother filterSplineSmooth_;
|
||||
double splineScale_ = 0.0;
|
||||
double pitchSplineDepth_ = 0.0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user