34template <
typename SampleType =
float, std::
size_t Channels = 2>
class TiltEqT {
35 static_assert(std::is_floating_point_v<SampleType>,
36 "TiltEqT requires a floating-point sample type");
37 static_assert(Channels > 0,
"TiltEqT requires at least one channel");
58 for (
auto& cascade : cascades_)
60 std::array<Coefficients, shelf_count> designed{};
61 double normalization = 1.0;
62 static_cast<void>(design(config_, sample_rate_, designed, normalization));
63 commit(designed, normalization);
70 std::array<Coefficients, shelf_count> designed{};
71 double normalization = 1.0;
72 if (!design(config_,
sample_rate, designed, normalization))
75 commit(designed, normalization);
82 if (same_config(requested, config_))
84 std::array<Coefficients, shelf_count> designed{};
85 double normalization = 1.0;
86 if (!design(requested, sample_rate_, designed, normalization))
89 commit(designed, normalization);
103 return normalization_gain_;
116 SampleType
process(SampleType input, std::size_t channel = 0) noexcept {
117 if (channel >= Channels)
119 if (!std::isfinite(
static_cast<double>(input)))
120 return recover(channel);
121 if (config_.tilt_db_per_octave == 0.0)
123 const SampleType output =
124 cascades_[channel].process(input) *
static_cast<SampleType
>(normalization_gain_);
125 if (!std::isfinite(
static_cast<double>(output)))
126 return recover(channel);
132 bool process_block(SampleType* samples, std::size_t frames, std::size_t channel = 0) noexcept {
133 if (channel >= Channels || (samples ==
nullptr && frames != 0))
135 for (std::size_t frame = 0; frame < frames; ++frame)
136 samples[frame] =
process(samples[frame], channel);
142 bool process_block(
const SampleType* input, SampleType* output, std::size_t frames,
143 std::size_t channel = 0) noexcept {
144 if (channel >= Channels || ((input ==
nullptr || output ==
nullptr) && frames != 0))
146 for (std::size_t frame = 0; frame < frames; ++frame)
147 output[frame] =
process(input[frame], channel);
159 bool reset(std::size_t channel)
noexcept {
160 if (channel >= Channels)
162 cascades_[channel].reset();
168 if (config_.tilt_db_per_octave == 0.0)
170 if (!std::isfinite(frequency_hz))
172 frequency_hz = std::clamp(frequency_hz, 0.0, sample_rate_ * 0.5);
173 const double omega = 2.0 * std::acos(-1.0) * frequency_hz / sample_rate_;
174 double result = normalization_gain_;
175 for (
const auto& section : coefficients_)
177 return std::isfinite(result) ? result : 0.0;
184 const double db = 20.0 * std::log10(
linear);
185 return std::isfinite(db) ? std::max(db, -200.0) : -200.0;
195 return config_.tilt_db_per_octave == 0.0 ? 0 : -1;
201 static bool valid_sample_rate(
double sample_rate)
noexcept {
206 static bool same_config(
const Config& lhs,
const Config& rhs)
noexcept {
207 return lhs.pivot_hz == rhs.pivot_hz && lhs.tilt_db_per_octave == rhs.tilt_db_per_octave;
211 return std::isfinite(
static_cast<double>(
c.b0)) &&
212 std::isfinite(
static_cast<double>(
c.b1)) &&
213 std::isfinite(
static_cast<double>(
c.b2)) &&
214 std::isfinite(
static_cast<double>(
c.a1)) && std::isfinite(
static_cast<double>(
c.a2));
217 static bool design_sections(
double stage_gain_db,
double sample_rate,
double high_hz,
218 std::array<Coefficients, shelf_count>& designed)
noexcept {
221 const double position =
224 BiquadT<SampleType> designer;
225 designer.set_coefficients(
227 static_cast<SampleType
>(0.7071067811865476),
static_cast<SampleType
>(
sample_rate),
228 static_cast<SampleType
>(stage_gain_db));
229 designed[
index] = designer.coefficients();
236 static double endpoint_delta_db(
const std::array<Coefficients, shelf_count>& designed,
239 double low_magnitude = 1.0;
240 double high_magnitude = 1.0;
241 for (
const auto& section : designed) {
245 return 20.0 * std::log10(high_magnitude / low_magnitude);
249 std::array<Coefficients, shelf_count>& designed,
250 double& normalization)
noexcept {
252 if (!valid_sample_rate(
sample_rate) || !std::isfinite(requested.pivot_hz) ||
253 !std::isfinite(requested.tilt_db_per_octave) || requested.pivot_hz <
min_pivot_hz ||
258 const double octaves = std::log2(high_hz /
min_pivot_hz);
259 const double target_delta_db = requested.tilt_db_per_octave * octaves;
260 if (target_delta_db == 0.0) {
261 if (!design_sections(0.0,
sample_rate, high_hz, designed))
264 const double direction = target_delta_db > 0.0 ? 1.0 : -1.0;
265 const double target_magnitude = std::abs(target_delta_db);
266 double low_gain = 0.0;
267 double high_gain = target_magnitude * 2.0 /
static_cast<double>(
shelf_count) + 0.5;
268 if (!design_sections(direction * high_gain,
sample_rate, high_hz, designed) ||
269 std::abs(endpoint_delta_db(designed,
sample_rate, high_hz)) < target_magnitude)
271 for (
int iteration = 0; iteration < 48; ++iteration) {
272 const double candidate_gain = (low_gain + high_gain) * 0.5;
273 if (!design_sections(direction * candidate_gain,
sample_rate, high_hz, designed))
275 if (std::abs(endpoint_delta_db(designed,
sample_rate, high_hz)) < target_magnitude)
276 low_gain = candidate_gain;
278 high_gain = candidate_gain;
280 if (!design_sections(direction * (low_gain + high_gain) * 0.5,
sample_rate, high_hz,
285 const double pivot_omega = 2.0 * std::acos(-1.0) * requested.pivot_hz /
sample_rate;
286 double pivot_magnitude = 1.0;
287 for (
const auto& section : designed)
289 if (!(std::isfinite(pivot_magnitude) && pivot_magnitude > 0.0))
291 normalization = 1.0 / pivot_magnitude;
292 return std::isfinite(normalization) && normalization > 0.0;
295 void commit(
const std::array<Coefficients, shelf_count>& designed,
296 double normalization)
noexcept {
297 coefficients_ = designed;
298 normalization_gain_ = normalization;
299 for (
auto& cascade : cascades_)
301 cascade.set_coefficients(std::span<const Coefficients, shelf_count>(coefficients_),
305 void reset_history() noexcept {
306 for (
auto& cascade : cascades_)
310 SampleType recover(std::size_t channel)
noexcept {
311 cascades_[channel].reset();
313 return SampleType{0};
316 std::array<Cascade, Channels> cascades_{};
317 std::array<Coefficients, shelf_count> coefficients_{};
320 double normalization_gain_ = 1.0;
321 std::uint64_t fault_count_ = 0;
@ high_shelf
Definition biquad.hpp:58
Executes a bounded cascade of normalized second-order sections (SOS).
Definition sos_cascade.hpp:60
void reset() noexcept
Definition sos_cascade.hpp:148
A reusable spectral slope built from a fixed cascade of high shelves.
Definition tilt_eq.hpp:34
double design_high_hz() const noexcept
Definition tilt_eq.hpp:99
BiquadCoefficientsT< SampleType > Coefficients
Definition tilt_eq.hpp:50
static constexpr int latency_samples() noexcept
Definition tilt_eq.hpp:188
static constexpr double default_sample_rate
Definition tilt_eq.hpp:48
double sample_rate() const noexcept
Definition tilt_eq.hpp:96
static constexpr double max_sample_rate
Definition tilt_eq.hpp:43
TiltEqT() noexcept
Definition tilt_eq.hpp:57
static constexpr double min_tilt_db_per_octave
Definition tilt_eq.hpp:44
bool process_block(SampleType *samples, std::size_t frames, std::size_t channel=0) noexcept
Process a caller-owned channel in place.
Definition tilt_eq.hpp:132
bool reset(std::size_t channel) noexcept
Clear one channel's recursive history. Invalid channels are rejected.
Definition tilt_eq.hpp:159
bool process_block(const SampleType *input, SampleType *output, std::size_t frames, std::size_t channel=0) noexcept
Process one channel into separate caller-owned storage.
Definition tilt_eq.hpp:142
double normalization_gain() const noexcept
Definition tilt_eq.hpp:102
std::uint64_t fault_count() const noexcept
Definition tilt_eq.hpp:105
SampleType process(SampleType input, std::size_t channel=0) noexcept
Process one channel.
Definition tilt_eq.hpp:116
double magnitude_db(double frequency_hz) const noexcept
Definition tilt_eq.hpp:180
void reset() noexcept
Clear all recursive history and the fault diagnostic without changing sample rate or configuration.
Definition tilt_eq.hpp:153
bool set_config(Config requested) noexcept
Apply one coherent pivot/slope request at a block boundary.
Definition tilt_eq.hpp:81
static constexpr double max_tilt_db_per_octave
Definition tilt_eq.hpp:45
bool prepare(double sample_rate) noexcept
Change sample rate and clear history. Invalid rates reject atomically.
Definition tilt_eq.hpp:67
double magnitude(double frequency_hz) const noexcept
Magnitude response of the active coefficient endpoint.
Definition tilt_eq.hpp:167
static constexpr double min_sample_rate
Definition tilt_eq.hpp:42
static constexpr double max_design_hz
Definition tilt_eq.hpp:47
Config config() const noexcept
Definition tilt_eq.hpp:93
static constexpr std::size_t shelf_count
Definition tilt_eq.hpp:41
static constexpr std::size_t channel_count
Definition tilt_eq.hpp:40
int tail_samples() const noexcept
Recursive shelves decay asymptotically when active.
Definition tilt_eq.hpp:194
Coefficients coefficients(std::size_t index) const noexcept
Definition tilt_eq.hpp:109
static constexpr double min_pivot_hz
Definition tilt_eq.hpp:46
Evaluate what a biquad (or a cascade of them) does to the spectrum.
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
double section_magnitude(const BiquadCoefficientsT< SampleType > &c, double omega)
Linear magnitude |H(e^jω)| of one section at normalized angular frequency ω = 2π·f/fs (radians/sample...
Definition frequency_response.hpp:45
@ reset_state
Definition sos_cascade.hpp:27
@ c
Definition spectrum_trace.hpp:35
TiltEqT< double > TiltEq64
Definition tilt_eq.hpp:325
bool biquad_is_stable(const BiquadCoefficientsT< SampleType > &c) noexcept
Return whether a normalized real biquad's poles are strictly inside the unit circle.
Definition biquad.hpp:34
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
TiltEqT< float > TiltEq
Definition tilt_eq.hpp:324
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
Fixed-capacity runtime executor for cascades of second-order IIR sections.
Definition tilt_eq.hpp:52
double pivot_hz
Definition tilt_eq.hpp:53
double tilt_db_per_octave
Definition tilt_eq.hpp:54