instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes
Trigger's fade pair folds into the AHD (and goes live); the release anchors right; Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include "core/audio/peaks.h"
|
||||
#include "core/instrument/engine/filter/voice_filter.h"
|
||||
#include "core/instrument/engine/velocity_curve.h"
|
||||
#include "core/util/curve_law.h" // the per-segment curve exponent domain + its neutral
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
@@ -39,34 +40,44 @@ inline constexpr int kMaxVoiceCount = 32;
|
||||
inline constexpr int kDefaultVoiceCount = 16;
|
||||
|
||||
// AHDSR amplitude envelope. holdFrames == 0 is exactly the pre-hold-stage ADSR (back-compat).
|
||||
// The three curve exponents shape the SLOPED stages only — Hold and Sustain are flat by
|
||||
// definition and carry none. `curve_law.h` owns what an exponent means.
|
||||
struct AdsrParams {
|
||||
std::int64_t attackFrames = 0;
|
||||
std::int64_t holdFrames = 0;
|
||||
std::int64_t decayFrames = 0;
|
||||
double sustainLevel = 1.0; // 0..1
|
||||
std::int64_t releaseFrames = 0;
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
double releaseCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// Attack -> Hold -> Decay over a bounded span: the shape every SUSTAIN-LESS envelope takes
|
||||
// (the Trigger amp, the Trigger filter envelope, the pitch envelope). Hold is a FRACTION of
|
||||
// the span left after attack and decay, never a time of its own — that is what makes
|
||||
// A + H + D <= span structural rather than clamped (see fitAhd in envelopes.h).
|
||||
struct AhdParams {
|
||||
std::int64_t attackFrames = 0;
|
||||
std::int64_t decayFrames = 0;
|
||||
double holdFraction = 1.0; // 0..1 of the span remaining after attack + decay
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// GATE = classic held note (AHDSR + sustain loop + note-off release). TRIGGER = one-shot:
|
||||
// note-off-immune, no sustain loop, plays a % of sample length shaped by fade-in/out. Both
|
||||
// honor the start point. Default Gate so an instrument with no params set plays as before.
|
||||
// note-off-immune, no sustain loop, plays a % of sample length shaped by the AHD. Both honor
|
||||
// the start point. Default Gate so an instrument with no params set plays as before.
|
||||
enum class PlayMode { Gate, Trigger };
|
||||
|
||||
// Playback covers [startFrame, playEnd), playEnd = startFrame +
|
||||
// round(lengthFraction*(frames - startFrame)). Amplitude ramps 0->1 over fadeInFrames at the
|
||||
// head and 1->0 over fadeOutFrames anchored to playEnd; unity between. Fades clamp so
|
||||
// fadeIn + fadeOut <= play length. The voice frees when the head reaches playEnd.
|
||||
// Trigger's play SPAN: [startFrame, playEnd), playEnd = startFrame +
|
||||
// round(lengthFraction*(frames - startFrame)). The voice frees when the head reaches playEnd.
|
||||
// The amplitude SHAPE over that span is PlayParams::trigAhd — the fade-in/fade-out pair that
|
||||
// used to live here is retired; do not reintroduce a second amplitude mechanism.
|
||||
struct TriggerParams {
|
||||
double lengthFraction = 1.0; // (0,1] of the post-start span to play
|
||||
std::int64_t fadeInFrames = 0;
|
||||
std::int64_t fadeOutFrames = 0;
|
||||
double lengthFraction = 1.0; // (0,1] of the post-start span to play
|
||||
};
|
||||
|
||||
// EQUAL_POWER (constant-power sin/cos) is the click-free default for Trigger's ramps; LINEAR is
|
||||
// the build-time residual.
|
||||
enum class FadeCurve { EqualPower, Linear };
|
||||
inline constexpr FadeCurve kDefaultFadeCurve = FadeCurve::EqualPower;
|
||||
|
||||
// VARISPEED: readPos_ += ratio_, pitch and duration coupled (an octave up plays half as long).
|
||||
// PRESERVE: the read advances at the source rate while a PitchShifter transposes the output
|
||||
// (an octave up keeps its length).
|
||||
@@ -83,14 +94,15 @@ inline constexpr PitchEngine kDefaultPitchEngine = PitchEngine::Preserve;
|
||||
// of real source, so output frame 0 is source frame 0 regardless of window size.
|
||||
inline constexpr double kPreserveWindowMs = 50.0;
|
||||
|
||||
// AD pitch-modulation envelope, off by default (enabled=false -> offset always 0 -> bit-identical
|
||||
// to the un-modulated engine). At note-on the offset rises to peakSemitones over attackFrames,
|
||||
// then falls to 0 over decayFrames; a zero attack gives a pure percussive pitch drop.
|
||||
// AHD pitch-modulation envelope, off by default (enabled=false -> offset always 0 ->
|
||||
// bit-identical to the un-modulated engine). At note-on the offset rises to peakSemitones over
|
||||
// attack, holds there, then falls to 0 over decay; a zero attack gives a pure percussive pitch
|
||||
// drop. The hold fraction defaults to 0 so an instance predating the stage plays exactly as its
|
||||
// attack-decay predecessor did.
|
||||
struct PitchEnvParams {
|
||||
bool enabled = false;
|
||||
std::int64_t attackFrames = 0;
|
||||
std::int64_t decayFrames = 0;
|
||||
double peakSemitones = 0.0; // signed depth at the peak
|
||||
bool enabled = false;
|
||||
double peakSemitones = 0.0; // signed depth at the peak
|
||||
AhdParams shape{0, 0, /*holdFraction=*/0.0, util::kCurveNeutral, util::kCurveNeutral};
|
||||
};
|
||||
|
||||
// Per-voice resonant filter, off by default (enabled=false -> the render path skips it
|
||||
@@ -106,7 +118,11 @@ struct FilterParams {
|
||||
double modAmount = 0.0; // bipolar [-1,+1], envelope -> cutoff
|
||||
double velAmount = 0.0; // bipolar [-1,+1], velocity -> cutoff
|
||||
double keyTrack = 0.0; // octaves of cutoff per octave of (note - root)
|
||||
AdsrParams env; // the same staged AHDSR the amp runs; frames
|
||||
// The filter envelope takes the same shape the amp does under the active play mode:
|
||||
// AHDSR in Gate, AHD in Trigger. Both are stored, so a mode flip cannot lose either
|
||||
// mode's dialled values (see core/instrument/CLAUDE.md).
|
||||
AdsrParams env; // Gate: the same staged AHDSR the amp runs; frames
|
||||
AhdParams trigEnv; // Trigger: the same staged AHD the amp runs; frames
|
||||
// Shapes velocity before velAmount scales it. Linear rather than the amp's flat() default
|
||||
// because a flat curve under a depth control would make every velocity the same offset;
|
||||
// the no-op at rest is velAmount == 0, not the curve. NOTE: this default only governs a
|
||||
@@ -121,8 +137,9 @@ struct FilterParams {
|
||||
// Preserve product default is layered on at (de)serialization, see kDefaultPitchEngine.
|
||||
struct PlayParams {
|
||||
PlayMode playMode = PlayMode::Gate;
|
||||
AdsrParams adsr;
|
||||
TriggerParams trigger;
|
||||
AdsrParams adsr; // Gate amp
|
||||
TriggerParams trigger; // Trigger play span
|
||||
AhdParams trigAhd; // Trigger amp
|
||||
PitchEngine pitchEngine = PitchEngine::Varispeed;
|
||||
PitchEnvParams pitchEnv;
|
||||
FilterParams filter;
|
||||
|
||||
Reference in New Issue
Block a user