Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
true_peak_limiter.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <cstddef>
12#include <cstdint>
13#include <limits>
14#include <span>
15#include <type_traits>
16#include <vector>
17
18namespace pulp::signal {
19
20template <typename SampleType = float, std::size_t MaxChannels = 2> class TruePeakLimiterT {
21 struct ScaledPeak {
22 double mantissa = 0.0;
23 int exponent = 0;
24 };
25
26 public:
27 static_assert(std::is_floating_point_v<SampleType>);
28 static_assert(MaxChannels > 0);
29
31
32 struct Params {
33 double ceiling_dbtp = -1.0;
34 double lookahead_ms = 5.0;
35 double release_ms = 100.0;
37 };
38
39 static constexpr std::size_t interpolation_factor() noexcept {
40 return 8;
41 }
42 static constexpr std::size_t interpolation_taps() noexcept {
43 return 129;
44 }
45 static constexpr int detector_latency_samples() noexcept {
46 return static_cast<int>((interpolation_taps() - 1) / 2);
47 }
48
49 static constexpr int internal_gain_lookahead_samples() noexcept {
51 }
52 static constexpr double detector_guard_db() noexcept {
53 return 0.50;
54 }
55 static constexpr double maximum_supported_sample_rate() noexcept {
56 return 384000.0;
57 }
58 static constexpr double maximum_lookahead_ms() noexcept {
59 return 20.0;
60 }
61 static constexpr std::size_t maximum_supported_channels() noexcept {
62 return MaxChannels;
63 }
64 static constexpr std::size_t detector_mac_count_per_channel() noexcept {
65 // Phase zero is the exact centre sample. Only the seven fractional
66 // phases require FIR dot products.
67 return 1 + (interpolation_factor() - 1) * interpolation_taps();
68 }
69
70 bool prepare(double sample_rate, std::size_t channels, Params params = {}) {
71 if (!(std::isfinite(sample_rate) && sample_rate >= 8000.0 &&
72 sample_rate <= maximum_supported_sample_rate()) ||
73 channels == 0 || channels > MaxChannels || !valid_params(params))
74 return false;
75
76 sample_rate_ = sample_rate;
77 channels_ = channels;
78 params_ = sanitized(params);
79 lookahead_samples_ =
80 static_cast<int>(std::ceil(params_.lookahead_ms * 0.001 * sample_rate_));
81 gain_lookahead_samples_ = internal_gain_lookahead_samples_ + lookahead_samples_;
82 latency_samples_ = detector_latency_samples() + gain_lookahead_samples_;
83 delay_.assign((static_cast<std::size_t>(latency_samples_) + 1) * channels_, SampleType{0});
84 for (std::size_t channel = 0; channel < channels_; ++channel) {
85 peak_values_[channel].assign(static_cast<std::size_t>(gain_lookahead_samples_) + 2,
86 ScaledPeak{});
87 peak_indices_[channel].assign(static_cast<std::size_t>(gain_lookahead_samples_) + 2, 0);
88 }
89 design_interpolator();
90 update_control_coefficients();
91 prepared_ = true;
92 reset();
93 return true;
94 }
95
96 void reset() noexcept {
97 for (auto& history : histories_)
98 history.fill(0.0);
99 std::fill(delay_.begin(), delay_.end(), SampleType{0});
100 history_write_ = 0;
101 delay_write_frame_ = 0;
102 frame_index_ = 0;
103 fault_count_ = 0;
104 for (std::size_t channel = 0; channel < MaxChannels; ++channel) {
105 queue_head_[channel] = 0;
106 queue_size_[channel] = 0;
107 gain_[channel] = 1.0;
108 }
109 }
110
111 bool set_ceiling_dbtp(double dbtp) noexcept {
112 if (!std::isfinite(dbtp))
113 return false;
114 const double clamped = std::clamp(dbtp, -24.0, 0.0);
115 if (clamped == params_.ceiling_dbtp)
116 return true;
117 params_.ceiling_dbtp = clamped;
118 update_control_coefficients();
119 return true;
120 }
121
122 bool set_release_ms(double milliseconds) noexcept {
123 if (!std::isfinite(milliseconds))
124 return false;
125 const double clamped = std::clamp(milliseconds, 5.0, 2000.0);
126 if (clamped == params_.release_ms)
127 return true;
128 params_.release_ms = clamped;
129 update_control_coefficients();
130 return true;
131 }
132
136 bool set_realtime_control_coefficients(double ceiling_dbtp, double safe_ceiling_linear,
137 double release_ms, double release_retain) noexcept {
138 if (!std::isfinite(ceiling_dbtp) || !std::isfinite(safe_ceiling_linear) ||
139 !std::isfinite(release_ms) || !std::isfinite(release_retain) || ceiling_dbtp < -24.0 ||
140 ceiling_dbtp > 0.0 || safe_ceiling_linear <= 0.0 || safe_ceiling_linear > 1.0 ||
141 release_ms < 5.0 || release_ms > 2000.0 || release_retain < 0.0 || release_retain > 1.0)
142 return false;
143 params_.ceiling_dbtp = ceiling_dbtp;
144 params_.release_ms = release_ms;
145 safe_ceiling_ = safe_ceiling_linear;
146 release_retain_ = release_retain;
147 return true;
148 }
149
150 double ceiling_dbtp() const noexcept {
151 return params_.ceiling_dbtp;
152 }
153 double lookahead_ms() const noexcept {
154 return params_.lookahead_ms;
155 }
156 double release_ms() const noexcept {
157 return params_.release_ms;
158 }
159 ChannelLink channel_link() const noexcept {
160 return params_.channel_link;
161 }
162 std::size_t channel_count() const noexcept {
163 return prepared_ ? channels_ : 0;
164 }
165 int latency_samples() const noexcept {
166 return prepared_ ? latency_samples_ : 0;
167 }
168 int tail_samples() const noexcept {
169 return latency_samples();
170 }
171 bool prepared() const noexcept {
172 return prepared_;
173 }
174 std::uint64_t fault_count() const noexcept {
175 return fault_count_;
176 }
177
179 double gain_reduction_db(std::size_t channel = 0) const noexcept {
180 if (!prepared_ || channel >= channels_)
181 return 0.0;
182 const double gain = gain_[params_.channel_link == ChannelLink::linked ? 0 : channel];
183 return gain > 0.0 ? -20.0 * std::log10(gain) : std::numeric_limits<double>::infinity();
184 }
185
188 std::array<double, interpolation_factor()>
189 detector_phases(std::span<const double, interpolation_taps()> newest_to_oldest) const noexcept {
190 std::array<double, interpolation_factor()> result{};
191 for (std::size_t phase = 0; phase < interpolation_factor(); ++phase)
192 for (std::size_t tap = 0; tap < interpolation_taps(); ++tap)
193 result[phase] += interpolation_[phase][tap] * newest_to_oldest[tap];
194 return result;
195 }
196
197 bool process_frame(std::span<const SampleType> input, std::span<SampleType> output) noexcept {
198 if (!prepared_ || input.size() != channels_ || output.size() != channels_)
199 return false;
200 for (std::size_t channel = 0; channel < channels_; ++channel) {
201 if (!std::isfinite(static_cast<double>(input[channel]))) {
202 ++fault_count_;
203 reset_after_fault();
204 std::fill(output.begin(), output.end(), SampleType{0});
205 return false;
206 }
207 }
208
209 std::array<ScaledPeak, MaxChannels> detected{};
210 for (std::size_t channel = 0; channel < channels_; ++channel) {
211 histories_[channel][history_write_] = static_cast<double>(input[channel]);
212 histories_[channel][history_write_ + interpolation_taps()] =
213 static_cast<double>(input[channel]);
214 detected[channel] = interpolated_peak(channel);
215 }
216 if (++history_write_ == interpolation_taps())
217 history_write_ = 0;
218
219 if (params_.channel_link == ChannelLink::linked) {
220 ScaledPeak peak{};
221 for (std::size_t channel = 0; channel < channels_; ++channel)
222 if (peak_less(peak, detected[channel]))
223 peak = detected[channel];
224 update_gain(0, peak);
225 } else {
226 for (std::size_t channel = 0; channel < channels_; ++channel)
227 update_gain(channel, detected[channel]);
228 }
229
230 const std::size_t delay_frames = static_cast<std::size_t>(latency_samples_) + 1;
231 const std::size_t read_frame =
232 (delay_write_frame_ + delay_frames - static_cast<std::size_t>(latency_samples_)) %
233 delay_frames;
234 for (std::size_t channel = 0; channel < channels_; ++channel) {
235 delay_[delay_write_frame_ * channels_ + channel] = input[channel];
236 const double gain = gain_[params_.channel_link == ChannelLink::linked ? 0 : channel];
237 const double sample =
238 static_cast<double>(delay_[read_frame * channels_ + channel]) * gain;
239 output[channel] =
240 std::isfinite(sample) ? static_cast<SampleType>(sample) : SampleType{0};
241 }
242 delay_write_frame_ = (delay_write_frame_ + 1) % delay_frames;
243 ++frame_index_;
244 return true;
245 }
246
247 bool process_interleaved(const SampleType* input, SampleType* output, std::size_t frames,
248 std::size_t channels) noexcept {
249 if (!prepared_ || input == nullptr || output == nullptr || channels != channels_)
250 return false;
251 for (std::size_t frame = 0; frame < frames; ++frame) {
252 const std::span<const SampleType> in(input + frame * channels_, channels_);
253 const std::span<SampleType> out(output + frame * channels_, channels_);
254 if (!process_frame(in, out))
255 return false;
256 }
257 return true;
258 }
259
260 private:
261#if defined(PULP_TRUE_PEAK_LIMITER_TEST_SEAMS)
262 friend struct TruePeakLimiterTestAccess;
263#endif
264
265 static constexpr double pi() noexcept {
266 return 3.141592653589793238462643383279502884;
267 }
268
269 static bool valid_params(const Params& params) noexcept {
270 return std::isfinite(params.ceiling_dbtp) && std::isfinite(params.lookahead_ms) &&
271 std::isfinite(params.release_ms) && params.ceiling_dbtp >= -24.0 &&
272 params.ceiling_dbtp <= 0.0 && params.lookahead_ms >= 0.0 &&
273 params.lookahead_ms <= maximum_lookahead_ms() && params.release_ms >= 5.0 &&
274 params.release_ms <= 2000.0;
275 }
276
277 static Params sanitized(Params params) noexcept {
278 params.ceiling_dbtp = std::clamp(params.ceiling_dbtp, -24.0, 0.0);
279 params.lookahead_ms = std::clamp(params.lookahead_ms, 0.0, maximum_lookahead_ms());
280 params.release_ms = std::clamp(params.release_ms, 5.0, 2000.0);
281 return params;
282 }
283
284 static double bessel_i0(double x) noexcept {
285 double sum = 1.0;
286 double term = 1.0;
287 const double quarter = x * x * 0.25;
288 for (int k = 1; k <= 24; ++k) {
289 term *= quarter / static_cast<double>(k * k);
290 sum += term;
291 }
292 return sum;
293 }
294
295 void design_interpolator() noexcept {
296 constexpr double beta = 10.5;
297 const double denominator = bessel_i0(beta);
298 const double center = static_cast<double>(detector_latency_samples());
299 for (std::size_t phase = 0; phase < interpolation_factor(); ++phase) {
300 const double fraction =
301 static_cast<double>(phase) / static_cast<double>(interpolation_factor());
302 double sum = 0.0;
303 for (std::size_t tap = 0; tap < interpolation_taps(); ++tap) {
304 const double offset = static_cast<double>(tap) - center + fraction;
305 const double sinc =
306 std::abs(offset) < 1.0e-15 ? 1.0 : std::sin(pi() * offset) / (pi() * offset);
307 const double position = (static_cast<double>(tap) - center) / center;
308 const double window =
309 bessel_i0(beta * std::sqrt(std::max(0.0, 1.0 - position * position))) /
310 denominator;
311 interpolation_[phase][tap] = sinc * window;
312 sum += interpolation_[phase][tap];
313 }
314 for (double& coefficient : interpolation_[phase])
315 coefficient /= sum;
316 }
317 }
318
319 static bool peak_less(const ScaledPeak& lhs, const ScaledPeak& rhs) noexcept {
320 if (lhs.mantissa == 0.0)
321 return rhs.mantissa != 0.0;
322 if (rhs.mantissa == 0.0)
323 return false;
324 return lhs.exponent != rhs.exponent ? lhs.exponent < rhs.exponent
325 : lhs.mantissa < rhs.mantissa;
326 }
327
328 static ScaledPeak scaled_peak(double mantissa, int exponent) noexcept {
329 if (!(mantissa > 0.0))
330 return {};
331 int adjustment = 0;
332 mantissa = std::frexp(mantissa, &adjustment);
333 return {mantissa, exponent + adjustment};
334 }
335
336 ScaledPeak interpolated_peak(std::size_t channel) const noexcept {
337 const auto* chronological = histories_[channel].data() + history_write_ + 1;
338 double scale = 0.0;
339 for (std::size_t tap = 0; tap < interpolation_taps(); ++tap)
340 scale = std::max(scale, std::abs(chronological[tap]));
341 if (scale == 0.0)
342 return {};
343
344 // Phase zero is the exact centre sample. Fractional-phase coefficient
345 // rows are stored oldest-to-newest so these contiguous loops vectorize.
346 double normalized_peak =
347 std::abs(chronological[static_cast<std::size_t>(detector_latency_samples())]) / scale;
348 const bool reciprocal_is_finite = scale >= std::numeric_limits<double>::min();
349 const double inverse_scale = reciprocal_is_finite ? 1.0 / scale : 0.0;
350 for (std::size_t phase = 1; phase < interpolation_factor(); ++phase) {
351 double value = 0.0;
352 for (std::size_t tap = 0; tap < interpolation_taps(); ++tap) {
353 const double normalized = reciprocal_is_finite ? chronological[tap] * inverse_scale
354 : chronological[tap] / scale;
355 value += interpolation_[phase][interpolation_taps() - 1 - tap] * normalized;
356 }
357 normalized_peak = std::max(normalized_peak, std::abs(value));
358 }
359
360 int scale_exponent = 0;
361 const double scale_mantissa = std::frexp(scale, &scale_exponent);
362 return scaled_peak(scale_mantissa * normalized_peak, scale_exponent);
363 }
364
365 double required_gain(const ScaledPeak& peak) const noexcept {
366 if (peak.mantissa == 0.0)
367 return 1.0;
368 int ceiling_exponent = 0;
369 const double ceiling_mantissa = std::frexp(safe_ceiling_, &ceiling_exponent);
370 const int shift = ceiling_exponent - peak.exponent;
371 if (shift < std::numeric_limits<double>::min_exponent - 2)
372 return 0.0;
373 if (shift > 1)
374 return 1.0;
375 return std::clamp(std::ldexp(ceiling_mantissa / peak.mantissa, shift), 0.0, 1.0);
376 }
377
378 void update_gain(std::size_t lane, ScaledPeak peak) noexcept {
379 auto& values = peak_values_[lane];
380 auto& indices = peak_indices_[lane];
381 const std::size_t capacity = values.size();
382 while (queue_size_[lane] > 0) {
383 const std::size_t back = (queue_head_[lane] + queue_size_[lane] - 1) % capacity;
384 if (peak_less(peak, values[back]))
385 break;
386 --queue_size_[lane];
387 }
388 const std::size_t write = (queue_head_[lane] + queue_size_[lane]) % capacity;
389 values[write] = peak;
390 indices[write] = frame_index_;
391 ++queue_size_[lane];
392 const std::uint64_t oldest =
393 frame_index_ > static_cast<std::uint64_t>(gain_lookahead_samples_)
394 ? frame_index_ - static_cast<std::uint64_t>(gain_lookahead_samples_)
395 : 0;
396 while (queue_size_[lane] > 0 && indices[queue_head_[lane]] < oldest) {
397 queue_head_[lane] = (queue_head_[lane] + 1) % capacity;
398 --queue_size_[lane];
399 }
400
401 const ScaledPeak future_peak =
402 queue_size_[lane] == 0 ? ScaledPeak{} : values[queue_head_[lane]];
403 const double required = required_gain(future_peak);
404 if (required < gain_[lane]) {
405 gain_[lane] = required;
406 } else {
407 gain_[lane] = required + release_retain_ * (gain_[lane] - required);
408 }
409 gain_[lane] = std::clamp(gain_[lane], 0.0, 1.0);
410 }
411
412 void reset_after_fault() noexcept {
413 const auto faults = fault_count_;
414 reset();
415 fault_count_ = faults;
416 }
417
418 void update_control_coefficients() noexcept {
419 safe_ceiling_ = std::pow(10.0, (params_.ceiling_dbtp - detector_guard_db()) / 20.0);
420 release_retain_ = dynamics::one_pole_retain(params_.release_ms * 0.001, sample_rate_);
421 }
422
423 Params params_{};
424 double sample_rate_ = 0.0;
425 double safe_ceiling_ = 1.0;
426 double release_retain_ = 0.0;
427 std::size_t channels_ = 0;
428 int lookahead_samples_ = 0;
429 int internal_gain_lookahead_samples_ = internal_gain_lookahead_samples();
430 int gain_lookahead_samples_ = 0;
431 int latency_samples_ = 0;
432 bool prepared_ = false;
433 std::uint64_t frame_index_ = 0;
434 std::uint64_t fault_count_ = 0;
435 std::size_t history_write_ = 0;
436 std::size_t delay_write_frame_ = 0;
437 std::array<std::array<double, interpolation_taps()>, interpolation_factor()> interpolation_{};
438 std::array<std::array<double, interpolation_taps() * 2>, MaxChannels> histories_{};
439 std::vector<SampleType> delay_{};
440 std::array<std::vector<ScaledPeak>, MaxChannels> peak_values_{};
441 std::array<std::vector<std::uint64_t>, MaxChannels> peak_indices_{};
442 std::array<std::size_t, MaxChannels> queue_head_{};
443 std::array<std::size_t, MaxChannels> queue_size_{};
444 std::array<double, MaxChannels> gain_{};
445};
446
449
450} // namespace pulp::signal
Definition true_peak_limiter.hpp:20
static constexpr double detector_guard_db() noexcept
Definition true_peak_limiter.hpp:52
static constexpr std::size_t interpolation_taps() noexcept
Definition true_peak_limiter.hpp:42
void reset() noexcept
Definition true_peak_limiter.hpp:96
static constexpr double maximum_lookahead_ms() noexcept
Definition true_peak_limiter.hpp:58
static constexpr std::size_t maximum_supported_channels() noexcept
Definition true_peak_limiter.hpp:61
std::array< double, interpolation_factor()> detector_phases(std::span< const double, interpolation_taps()> newest_to_oldest) const noexcept
Reconstruct the eight detector phases for one channel without changing limiter state.
Definition true_peak_limiter.hpp:189
ChannelLink channel_link() const noexcept
Definition true_peak_limiter.hpp:159
bool process_interleaved(const SampleType *input, SampleType *output, std::size_t frames, std::size_t channels) noexcept
Definition true_peak_limiter.hpp:247
bool prepare(double sample_rate, std::size_t channels, Params params={})
Definition true_peak_limiter.hpp:70
int tail_samples() const noexcept
Definition true_peak_limiter.hpp:168
double ceiling_dbtp() const noexcept
Definition true_peak_limiter.hpp:150
static constexpr std::size_t detector_mac_count_per_channel() noexcept
Definition true_peak_limiter.hpp:64
bool process_frame(std::span< const SampleType > input, std::span< SampleType > output) noexcept
Definition true_peak_limiter.hpp:197
bool set_realtime_control_coefficients(double ceiling_dbtp, double safe_ceiling_linear, double release_ms, double release_retain) noexcept
Install already-computed control coefficients without calling libm.
Definition true_peak_limiter.hpp:136
double lookahead_ms() const noexcept
Definition true_peak_limiter.hpp:153
int latency_samples() const noexcept
Definition true_peak_limiter.hpp:165
bool prepared() const noexcept
Definition true_peak_limiter.hpp:171
static constexpr int internal_gain_lookahead_samples() noexcept
Fixed future-peak horizon required by the output gain scheduler.
Definition true_peak_limiter.hpp:49
bool set_release_ms(double milliseconds) noexcept
Definition true_peak_limiter.hpp:122
double gain_reduction_db(std::size_t channel=0) const noexcept
Current attenuation as a non-negative dB magnitude.
Definition true_peak_limiter.hpp:179
static constexpr std::size_t interpolation_factor() noexcept
Definition true_peak_limiter.hpp:39
bool set_ceiling_dbtp(double dbtp) noexcept
Definition true_peak_limiter.hpp:111
std::size_t channel_count() const noexcept
Definition true_peak_limiter.hpp:162
ChannelLink
Definition true_peak_limiter.hpp:30
@ linked
Definition true_peak_limiter.hpp:30
@ independent
Definition true_peak_limiter.hpp:30
double release_ms() const noexcept
Definition true_peak_limiter.hpp:156
std::uint64_t fault_count() const noexcept
Definition true_peak_limiter.hpp:174
static constexpr int detector_latency_samples() noexcept
Definition true_peak_limiter.hpp:45
static constexpr double maximum_supported_sample_rate() noexcept
Definition true_peak_limiter.hpp:55
The published compressor equations, once.
@ back
Definition gpu_pass.hpp:283
double one_pole_retain(double tau_seconds, double sample_rate)
α = exp(−1/(τ·fs)) — the RETAIN coefficient (see the file doc block).
Definition dynamics_core.hpp:155
void write(std::uint8_t *b, std::size_t p, std::uint64_t v, unsigned n) noexcept
Definition spectral_frame_codec.hpp:117
@ quarter
Definition units.hpp:293
Definition additive_bank.hpp:218
float sinc(float x)
Sinc function: sin(pi*x) / (pi*x), with sinc(0) = 1.
Definition special_functions.hpp:17
@ center
Definition multi_channel_meter.hpp:68
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
TruePeakLimiterT< double > TruePeakLimiter64
Definition true_peak_limiter.hpp:448
@ oldest
Recycle the grain furthest through its window — the quietest one when durations match,...
Definition granular.hpp:123
TruePeakLimiterT< float > TruePeakLimiter
Definition true_peak_limiter.hpp:447
float sum(const float *data, std::size_t count) noexcept
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
Definition true_peak_limiter.hpp:32
double release_ms
Definition true_peak_limiter.hpp:35
ChannelLink channel_link
Definition true_peak_limiter.hpp:36
double lookahead_ms
Definition true_peak_limiter.hpp:34
double ceiling_dbtp
Definition true_peak_limiter.hpp:33