// filter_saturate.h — the drive stage's soft limiter. Header-inline: it sits inside the
// per-voice per-sample recursion.

#pragma once

#include <cmath>

namespace reasampler::instrument::engine::filter {

// Odd, smooth, strictly monotone, bounded by 1/depth, with unit slope at the origin.
//
// Three properties are load-bearing and none of them are tuning:
//   - depth == 0 makes this ALGEBRAICALLY the identity (x / sqrt(1) == x, exact in IEEE), so
//     drive = 0 is bit-exact linear whether or not the caller special-cases it. (voice_filter.h
//     gates the call on drive != 0 anyway, but as a perf optimization, not because correctness
//     needs it.)
//   - |softLimit(x, d)| <= |x| for every d, so dropping it into the resonance state update can
//     only ever shrink the state. The filter therefore cannot gain energy from the drive stage:
//     stability at any Q and any cutoff is structural, not a tuned margin, and it can never
//     self-oscillate.
//   - Unit slope at the origin, so the shaper adds no gain of its own at any depth. What reaches
//     it is the resonance state, already multiplied by roughly 2*Q, which is why drive and
//     resonance interact: the same drive setting bites harder the more resonance is dialled in.
//
// The retired feedbackSaturate() is deliberately not carried forward: it had 0.75 slope at the
// origin, a fixed +/-2.0 threshold calibrated for firmware excursion levels, and turned over
// (non-monotone) past x = 6. That absolute threshold is the origin of the level-dependent
// resonance this rewrite removes — do not reintroduce it.
inline float softLimit(float x, float depth) {
    const float s = depth * x;
    return x / std::sqrt(1.0f + s * s);
}

}  // namespace reasampler::instrument::engine::filter
