// filter_params.h — control-domain mapping for the voice filter: normalized [0,1] knob
// positions to cutoff Hz, Q, and drive depth. Deliberately sample-rate-free — the Nyquist
// clamp is a property of the bilinear transform and lives in filter_coeffs, so the persisted
// normalized cutoff means the same frequency at every project rate.

#pragma once

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

// The audio band the cutoff control sweeps: three exact decades, so norm 1/3 is 200 Hz and
// norm 2/3 is 2 kHz. NOT derived from the sample rate — a rate-dependent endpoint would make
// one saved preset sound different at 44.1k and 96k, and at 44.1k the top of the travel would
// be dead against the Nyquist clamp (the ported firmware's 23 kHz endpoint had exactly that
// defect). 20 kHz sits under 0.48*sr at 44.1 kHz and above; below that (e.g. 32 kHz, 22.05 kHz)
// the clamp still handles it correctly, it just eats the top of the knob travel at those rates.
inline constexpr float kFilterCutoffMinHz = 20.0f;
inline constexpr float kFilterCutoffMaxHz = 20000.0f;

// Q spans the full range with Butterworth (sqrt(2)) at the control's center detent.
inline constexpr float kFilterQMin = 0.1f;
inline constexpr float kFilterQMax = 10.0f;
inline constexpr float kFilterQCenter = 1.41421356f;

// Depth at the top of the drive control. The limiter's knee is at 1/depth, and the resonance
// swings the state to roughly 2*Q*level, so this is the range over which drive bites. Chosen
// against measurement rather than by feel: at max drive, full-scale input and max resonance the
// resonant peak lands ~10 dB under the passband — plainly crushed, which is the asked-for
// "extreme". Raising it further inverts the filter's shape (measured 21 dB under passband at
// depth 64), turning the peak the user dialled in into a notch.
inline constexpr float kFilterDriveDepthMax = 4.0f;

// Out-of-range norm clamps to the endpoints.
float filterCutoffHzFromNorm(float norm);

// Exact inverse of filterCutoffHzFromNorm over the band; out-of-band Hz clamps to 0 or 1.
float filterNormFromCutoffHz(float hz);

// A single smooth curve — quadratic in log Q — through (0, kFilterQMin),
// (0.5, kFilterQCenter), (1, kFilterQMax), rather than two spliced log segments. Same three
// anchors either way, but the single curve has no slope kink at the center detent.
float filterQFromNorm(float norm);

// Exact inverse of filterQFromNorm; out-of-range Q clamps to 0 or 1.
float filterNormFromQ(float q);

// Drive depth for the in-loop limiter. Square law, not linear: the knee is 1/depth, so a linear
// depth would spend most of the audible travel in the first tenth of the knob. Exactly 0 at
// norm 0 — the limiter is then algebraically the identity, which is what makes drive=0 bit-exact
// linear rather than merely close.
float filterDriveDepthFromNorm(float norm);

// Exact inverse of filterDriveDepthFromNorm; out-of-range depth clamps to 0 or 1. The analytic
// inverse of a frozen law is not a change to it — it has the standing the two inverses above
// already have.
float filterNormFromDriveDepth(float depth);

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