Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tilt_eq.hpp
Go to the documentation of this file.
1#pragma once
2
5
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <span>
16#include <type_traits>
17
18namespace pulp::signal {
19
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");
38
39 public:
40 static constexpr std::size_t channel_count = Channels;
41 static constexpr std::size_t shelf_count = 9;
42 static constexpr double min_sample_rate = 8000.0;
43 static constexpr double max_sample_rate = 384000.0;
44 static constexpr double min_tilt_db_per_octave = -6.0;
45 static constexpr double max_tilt_db_per_octave = 6.0;
46 static constexpr double min_pivot_hz = 31.25;
47 static constexpr double max_design_hz = 16000.0;
48 static constexpr double default_sample_rate = 48000.0;
49
51
52 struct Config {
53 double pivot_hz = 1000.0;
54 double tilt_db_per_octave = 0.0;
55 };
56
57 TiltEqT() noexcept {
58 for (auto& cascade : cascades_)
59 static_cast<void>(cascade.prepare(shelf_count));
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);
64 }
65
67 bool prepare(double sample_rate) noexcept {
68 if (!valid_sample_rate(sample_rate))
69 return false;
70 std::array<Coefficients, shelf_count> designed{};
71 double normalization = 1.0;
72 if (!design(config_, sample_rate, designed, normalization))
73 return false;
74 sample_rate_ = sample_rate;
75 commit(designed, normalization);
76 return true;
77 }
78
81 bool set_config(Config requested) noexcept {
82 if (same_config(requested, config_))
83 return true;
84 std::array<Coefficients, shelf_count> designed{};
85 double normalization = 1.0;
86 if (!design(requested, sample_rate_, designed, normalization))
87 return false;
88 config_ = requested;
89 commit(designed, normalization);
90 return true;
91 }
92
93 Config config() const noexcept {
94 return config_;
95 }
96 double sample_rate() const noexcept {
97 return sample_rate_;
98 }
99 double design_high_hz() const noexcept {
100 return std::min(max_design_hz, 0.4 * sample_rate_);
101 }
102 double normalization_gain() const noexcept {
103 return normalization_gain_;
104 }
105 std::uint64_t fault_count() const noexcept {
106 return fault_count_;
107 }
108
109 Coefficients coefficients(std::size_t index) const noexcept {
110 return index < shelf_count ? coefficients_[index] : Coefficients{};
111 }
112
116 SampleType process(SampleType input, std::size_t channel = 0) noexcept {
117 if (channel >= Channels)
118 return input;
119 if (!std::isfinite(static_cast<double>(input)))
120 return recover(channel);
121 if (config_.tilt_db_per_octave == 0.0)
122 return input;
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);
127 return output;
128 }
129
132 bool process_block(SampleType* samples, std::size_t frames, std::size_t channel = 0) noexcept {
133 if (channel >= Channels || (samples == nullptr && frames != 0))
134 return false;
135 for (std::size_t frame = 0; frame < frames; ++frame)
136 samples[frame] = process(samples[frame], channel);
137 return true;
138 }
139
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))
145 return false;
146 for (std::size_t frame = 0; frame < frames; ++frame)
147 output[frame] = process(input[frame], channel);
148 return true;
149 }
150
153 void reset() noexcept {
154 reset_history();
155 fault_count_ = 0;
156 }
157
159 bool reset(std::size_t channel) noexcept {
160 if (channel >= Channels)
161 return false;
162 cascades_[channel].reset();
163 return true;
164 }
165
167 double magnitude(double frequency_hz) const noexcept {
168 if (config_.tilt_db_per_octave == 0.0)
169 return 1.0;
170 if (!std::isfinite(frequency_hz))
171 frequency_hz = 0.0;
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_)
176 result *= section_magnitude(section, omega);
177 return std::isfinite(result) ? result : 0.0;
178 }
179
180 double magnitude_db(double frequency_hz) const noexcept {
181 const double linear = magnitude(frequency_hz);
182 if (!(linear > 0.0))
183 return -200.0;
184 const double db = 20.0 * std::log10(linear);
185 return std::isfinite(db) ? std::max(db, -200.0) : -200.0;
186 }
187
188 static constexpr int latency_samples() noexcept {
189 return 0;
190 }
191
194 int tail_samples() const noexcept {
195 return config_.tilt_db_per_octave == 0.0 ? 0 : -1;
196 }
197
198 private:
200
201 static bool valid_sample_rate(double sample_rate) noexcept {
202 return std::isfinite(sample_rate) && sample_rate >= min_sample_rate &&
204 }
205
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;
208 }
209
210 static bool finite(const Coefficients& c) noexcept {
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));
215 }
216
217 static bool design_sections(double stage_gain_db, double sample_rate, double high_hz,
218 std::array<Coefficients, shelf_count>& designed) noexcept {
219 const double ratio = high_hz / min_pivot_hz;
220 for (std::size_t index = 0; index < shelf_count; ++index) {
221 const double position =
222 (static_cast<double>(index) + 0.5) / static_cast<double>(shelf_count);
223 const double center_hz = min_pivot_hz * std::pow(ratio, position);
224 BiquadT<SampleType> designer;
225 designer.set_coefficients(
226 BiquadT<SampleType>::Type::high_shelf, static_cast<SampleType>(center_hz),
227 static_cast<SampleType>(0.7071067811865476), static_cast<SampleType>(sample_rate),
228 static_cast<SampleType>(stage_gain_db));
229 designed[index] = designer.coefficients();
230 if (!finite(designed[index]) || !biquad_is_stable(designed[index]))
231 return false;
232 }
233 return true;
234 }
235
236 static double endpoint_delta_db(const std::array<Coefficients, shelf_count>& designed,
237 double sample_rate, double high_hz) noexcept {
238 const double scale = 2.0 * std::acos(-1.0) / sample_rate;
239 double low_magnitude = 1.0;
240 double high_magnitude = 1.0;
241 for (const auto& section : designed) {
242 low_magnitude *= section_magnitude(section, scale * min_pivot_hz);
243 high_magnitude *= section_magnitude(section, scale * high_hz);
244 }
245 return 20.0 * std::log10(high_magnitude / low_magnitude);
246 }
247
248 static bool design(const Config& requested, double sample_rate,
249 std::array<Coefficients, shelf_count>& designed,
250 double& normalization) noexcept {
251 const double high_hz = std::min(max_design_hz, 0.4 * sample_rate);
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 ||
254 requested.pivot_hz > high_hz || requested.tilt_db_per_octave < min_tilt_db_per_octave ||
255 requested.tilt_db_per_octave > max_tilt_db_per_octave)
256 return false;
257
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))
262 return false;
263 } else {
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)
270 return false;
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))
274 return false;
275 if (std::abs(endpoint_delta_db(designed, sample_rate, high_hz)) < target_magnitude)
276 low_gain = candidate_gain;
277 else
278 high_gain = candidate_gain;
279 }
280 if (!design_sections(direction * (low_gain + high_gain) * 0.5, sample_rate, high_hz,
281 designed))
282 return false;
283 }
284
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)
288 pivot_magnitude *= section_magnitude(section, pivot_omega);
289 if (!(std::isfinite(pivot_magnitude) && pivot_magnitude > 0.0))
290 return false;
291 normalization = 1.0 / pivot_magnitude;
292 return std::isfinite(normalization) && normalization > 0.0;
293 }
294
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_)
300 static_cast<void>(
301 cascade.set_coefficients(std::span<const Coefficients, shelf_count>(coefficients_),
303 }
304
305 void reset_history() noexcept {
306 for (auto& cascade : cascades_)
307 cascade.reset();
308 }
309
310 SampleType recover(std::size_t channel) noexcept {
311 cascades_[channel].reset();
312 ++fault_count_;
313 return SampleType{0};
314 }
315
316 std::array<Cascade, Channels> cascades_{};
317 std::array<Coefficients, shelf_count> coefficients_{};
318 Config config_{};
319 double sample_rate_ = default_sample_rate;
320 double normalization_gain_ = 1.0;
321 std::uint64_t fault_count_ = 0;
322};
323
326
327} // namespace pulp::signal
@ 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 biquad.hpp:24
Definition tilt_eq.hpp:52
double pivot_hz
Definition tilt_eq.hpp:53
double tilt_db_per_octave
Definition tilt_eq.hpp:54