DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
ContinuousClip.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
14#include "ClipperShape.h"
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <limits>
19#include <vector>
20
22{
24using Moments = std::array<double, 4>;
25inline double choose(int n, int k)
26{
27 double v = 1;
28 for (int i = 1; i <= k; ++i)
29 v *= double(n - i + 1) / i;
30 return v;
31}
32template <int N> struct Gauss
33{
34 std::array<double, N> nodes{}, weights{};
36 {
37 for (int i = 0; i < (N + 1) / 2; ++i)
38 {
39 double x = std::cos(pi<double> * (i + .75) / (N + .5)), derivative = 0;
40 for (int it = 0; it < 30; ++it)
41 {
42 double p0 = 1, p1 = x;
43 for (int j = 2; j <= N; ++j)
44 {
45 const double p = ((2 * j - 1) * x * p1 - (j - 1) * p0) / j;
46 p0 = p1;
47 p1 = p;
48 }
49 derivative = N * (x * p1 - p0) / (x * x - 1);
50 const double step = p1 / derivative;
51 x -= step;
52 if (std::abs(step) < 2e-16)
53 break;
54 }
55 const double w = 1 / ((1 - x * x) * derivative * derivative);
56 nodes[i] = (1 - x) / 2;
57 nodes[N - 1 - i] = (1 + x) / 2;
58 weights[i] = weights[N - 1 - i] = w;
59 }
60 }
61};
62
63template <Curve C, int Degree, bool NativeSlope = false, bool SubtractLinear = true,
64 int MomentCount = 4> class Interval
65{
66 static_assert(Degree >= 1 && Degree <= 11 && Degree % 2 == 1);
67 static_assert(MomentCount >= 1 && MomentCount <= 8);
68 static_assert(Degree + MomentCount <= 16); // Eight-point exact polynomial quadrature.
69 using Moments = std::array<double, MomentCount>;
70 static constexpr bool rational =
71 C == Curve::GoldenRatio || C == Curve::SymmetricKnee || C == Curve::AsymmetricKnee;
72 static constexpr int count = Degree + 1;
73 static constexpr double referenceSlope = NativeSlope ? clipperSmallSignalSlope<C, double>() : 1.;
74 using Poly = std::array<double, count>;
75 std::array<Poly, count> matrix_{}, bernsteinMatrix_{};
76 Gauss<8> linearQuadrature_;
77 Gauss<16> nonlinearQuadrature_;
78 double ceiling_;
79 double reconstructionBound_ = 1;
80 double arithmeticBound_ = 1;
81 std::array<double, 11> levels_{};
82 int levelCount_ = 0;
83 static double value(const Poly &p, double t)
84 {
85 double v = p[Degree];
86 for (int i = Degree - 1; i >= 0; --i)
87 v = v * t + p[i];
88 return v;
89 }
90 static double slope(const Poly &p, double t)
91 {
92 double v = Degree * p[Degree];
93 for (int i = Degree - 1; i >= 1; --i)
94 v = v * t + i * p[i];
95 return v;
96 }
97 static void add(Moments &a, const Moments &b)
98 {
99 for (int i = 0; i < MomentCount; ++i)
100 a[i] += b[i];
101 }
102 template <int N>
103 Moments quadrature(const Poly &p, double a, double b, const Gauss<N> &q, bool fixed = false,
104 double fixedValue = 0) const
105 {
106 Moments out{};
107 for (int i = 0; i < N; ++i)
108 {
109 const double t = a + (b - a) * q.nodes[i], x = value(p, t);
110 const double reference = SubtractLinear ? referenceSlope * x : 0.;
111 double v = (b - a) * q.weights[i] *
112 (fixed ? fixedValue - reference : clipperShape<C>(x, ceiling_) - reference);
113 for (int j = 0; j < MomentCount; ++j)
114 {
115 out[j] += v;
116 v *= t;
117 }
118 }
119 return out;
120 }
121 std::array<Moments, 2> tanhQuadrature(const Poly &p, double a, double b) const
122 {
123 // Embedded G7/K15 rules reuse seven evaluations in the error estimate.
124 // Symmetric abscissae and weights are defined on [-1,1]. This changes
125 // neither the curve nor the adaptive tolerance below.
126 static constexpr std::array<double, 8> nodes{
127 .99145537112081263921, .94910791234275852453, .86486442335976907279,
128 .74153118559939443986, .58608723546769113029, .40584515137739716691,
129 .20778495500789846760, 0.};
130 static constexpr std::array<double, 8> fineWeights{
131 .02293532201052922496, .06309209262997855329, .10479001032225018384,
132 .14065325971552591875, .16900472663926790283, .19035057806478540991,
133 .20443294007529889241, .20948214108472782801};
134 static constexpr std::array<double, 4> coarseWeights{
135 .12948496616886969327, .27970539148927666790,
136 .38183005050511894495, .41795918367346938776};
137 std::array<Moments, 2> result{};
138 const double half = (b - a) * .5, center = a + half;
139 const auto accumulate = [&](double t, double fineWeight, double coarseWeight)
140 {
141 const double x = value(p, t);
142 const double reference = SubtractLinear ? referenceSlope * x : 0.;
143 double v = half * (clipperShape<C>(x, ceiling_) - reference);
144 for (int j = 0; j < MomentCount; ++j)
145 {
146 result[1][j] += fineWeight * v;
147 if (coarseWeight != 0.) result[0][j] += coarseWeight * v;
148 v *= t;
149 }
150 };
151 accumulate(center, fineWeights[7], coarseWeights[3]);
152 for (int i = 0; i < 7; ++i)
153 {
154 const double coarseWeight = i % 2 ? coarseWeights[i / 2] : 0.;
155 accumulate(center - half * nodes[i], fineWeights[i], coarseWeight);
156 accumulate(center + half * nodes[i], fineWeights[i], coarseWeight);
157 }
158 return result;
159 }
160 Moments adaptive(const Poly &p, double a, double b, int depth = 0) const
161 {
162 const auto estimates = [&] {
163 if constexpr (C == Curve::Tanh) return tanhQuadrature(p, a, b);
164 else return std::array<Moments, 2>{quadrature(p, a, b, linearQuadrature_),
165 quadrature(p, a, b, nonlinearQuadrature_)};
166 }();
167 const auto &coarse = estimates[0], &fine = estimates[1];
168 const double scale =
169 1 + std::abs(value(p, a)) + std::abs(value(p, b)) + std::abs(value(p, (a + b) / 2));
170 const double tolerance = (b - a) * 1e-13 * scale;
171 double error = 0;
172 for (int i = 0; i < MomentCount; ++i)
173 error = std::max(error, std::abs(fine[i] - coarse[i]));
174 if (error <= tolerance)
175 return fine;
176 if (depth >= 24)
177 return {std::numeric_limits<double>::quiet_NaN()};
178 auto out = adaptive(p, a, (a + b) / 2, depth + 1);
179 add(out, adaptive(p, (a + b) / 2, b, depth + 1));
180 return out;
181 }
182 Moments linearMoments(const Poly &p, double a, double b) const
183 {
184 // Integrate a complete, provably linear interval algebraically. Retain
185 // the established four-moment arithmetic for existing callers.
186 if constexpr (MomentCount != 4)
187 {
188 if (a == 0 && b == 1)
189 {
190 static constexpr auto weights = [] {
191 std::array<Poly, MomentCount> result{};
192 for (int j = 0; j < MomentCount; ++j)
193 for (int k = 0; k < count; ++k)
194 result[j][k] = 1. / (j + k + 1);
195 return result;
196 }();
197 Moments out{};
198 for (int j = 0; j < MomentCount; ++j)
199 for (int k = 0; k < count; ++k)
200 out[j] += p[k] * weights[j][k];
201 return out;
202 }
203 }
204 return quadrature(p, a, b, linearQuadrature_);
205 }
206 Moments segment(const Poly &p, double a, double b) const
207 {
208 const double x = value(p, (a + b) / 2);
209 if constexpr (C == Curve::Hard)
210 {
211 if (std::abs(x) <= ceiling_)
212 {
213 if constexpr (SubtractLinear) return {};
214 else return linearMoments(p, a, b);
215 }
216 return quadrature(p, a, b, linearQuadrature_, true, std::copysign(ceiling_, x));
217 }
218 else if constexpr (C == Curve::Sine)
219 {
220 if (std::abs(x) >= ceiling_ * halfPi<double>)
221 return quadrature(p, a, b, linearQuadrature_, true, clipperShape<C>(x, ceiling_));
222 }
223 else if constexpr (rational)
224 {
225 const auto limits = clipperLinearLimits<C>(ceiling_);
226 if (x >= limits[0] && x <= limits[1])
227 {
228 if constexpr (SubtractLinear) return {};
229 else return linearMoments(p, a, b);
230 }
231 }
232 else if constexpr (C == Curve::Tanh)
233 {
234 if (std::abs(x) >= 24 * ceiling_)
235 return quadrature(p, a, b, linearQuadrature_, true, std::copysign(ceiling_, x));
236 }
237 if constexpr (C == Curve::Tanh || rational)
238 return adaptive(p, a, b);
239 else
240 return quadrature(p, a, b, nonlinearQuadrature_);
241 }
242 double root(const Poly &p, double a, double b, double target) const
243 {
244 double fa = value(p, a) - target, fb = value(p, b) - target;
245 if (fa == 0)
246 return a;
247 if (fb == 0)
248 return b;
249 if ((fa > 0) == (fb > 0))
250 return std::numeric_limits<double>::quiet_NaN();
251 double x = a + (b - a) * (-fa) / (fb - fa);
252 for (int i = 0; i < 64; ++i)
253 {
254 const double f = value(p, x) - target;
255 if (std::abs(f) <= 2e-15 || b - a < 2e-14)
256 return x;
257 if ((f > 0) == (fa > 0))
258 {
259 a = x;
260 fa = f;
261 }
262 else
263 {
264 b = x;
265 fb = f;
266 }
267 const double d = slope(p, x);
268 const double candidate = d != 0 ? x - f / d : (a + b) / 2;
269 x = candidate > a && candidate < b ? candidate : (a + b) / 2;
270 }
271 return std::numeric_limits<double>::quiet_NaN();
272 }
273 Moments recurse(const Poly &p, const Poly &bernstein, double a, double b, int depth) const
274 {
275 const auto bounds = std::minmax_element(bernstein.begin(), bernstein.end());
276 const double low = *bounds.first, high = *bounds.second;
277 if constexpr (C == Curve::Hard)
278 {
279 if (low >= -ceiling_ && high <= ceiling_)
280 {
281 if constexpr (SubtractLinear) return {};
282 else return linearMoments(p, a, b);
283 }
284 }
285 if constexpr (rational)
286 {
287 const auto limits = clipperLinearLimits<C>(ceiling_);
288 if (low >= limits[0] && high <= limits[1])
289 {
290 if constexpr (SubtractLinear) return {};
291 else return linearMoments(p, a, b);
292 }
293 }
294 bool crosses = false;
295 for (int i = 0; i < levelCount_; ++i)
296 crosses = crosses || (low < levels_[i] && levels_[i] < high);
297 if (!crosses)
298 return segment(p, a, b);
299 // Clipping curves are 1-Lipschitz; this branch's moment error is bounded
300 // by width*range. Its width-scaled bound also sums across all leaves.
301 if (high - low < 1e-13)
302 return quadrature(p, a, b, nonlinearQuadrature_);
303 bool increasing = true, decreasing = true;
304 for (int i = 1; i < count; ++i)
305 {
306 increasing = increasing && bernstein[i] >= bernstein[i - 1];
307 decreasing = decreasing && bernstein[i] <= bernstein[i - 1];
308 }
309 if (increasing || decreasing)
310 {
311 std::array<double, 13> edges{};
312 int n = 0;
313 edges[n++] = a;
314 const double va = value(p, a), vb = value(p, b);
315 for (int i = 0; i < levelCount_; ++i)
316 if (levels_[i] > std::min(va, vb) && levels_[i] < std::max(va, vb))
317 {
318 const double crossing = root(p, a, b, levels_[i]);
319 if (!std::isfinite(crossing))
320 return {std::numeric_limits<double>::quiet_NaN()};
321 edges[n++] = crossing;
322 }
323 edges[n++] = b;
324 std::sort(edges.begin(), edges.begin() + n);
325 Moments out{};
326 for (int i = 0; i + 1 < n; ++i)
327 add(out, segment(p, edges[i], edges[i + 1]));
328 return out;
329 }
330 if (depth >= 32)
331 return {std::numeric_limits<double>::quiet_NaN()};
332 Poly work = bernstein, left{}, right{};
333 left[0] = work[0];
334 right[Degree] = work[Degree];
335 for (int level = 1; level < count; ++level)
336 {
337 for (int i = 0; i < count - level; ++i)
338 work[i] = (work[i] + work[i + 1]) * .5;
339 left[level] = work[0];
340 right[Degree - level] = work[Degree - level];
341 }
342 auto out = recurse(p, left, a, (a + b) / 2, depth + 1);
343 add(out, recurse(p, right, (a + b) / 2, b, depth + 1));
344 return out;
345 }
346
347 public:
348 std::vector<double> linearKernel() const
349 {
350 static_assert(MomentCount == 4); // Cubic B-spline synthesis below.
351 constexpr std::array<Moments, 4> kernels{
352 {{1, -3, 3, -1}, {4, 0, -6, 3}, {1, 3, 3, -3}, {0, 0, 0, 1}}};
353 std::vector<double> result(Degree + 4);
354 for (int i = 0; i < count; ++i)
355 {
356 Moments moments{};
357 for (int j = 0; j < 4; ++j)
358 for (int k = 0; k < count; ++k)
359 moments[j] += matrix_[i][k] / (k + j + 1);
360 for (int lag = 0; lag < 4; ++lag)
361 for (int j = 0; j < 4; ++j)
362 result[Degree - i + lag] += moments[j] * kernels[lag][j] / 6;
363 }
364 return result;
365 }
366 explicit Interval(double ceiling) : ceiling_(ceiling)
367 {
368 constexpr int begin = -(Degree - 1) / 2;
369 for (int node = 0; node < count; ++node)
370 {
371 std::array<long double, count> p{};
372 p[0] = 1;
373 int order = 0;
374 const int x = begin + node;
375 for (int other = 0; other < count; ++other)
376 if (other != node)
377 {
378 const int y = begin + other;
379 std::array<long double, count> next{};
380 for (int j = 0; j <= order; ++j)
381 {
382 next[j] -= p[j] * y / (x - y);
383 next[j + 1] += p[j] / (x - y);
384 }
385 p = next;
386 ++order;
387 }
388 for (int j = 0; j < count; ++j)
389 matrix_[node][j] = static_cast<double>(p[j]);
390 }
391 for (int i = 0; i < count; ++i)
392 for (int j = 0; j <= i; ++j)
393 bernsteinMatrix_[i][j] = choose(i, j) / choose(Degree, j);
394 // Convex-hull bound on the complete reconstructed interval. It permits
395 // exact linear-region bypass before the per-sample matrix products.
396 for (int row = 0; row < count; ++row)
397 {
398 long double norm = 0, arithmeticNorm = 0;
399 for (int node = 0; node < count; ++node)
400 {
401 long double coefficient = 0;
402 for (int column = 0; column <= row; ++column)
403 {
404 coefficient += static_cast<long double>(bernsteinMatrix_[row][column]) *
405 matrix_[node][column];
406 arithmeticNorm +=
407 std::abs(static_cast<long double>(bernsteinMatrix_[row][column]) *
408 matrix_[node][column]);
409 }
410 norm += std::abs(coefficient);
411 }
412 reconstructionBound_ = std::max(reconstructionBound_, static_cast<double>(norm));
413 arithmeticBound_ = std::max(arithmeticBound_, static_cast<double>(arithmeticNorm));
414 }
415 setCeiling(ceiling);
416 }
417 void setCeiling(double ceiling)
418 {
419 ceiling_ = ceiling;
420 if constexpr (C == Curve::Tanh)
421 {
422 levels_ = {-24 * ceiling, -12 * ceiling, -6 * ceiling, -3 * ceiling, -ceiling, 0,
423 ceiling, 3 * ceiling, 6 * ceiling, 12 * ceiling, 24 * ceiling};
424 levelCount_ = 11;
425 }
426 else if constexpr (rational)
427 {
428 const auto limits = clipperLinearLimits<C>(ceiling);
429 levels_[0] = limits[0];
430 levels_[1] = limits[1];
431 levelCount_ = 2;
432 }
433 else
434 {
435 const double limit = C == Curve::Sine ? ceiling * halfPi<double> : ceiling;
436 levels_[0] = -limit;
437 levels_[1] = limit;
438 levelCount_ = 2;
439 }
440 }
441 // Convex-hull norm for the complete interpolation interval. Offline
442 // callers can bound finite-source tails using the same reconstruction.
443 [[nodiscard]] double reconstructionBound() const noexcept
444 {
445 return reconstructionBound_;
446 }
453 [[nodiscard]] Moments polynomial(const Poly &power) const noexcept
454 {
455 Poly bernstein{};
456 for (double coefficient : power)
457 if (!std::isfinite(coefficient))
458 return {std::numeric_limits<double>::quiet_NaN()};
459 if (std::all_of(power.begin(), power.end(), [](double value) { return value == 0; }))
460 return {};
461 for (int i = 0; i < count; ++i)
462 for (int j = 0; j <= i; ++j)
463 bernstein[i] += power[j] * bernsteinMatrix_[i][j];
464 return recurse(power, bernstein, 0, 1, 0);
465 }
466 Moments operator()(const std::array<double, count> &samples) const noexcept
467 {
468 if (std::all_of(samples.begin(), samples.end(), [](double value) { return value == 0; }))
469 return {};
470 if constexpr (SubtractLinear && (C == Curve::Hard || rational))
471 {
472 double low = samples[0], high = samples[0];
473 for (double sample : samples)
474 {
475 if (!std::isfinite(sample))
476 return {std::numeric_limits<double>::quiet_NaN()};
477 low = std::min(low, sample);
478 high = std::max(high, sample);
479 }
480 const double center = 0.5 * low + 0.5 * high;
481 const double radius = (0.5 * high - 0.5 * low) * reconstructionBound_;
482 // Covers both dot-product reductions, the rounded basis and the
483 // midpoint/radius arithmetic; count is at most twelve.
484 const double margin = 128 * std::numeric_limits<double>::epsilon() * arithmeticBound_ *
485 (std::abs(center) + radius) +
486 256 * std::numeric_limits<double>::denorm_min();
487 const auto limits = [&] {
488 if constexpr (rational)
489 return clipperLinearLimits<C>(ceiling_);
490 else
491 return std::array<double, 2>{-ceiling_, ceiling_};
492 }();
493 if (center - radius - margin >= limits[0] && center + radius + margin <= limits[1])
494 return {};
495 }
496 Poly power{}, bernstein{};
497 for (int i = 0; i < count; ++i)
498 for (int j = 0; j < count; ++j)
499 power[j] += samples[i] * matrix_[i][j];
500 for (int i = 0; i < count; ++i)
501 for (int j = 0; j <= i; ++j)
502 bernstein[i] += power[j] * bernsteinMatrix_[i][j];
503 return recurse(power, bernstein, 0, 1, 0);
504 }
505};
506
507inline std::vector<double> inverseBoxPower(int order)
508{
509 // (asin(sqrt(s))/sqrt(s))^4, where s=(2-z-z^-1)/4. Restores the
510 // integrated cubic B-spline passband without an IIR pole near Nyquist.
511 std::vector<double> series(static_cast<std::size_t>(order + 1));
512 for (int k = 0; k <= order; ++k)
513 series[k] = choose(2 * k, k) / std::pow(4., k) / (2 * k + 1);
514 std::vector<double> combined(static_cast<std::size_t>(order + 1));
515 combined[0] = 1;
516 for (int power = 0; power < 4; ++power)
517 {
518 std::vector<double> next(combined.size());
519 for (int i = 0; i <= order; ++i)
520 for (int j = 0; i + j <= order; ++j)
521 next[i + j] += combined[i] * series[j];
522 combined = std::move(next);
523 }
524 std::vector<double> taps(static_cast<std::size_t>(2 * order + 1)), basis(taps.size());
525 basis[order] = 1;
526 for (int k = 0; k <= order; ++k)
527 {
528 for (std::size_t i = 0; i < taps.size(); ++i)
529 taps[i] += combined[k] * basis[i];
530 if (k == order)
531 break;
532 std::vector<double> next(taps.size());
533 for (int j = order - k; j <= order + k; ++j)
534 {
535 next[j - 1] -= .25 * basis[j];
536 next[j] += .5 * basis[j];
537 next[j + 1] -= .25 * basis[j];
538 }
539 basis = std::move(next);
540 }
541 return taps;
542}
543
556template <Curve C, int Degree = 11, bool NativeSlope = false, bool SubtractLinear = true> class Residual final
557{
558 static constexpr int degree = Degree;
560 std::array<double, degree + 1> samples_{};
561 std::array<Moments, 4> history_{};
562 int sampleWrite_ = 0, momentWrite_ = 0;
563
564 public:
566 void reset(double ceiling) noexcept
567 {
568 integral_.setCeiling(ceiling);
569 samples_.fill(0);
570 history_.fill({});
571 sampleWrite_ = momentWrite_ = 0;
572 }
574 [[nodiscard]] std::vector<double> linearKernel() const
575 {
576 return integral_.linearKernel();
577 }
579 double process(double x) noexcept
580 {
581 samples_[sampleWrite_] = x;
582 sampleWrite_ = (sampleWrite_ + 1) % (degree + 1);
583 std::array<double, degree + 1> ordered{};
584 for (int i = 0; i <= degree; ++i)
585 ordered[i] = samples_[(sampleWrite_ + i) % (degree + 1)];
586 history_[momentWrite_] = integral_(ordered);
587 const auto &a = history_[(momentWrite_ + 1) % 4];
588 const auto &b = history_[(momentWrite_ + 2) % 4];
589 const auto &c = history_[(momentWrite_ + 3) % 4];
590 const auto &d = history_[momentWrite_];
591 const double out = (a[3] + b[0] + 3 * b[1] + 3 * b[2] - 3 * b[3] + 4 * c[0] - 6 * c[2] +
592 3 * c[3] + d[0] - 3 * d[1] + 3 * d[2] - d[3]) /
593 6;
594 momentWrite_ = (momentWrite_ + 1) % 4;
595 return out;
596 }
597};
598} // namespace dspark::detail::continuous_clip
Moments operator()(const std::array< double, count > &samples) const noexcept
std::vector< double > linearKernel() const
Moments polynomial(const Poly &power) const noexcept
Integrates an already reconstructed power polynomial on [0,1]. Returns integral(t^j * shape(p(t)),...
Streaming cubic B-spline moments of a reconstructed clipping residual.
double process(double x) noexcept
Processes one high-rate sample, returning its integrated residual.
std::vector< double > linearKernel() const
Constructs the matching linear FIR during setup.
void reset(double ceiling) noexcept
Resets reconstruction history and sets the positive finite ceiling.
std::array< double, 4 > Moments
std::vector< double > inverseBoxPower(int order)