pub struct Easing {
pub attack: f32,
pub release: f32,
}Expand description
A binding’s easing time constants in seconds (ADR-0019, widened to a pair by ADR-0035).
attack applies while the incoming value is above the held one and
release while it is at or below — so a percussive parameter can reach its
target in a frame or two and then glide back over most of a second, which no
single constant expresses at any value.
The scalar [smoothing] form builds Easing::symmetric, which is the
low-pass ADR-0019 shipped: with both constants equal the direction test picks
the same number either way, so the arithmetic is bit-for-bit unchanged.
Fields§
§attack: f32Constant used while the raw value is above the held value (rising).
release: f32Constant used while the raw value is at or below the held value.
Implementations§
Source§impl Easing
impl Easing
Sourcepub const INSTANT: Self
pub const INSTANT: Self
No smoothing on either side: the value is applied instantly. The default
for a parameter absent from [smoothing].
Sourcepub const fn symmetric(tau: f32) -> Self
pub const fn symmetric(tau: f32) -> Self
One constant in both directions — the scalar [smoothing] form.
Sourcepub fn step(self, held: f32, raw: f32, dt: f32) -> f32
pub fn step(self, held: f32, raw: f32, dt: f32) -> f32
One frame of the one-pole envelope: ease held toward raw over dt
real seconds, using whichever constant the direction of travel selects.
The single implementation of this vocabulary. The render layer’s per-binding smoother and the spectrum scene’s per-element smoother both call it, so “smoothing in seconds, frame-rate independent, asymmetric by direction” means exactly one thing everywhere (ADR-0019 / ADR-0035, Plan 0034 Phase 3).
The direction test is against the held value, not the raw signal’s own derivative: a value already above its new target releases toward it even while the input is still rising. That is the envelope-follower convention, and it is what keeps the behavior stable under a noisy input.
dt is finite and positive, as Scene::advance states it: the
renderer’s sanitize_frame_dt is the only answer to a degenerate frame
delta (ADR-0191), and nothing here second-guesses it. Outside that
precondition the arithmetic answers: dt = 0 gives alpha = 0, which
holds held, and a non-finite dt poisons one frame that the
non-finite-held guard below turns back into raw on the next.
A selected constant of <= 0 (the default) or non-finite passes raw
through unchanged. Total and allocation-free — it runs per element per
frame.
A non-finite held or raw also passes raw through — a snap,
which is what a smoother with no valid state should do (Plan 0038
Phase 9). This is not a theoretical edge: log(0) is -inf and silence
produces it every time the music stops, so a [smoothing]-listed binding
reaches this on ordinary material. Without the guard the arithmetic below
is -inf + alpha * (-inf - -inf) = NaN, and NaN is absorbing
here — raw > held is false for every raw, so the release branch is
taken and the state stays NaN forever. The binding would be dead for
the rest of the preset’s run, recovering only on a switch.
Both operands are checked because guarding raw alone does not fix it:
a stored -inf against a finite raw selects attack and computes
-inf + inf, which is NaN on the very next frame.
The ease ends by snapping to raw at its fixed point. In f32 the
one-pole never arrives on its own: a frame moves held only while
alpha * gap is at least half the float spacing u at held, so it
stalls at a gap of about u / (2 * alpha) and stays there. A consumer
that truncates the value (as usize, floor) would then draw one step
low forever. So a frame that makes no progress returns raw.
The test is “no progress”, not “within some distance”, because the stall
gap scales with 1 / alpha: toward 2.0 it is ~3 spacings at tau 0.1 s
and 60 Hz but ~144 at tau 2 s and 144 Hz, so any fixed threshold is
outside some slow ease’s stall and never fires. The fixed point always
comes: alpha < 1 puts the exact sum strictly between held and raw,
round-to-nearest keeps it in [held, raw], so the value moves
monotonically and must reach raw or a stall in finitely many frames.
The jump is at most the stall gap, about 2^-24 / alpha of the value.
alpha > 0 guards the snap. For dt / tau below roughly 3e-8,
1 - exp(-dt/tau) rounds to exactly zero and every frame makes no
progress; without the guard the slowest possible ease would become an
instant one. There the value holds.